@allorc/a423-sdk 0.2.0 → 0.4.0
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/CHANGELOG.md +18 -0
- package/ESCROW.md +162 -0
- package/INTEGRATION.md +14 -2
- package/LEGACY.md +251 -0
- package/NOTICE +24 -0
- package/OPERATIONS.md +182 -0
- package/README.md +65 -311
- package/SECURITY.md +11 -5
- package/contracts/A423Escrow.sol +135 -0
- package/contracts/standard-input.json +80 -0
- package/examples/operations-dashboard.html +16 -0
- package/examples/operations-server.mjs +19 -0
- package/package.json +67 -5
- package/scripts/deploy-escrow.mjs +24 -0
- package/scripts/operations-worker.mjs +22 -0
- package/src/authority/index.cjs +81 -0
- package/src/authority/index.d.ts +24 -0
- package/src/authority/index.mjs +2 -0
- package/src/escrow/artifact.json +661 -0
- package/src/escrow/index.cjs +150 -0
- package/src/escrow/index.d.mts +48 -0
- package/src/escrow/index.d.ts +48 -0
- package/src/escrow/index.mjs +2 -0
- package/src/escrow/services.cjs +119 -0
- package/src/operations/application.cjs +38 -0
- package/src/operations/factory.cjs +33 -0
- package/src/operations/index.cjs +105 -0
- package/src/operations/index.d.ts +29 -0
- package/src/operations/index.mjs +2 -0
- package/src/storage/index.cjs +83 -0
- package/src/storage/index.d.ts +37 -0
- package/src/storage/index.mjs +2 -0
- package/src/workflow.cjs +14 -2
- package/src/workflow.d.ts +3 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Dry-run by default. Explicit --broadcast deploys; --verify submits published sources to Sourcify.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {JsonRpcProvider,Wallet,ContractFactory,getAddress} from 'ethers';
|
|
4
|
+
import escrow from '../src/escrow/index.cjs';
|
|
5
|
+
const args=new Set(process.argv.slice(2));
|
|
6
|
+
const chainId=Number(process.env.A423_CHAIN_ID),token=getAddress(process.env.A423_TOKEN_ADDRESS);
|
|
7
|
+
if(!Number.isSafeInteger(chainId)||chainId<=0)throw Error('Explicit A423_CHAIN_ID required');
|
|
8
|
+
const provider=new JsonRpcProvider(process.env.A423_RPC_URL);if(Number((await provider.getNetwork()).chainId)!==chainId)throw Error('RPC chain mismatch');
|
|
9
|
+
if(await provider.getCode(token)==='0x')throw Error('Token has no code');
|
|
10
|
+
if(!args.has('--broadcast')){console.log(JSON.stringify({dryRun:true,chainId,token,bytecodeBytes:(escrow.ESCROW_BYTECODE.length-2)/2}));process.exit(0);}
|
|
11
|
+
if(!process.env.A423_DEPLOYER_KEY)throw Error('A423_DEPLOYER_KEY required for explicit deployment');
|
|
12
|
+
const signer=new Wallet(process.env.A423_DEPLOYER_KEY,provider),factory=new ContractFactory(escrow.ESCROW_ABI,escrow.ESCROW_BYTECODE,signer);
|
|
13
|
+
const contract=await factory.deploy(token);const receipt=await contract.deploymentTransaction().wait(Number(process.env.A423_CONFIRMATIONS||1));
|
|
14
|
+
const address=await contract.getAddress();const result={chainId,token,address,transaction:receipt.hash,verification:'not_requested'};
|
|
15
|
+
await fs.writeFile(process.env.A423_DEPLOYMENT_FILE||'a423-deployment.json',JSON.stringify(result,null,2)+'\n',{mode:0o600});
|
|
16
|
+
if(args.has('--verify')){
|
|
17
|
+
const stdJsonInput=JSON.parse(await fs.readFile(new URL('../contracts/standard-input.json',import.meta.url),'utf8'));
|
|
18
|
+
const artifact=JSON.parse(await fs.readFile(new URL('../src/escrow/artifact.json',import.meta.url),'utf8'));
|
|
19
|
+
const response=await fetch(`https://sourcify.dev/server/v2/verify/${chainId}/${address}`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({stdJsonInput,compilerVersion:artifact.compiler.split('.Emscripten')[0],contractIdentifier:'A423Escrow.sol:A423Escrow',creationTransactionHash:receipt.hash}),redirect:'error',signal:AbortSignal.timeout(30000)});
|
|
20
|
+
const verification=await response.json();if(!response.ok)throw Error('Deployed, but source verification submission failed: '+response.status);
|
|
21
|
+
result.verification='submitted';result.verificationRequest=verification;
|
|
22
|
+
await fs.writeFile(process.env.A423_DEPLOYMENT_FILE||'a423-deployment.json',JSON.stringify(result,null,2)+'\n',{mode:0o600});
|
|
23
|
+
}
|
|
24
|
+
console.log(JSON.stringify(result));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Opt-in operational loop; application config must explicitly supply signer approvals and execution callbacks.
|
|
2
|
+
import {pathToFileURL} from 'node:url';
|
|
3
|
+
import {setTimeout} from 'node:timers/promises';
|
|
4
|
+
import {watchEvidenceOnce,runMaintenanceOnce,enqueueRunningJobs,runExecutionOnce} from '../src/operations/index.mjs';
|
|
5
|
+
if(!process.env.A423_CONFIG_MODULE)throw Error('A423_CONFIG_MODULE required');
|
|
6
|
+
const {applicationOptions:app,workerOptions:worker}=await import(pathToFileURL(process.env.A423_CONFIG_MODULE).href);
|
|
7
|
+
if(!worker?.actorFor)throw Error('workerOptions.actorFor must explicitly delegate maintenance actors');
|
|
8
|
+
const controller=new AbortController();for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>controller.abort());
|
|
9
|
+
while(!controller.signal.aborted){
|
|
10
|
+
try{
|
|
11
|
+
await watchEvidenceOnce({store:app.store,revalidate:app.revalidate});
|
|
12
|
+
if(worker.index)await worker.index();
|
|
13
|
+
if(worker.replaceFees)await worker.replaceFees();
|
|
14
|
+
const maintenance=await runMaintenanceOnce({workflow:app.workflow,store:app.store,actorFor:worker.actorFor,allowExecute:worker.allowExecute===true});
|
|
15
|
+
for(const result of maintenance)if(result.error)console.error('A423 maintenance failed for job',result.id);
|
|
16
|
+
await enqueueRunningJobs({store:app.store,queue:app.queue});
|
|
17
|
+
if(worker.execute)await runExecutionOnce({workflow:app.workflow,queue:app.queue,execute:worker.execute,beforeExecute:worker.beforeExecute});
|
|
18
|
+
if(worker.webhooks){await worker.webhooks.project(app.store);for(let i=0;i<10;i++)if(!await worker.webhooks.deliverOne())break;}
|
|
19
|
+
if(worker.authority)await worker.authority.pruneChallenges();
|
|
20
|
+
}catch(error){console.error('A423 worker tick failed:',error.code||error.name);}
|
|
21
|
+
try{await setTimeout(5000,undefined,{signal:controller.signal});}catch{}
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {createPublicKey,sign,verify,randomBytes,createHash}=require('node:crypto');
|
|
3
|
+
const encode=x=>Buffer.from(JSON.stringify(x)).toString('base64url');
|
|
4
|
+
const fingerprint=key=>createHash('sha256').update(createPublicKey(key).export({type:'spki',format:'der'})).digest('hex');
|
|
5
|
+
function requireValue(ok,message){if(!ok)throw Error(message);}
|
|
6
|
+
function publicKey(pem){const key=createPublicKey(pem);requireValue(key.asymmetricKeyType==='ed25519','Ed25519 public key required');return key;}
|
|
7
|
+
function createAuthority({issuer,audience,privateKey,kv,credentialTtlSeconds=3600,challengeTtlSeconds=120,now=()=>Date.now()}={}){
|
|
8
|
+
requireValue(issuer&&audience&&privateKey&&kv,'Authority configuration required');publicKey(createPublicKey(privateKey).export({type:'spki',format:'pem'}));
|
|
9
|
+
requireValue(Number.isInteger(credentialTtlSeconds)&&credentialTtlSeconds>0&&credentialTtlSeconds<=86400,'Credential TTL must be 1..86400 seconds');
|
|
10
|
+
requireValue(Number.isInteger(challengeTtlSeconds)&&challengeTtlSeconds>0&&challengeTtlSeconds<=600,'Challenge TTL must be 1..600 seconds');
|
|
11
|
+
const ns='authority:'+issuer+':'+audience;
|
|
12
|
+
const read=(kind,id)=>kv.get(ns+':'+kind,id),put=(kind,id,v)=>kv.put(ns+':'+kind,id,v);
|
|
13
|
+
const safe=x=>requireValue(typeof x==='string'&&/^[A-Za-z0-9:_-]{1,256}$/.test(x),'Invalid identity');
|
|
14
|
+
async function records(agentId){const agent=await read('agents',agentId);requireValue(agent&&!agent.revoked,'Unknown/revoked agent');const org=await read('organizations',agent.organization);requireValue(org&&!org.revoked,'Unknown/revoked organization');return {agent,org};}
|
|
15
|
+
const registration=({organization,agentId,publicKey: key,scopes})=>({version:'a423.enrollment.v1',issuer,audience,organization,agent_id:agentId,key_fingerprint:fingerprint(key),scopes:[...scopes].sort()});
|
|
16
|
+
return {
|
|
17
|
+
publicKey:createPublicKey(privateKey).export({type:'spki',format:'pem'}),
|
|
18
|
+
registration,
|
|
19
|
+
async isActive(agentId){try{await records(agentId);return true;}catch{return false;}},
|
|
20
|
+
/** Administrative capability: perform real-world approval outside this module before calling. */
|
|
21
|
+
async registerOrganization({id,name,publicKey:key}){safe(id);requireValue(typeof name==='string'&&name.trim()&&name.length<=256,'Organization name required');publicKey(key);requireValue(await kv.create(ns+':organizations',id,{id,name,publicKey:key,revoked:false}),'Organization already registered');},
|
|
22
|
+
async revokeOrganization(id){const org=await read('organizations',id);requireValue(org,'Unknown organization');await put('organizations',id,{...org,revoked:true});},
|
|
23
|
+
async registerAgent({organization,agentId,publicKey:key,scopes=['workflow'],organizationSignature}){
|
|
24
|
+
safe(agentId);const org=await read('organizations',organization);requireValue(org&&!org.revoked,'Organization not approved');publicKey(key);
|
|
25
|
+
requireValue(Array.isArray(scopes)&&scopes.length>0&&scopes.length<=64&&scopes.every(s=>typeof s==='string'&&/^[a-z0-9:_-]{1,128}$/.test(s)),'Invalid scopes');
|
|
26
|
+
const statement=registration({organization,agentId,publicKey:key,scopes});
|
|
27
|
+
requireValue(verify(null,Buffer.from(JSON.stringify(statement)),publicKey(org.publicKey),Buffer.from(organizationSignature,'base64url')),'Invalid organization enrollment signature');
|
|
28
|
+
requireValue(await kv.create(ns+':agents',agentId,{id:agentId,organization,publicKey:key,scopes:[...new Set(scopes)],revoked:false}),'Agent already registered');
|
|
29
|
+
},
|
|
30
|
+
async revokeAgent(agentId){const agent=await read('agents',agentId);requireValue(agent,'Unknown agent');await put('agents',agentId,{...agent,revoked:true});},
|
|
31
|
+
async challenge(agentId){const {agent}=await records(agentId);const id=randomBytes(32).toString('base64url');const statement={version:'a423.challenge.v1',issuer,audience,id,agent_id:agentId,key_fingerprint:fingerprint(agent.publicKey),expires_at:Math.floor(now()/1000)+challengeTtlSeconds};await kv.create(ns+':challenges',id,statement);return statement;},
|
|
32
|
+
async exchange({challengeId,signature}){
|
|
33
|
+
safe(challengeId);const challenge=await kv.take(ns+':challenges',challengeId);requireValue(challenge&&challenge.expires_at>Math.floor(now()/1000),'Challenge expired, consumed or unknown');const {agent}=await records(challenge.agent_id);
|
|
34
|
+
requireValue(verify(null,Buffer.from(JSON.stringify(challenge)),publicKey(agent.publicKey),Buffer.from(signature,'base64url')),'Invalid challenge signature');
|
|
35
|
+
const issued=Math.floor(now()/1000);const payload={version:'a423.credential.v1',iss:issuer,aud:audience,sub:agent.id,org:agent.organization,key_fingerprint:fingerprint(agent.publicKey),scopes:agent.scopes,iat:issued,exp:issued+credentialTtlSeconds,jti:randomBytes(24).toString('base64url')};const content=encode(payload);return {credential:content+'.'+sign(null,Buffer.from(content),privateKey).toString('base64url'),expires_at:payload.exp};
|
|
36
|
+
},
|
|
37
|
+
async verifyCredential(credential,{scope='workflow'}={}){
|
|
38
|
+
requireValue(typeof credential==='string'&&credential.length<=16000,'Invalid credential');const parts=credential.split('.');requireValue(parts.length===2,'Invalid credential encoding');
|
|
39
|
+
requireValue(verify(null,Buffer.from(parts[0]),createPublicKey(privateKey),Buffer.from(parts[1],'base64url')),'Invalid authority signature');
|
|
40
|
+
const p=JSON.parse(Buffer.from(parts[0],'base64url').toString('utf8'));const time=Math.floor(now()/1000);
|
|
41
|
+
requireValue(p.version==='a423.credential.v1'&&p.iss===issuer&&p.aud===audience&&Number.isSafeInteger(p.exp)&&Number.isSafeInteger(p.iat)&&p.iat<=time&&p.exp>time&&p.exp-p.iat<=credentialTtlSeconds,'Expired or invalid credential');
|
|
42
|
+
const {agent}=await records(p.sub);requireValue(p.org===agent.organization&&p.key_fingerprint===fingerprint(agent.publicKey)&&p.scopes.includes(scope)&&agent.scopes.includes(scope),'Credential binding/scope mismatch');return p;
|
|
43
|
+
},
|
|
44
|
+
async pruneChallenges(){let removed=0;for(const r of await kv.list(ns+':challenges'))if(r.value.expires_at<=Math.floor(now()/1000)){await kv.take(ns+':challenges',r.id);removed++;}return removed;}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createCredentialAuthenticator({authority,scope='workflow'}={}){return async request=>{const h=request.headers.get('authorization')||'';if(!h.startsWith('Bearer '))return null;try{return (await authority.verifyCredential(h.slice(7),{scope})).sub;}catch{return null;}};}
|
|
48
|
+
/** Renew using a fresh proof of possession. Never prolong a credential from a bearer token alone. */
|
|
49
|
+
function createCredentialProvider({agentId,challenge,exchange,signChallenge,refreshBeforeSeconds=60,now=()=>Date.now()}={}){
|
|
50
|
+
let current=null,inflight=null;
|
|
51
|
+
return async()=>{
|
|
52
|
+
if(current&¤t.expires_at-Math.floor(now()/1000)>refreshBeforeSeconds)return current.credential;
|
|
53
|
+
if(!inflight)inflight=(async()=>{const c=await challenge(agentId);const signature=await signChallenge(Buffer.from(JSON.stringify(c)));current=await exchange({challengeId:c.id,signature});return current.credential;})().finally(()=>{inflight=null;});
|
|
54
|
+
return inflight;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
module.exports={createAuthority,createCredentialAuthenticator,createCredentialProvider};
|
|
58
|
+
/** Public proof endpoints only. Administrative enrollment is deliberately not exposed. */
|
|
59
|
+
function createAuthorityHandler({authority,basePath='/api/a423-auth',allowRequest=async()=>false}={}){
|
|
60
|
+
return async request=>{
|
|
61
|
+
const path=new URL(request.url).pathname;
|
|
62
|
+
if(request.method!=='POST'||![basePath+'/challenge',basePath+'/exchange'].includes(path))return Response.json({error:'not found'},{status:404});
|
|
63
|
+
if(await allowRequest(request)!==true)return Response.json({error:'rate limit or policy denied'},{status:429});
|
|
64
|
+
try{if(!request.body||!(request.headers.get('content-type')||'').startsWith('application/json'))throw Error('JSON required');const reader=request.body.getReader();let size=0;const chunks=[];try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>16384){await reader.cancel();throw Error('Body too large');}chunks.push(Buffer.from(value));}}finally{reader.releaseLock();}const body=JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
65
|
+
const result=path.endsWith('/challenge')?await authority.challenge(body.agent_id):await authority.exchange(body);
|
|
66
|
+
return Response.json(result,{headers:{'cache-control':'no-store'}});
|
|
67
|
+
}catch{return Response.json({error:'invalid proof or request'},{status:400});}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
module.exports.createAuthorityHandler=createAuthorityHandler;
|
|
71
|
+
/** Receiver-side verification uses public trust roots and a revocation/status lookup, never the authority private key. */
|
|
72
|
+
function createCredentialVerifier({issuer,audience,publicKey:trustedKey,isActive,maxTtlSeconds=86400,now=()=>Date.now()}={}){
|
|
73
|
+
const key=publicKey(trustedKey);requireValue(typeof isActive==='function','Explicit revocation/status lookup required');
|
|
74
|
+
return async(credential,{scope='workflow'}={})=>{
|
|
75
|
+
requireValue(typeof credential==='string'&&credential.length<=16000,'Invalid credential');const parts=credential.split('.');requireValue(parts.length===2&&verify(null,Buffer.from(parts[0]),key,Buffer.from(parts[1],'base64url')),'Invalid authority signature');
|
|
76
|
+
const p=JSON.parse(Buffer.from(parts[0],'base64url').toString('utf8')),time=Math.floor(now()/1000);
|
|
77
|
+
requireValue(p.version==='a423.credential.v1'&&p.iss===issuer&&p.aud===audience&&Number.isSafeInteger(p.exp)&&Number.isSafeInteger(p.iat)&&p.iat<=time&&p.exp>time&&p.exp-p.iat<=maxTtlSeconds&&Array.isArray(p.scopes)&&p.scopes.includes(scope),'Expired or invalid credential');
|
|
78
|
+
requireValue(await isActive(p)===true,'Agent/organization revoked or unavailable');return p;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
module.exports.createCredentialVerifier=createCredentialVerifier;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KeyObject } from 'node:crypto';
|
|
2
|
+
import type { KeyValueStore } from '../storage/index.js';
|
|
3
|
+
export interface Challenge {version: string;issuer: string;audience: string;id: string;agent_id: string;key_fingerprint: string;expires_at: number}
|
|
4
|
+
export interface Credential {credential: string;expires_at: number}
|
|
5
|
+
export interface Enrollment {organization: string;agentId: string;publicKey: string;scopes: string[]}
|
|
6
|
+
export interface Authority {
|
|
7
|
+
publicKey: string;
|
|
8
|
+
isActive(agentId: string): Promise<boolean>;
|
|
9
|
+
registration(input: Enrollment): object;
|
|
10
|
+
registerOrganization(input: {id: string;name: string;publicKey: string}): Promise<void>;
|
|
11
|
+
revokeOrganization(id: string): Promise<void>;
|
|
12
|
+
registerAgent(input: Enrollment & {organizationSignature: string}): Promise<void>;
|
|
13
|
+
revokeAgent(id: string): Promise<void>;
|
|
14
|
+
challenge(agentId: string): Promise<Challenge>;
|
|
15
|
+
exchange(input: {challengeId: string;signature: string}): Promise<Credential>;
|
|
16
|
+
verifyCredential(token: string, options?: {scope?: string}): Promise<{sub: string;org: string;scopes: string[];exp: number;[key: string]: unknown}>;
|
|
17
|
+
pruneChallenges(): Promise<number>;
|
|
18
|
+
}
|
|
19
|
+
export function createAuthority(options: {issuer: string;audience: string;privateKey: string | KeyObject;kv: KeyValueStore;credentialTtlSeconds?: number;challengeTtlSeconds?: number;now?(): number}): Authority;
|
|
20
|
+
export function createCredentialAuthenticator(options: {authority: Pick<Authority,'verifyCredential'>;scope?: string}): (request: Request) => Promise<string | null>;
|
|
21
|
+
export function createCredentialProvider(options: {agentId: string;challenge(id: string): Promise<Challenge>;exchange(input: {challengeId: string;signature: string}): Promise<Credential>;signChallenge(bytes: Buffer): Promise<string>;refreshBeforeSeconds?: number;now?(): number}): () => Promise<string>;
|
|
22
|
+
export function createAuthorityHandler(options: {authority: Authority;basePath?: string;allowRequest?(request: Request): Promise<boolean>}): (request: Request) => Promise<Response>;
|
|
23
|
+
|
|
24
|
+
export function createCredentialVerifier(options: {issuer: string;audience: string;publicKey: string;isActive(payload: any): Promise<boolean>;maxTtlSeconds?: number;now?(): number}): Authority['verifyCredential'];
|