@allorc/a423-sdk 0.6.0 → 0.8.1

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/SECURITY.md CHANGED
@@ -17,3 +17,7 @@ Reorg watchers quarantine stale/unavailable evidence; they cannot guarantee fina
17
17
  Report vulnerabilities privately through the repository's configured security reporting channel. Do not post credentials, customer records or live exploit details publicly. No completed independent audit is claimed.
18
18
 
19
19
  Closing settlements in 0.5 require a new deployment and exact current proposal acceptance. Publishing freezes ordinary release, but cannot reverse a release already mined. Proposals never extend original refund/resolution deadlines. Unsubmitted partial-work claims do not remove the original expiry refund right; arbitrary expenses are not automatically owed. First valid closing settlement/refund/arbitration/timeout wins; no later operation can settle the same job again. See SETTLEMENT.md.
20
+
21
+ ## Off-chain profit policy (0.8)
22
+
23
+ Private cost snapshots and exception decisions live in backend KV, not agreement terms/events. Configure role-aware projectors and never share worker-private responses through public caches. The policy is opt-in and cannot constrain direct external contract calls or prove costs are real. Owner exceptions must be checked by a trusted server approval system, never client flags. Malformed-pending recovery requires an adapter impossibility proof and refuses any known signed/external intent; do not add permissive recovery hooks. See PROFIT-POLICY.md.
package/SETTLEMENT.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Negotiated closing settlements — 0.5.0
2
2
 
3
+ [Start here: installation, arguments and lifecycle maps](./README.md) · [All examples](./examples/README.md)
4
+
3
5
  For the 0.6 accepted cancellation JSON → escrow execution → confirmed receipt subflow, see [CANCELLATION.md](./CANCELLATION.md). The 0.5 contract is unchanged; an accepted instruction is not proof of payment.
4
6
 
5
7
  This release adds a closing negotiation for cancellation and partial-work compensation. A party proposes a worker payment, the other can counter or accept, and the contract pays the agreed amount and refunds the remainder atomically. It closes the whole job. **It does not introduce partial releases while continuing work or milestone payments.**
@@ -0,0 +1,29 @@
1
+ # A423 examples
2
+
3
+ Start with the [main README](../README.md) for installation, configuration arguments and lifecycle diagrams. These examples target the source version in `package.json`; check the installed npm version before copying newer APIs.
4
+
5
+ | Example | Purpose | Chain activity |
6
+ |---|---|---|
7
+ | [quickstart.mjs](./quickstart.mjs) | Runnable in-memory agreement and pure profit calculation | None; no wallet or funding adapter |
8
+ | [operations-server.mjs](./operations-server.mjs) | Localhost HTTP shell around a configured application handler | Only through your explicitly configured adapters/signers |
9
+ | [operations-dashboard.html](./operations-dashboard.html) | Reference UI for states, wallet consent, cancellation JSON, receipts and private economics | Actions use the authenticated backend |
10
+ | [nextjs/lib/a423.ts](./nextjs/lib/a423.ts) | Older root API example | No settlement adapter; records approval only |
11
+ | [client.mjs](./client.mjs) | Older root API client example | Depends on the configured server; not the reference workflow client |
12
+
13
+ Run the no-chain example from the source repository:
14
+
15
+ ```sh
16
+ node examples/quickstart.mjs
17
+ ```
18
+
19
+ After installing the package in another app, copy the quickstart file and run it there; it imports public package entry points.
20
+
21
+ For the modern escrow path, construct `createEscrowApplication` with your authority, durable storage, deployment registry, signing/spending approvals and artifact validator. Mount its handler in Next.js with the Node runtime or use the local shell. The old `examples/nextjs` code intentionally demonstrates the preserved root API; do not mistake it for the new escrow workflow.
22
+
23
+ The shell requires your own configuration module exporting `applicationOptions`:
24
+
25
+ ```sh
26
+ A423_CONFIG_MODULE=/absolute/path/config.mjs node examples/operations-server.mjs
27
+ ```
28
+
29
+ The operational worker separately requires `workerOptions`; see [OPERATIONS.md](../OPERATIONS.md). Installing a package, mounting an HTTP handler and running a keeper are separate steps. No example supplies default credentials, signing keys or a deployed contract.
@@ -7,10 +7,10 @@
7
7
  const $=id=>document.getElementById(id);let credential='';
8
8
  async function api(path,body,key){const response=await fetch(path,{method:body===undefined?'GET':'POST',headers:{authorization:'Bearer '+credential,...(body===undefined?{}:{'content-type':'application/json'}),...(key?{'idempotency-key':key}:{})},body:body===undefined?undefined:JSON.stringify(body)});const data=await response.json();if(!response.ok)throw Error(JSON.stringify(data));return data;}
9
9
  function show(error){$('status').textContent=error.message||String(error);}
10
- async function load(){credential=$('token').value;const data=await api('/api/state');$('jobs').replaceChildren();for(const job of data.a423.jobs){const article=document.createElement('article');const title=document.createElement('h2');title.textContent=job.id+' · '+job.state+(job.safety?.blocked?' · SAFETY HOLD':'')+(job.pending?' · '+job.pending.action+' pending':'');article.append(title);const info=document.createElement('pre');info.textContent=JSON.stringify({work_status:job.work_status,escrow_ledger:job.escrow_ledger,negotiation:job.negotiation,subflows:job.subflows,active_subflow_id:job.active_subflow_id,safety:job.safety,participants:job.participants,agreement:job.agreement,submission:job.submission,evidence:job.evidence},null,2);article.append(info);
11
- const action=document.createElement('select');for(const value of ['quote','accept','fund','start','submit','verify','release','refund','dispute','resolve','timeout','propose_settlement','counter_settlement','approve_cancellation','execute_cancellation','accept_settlement','cancel','expire']){const o=document.createElement('option');o.textContent=value;action.append(o);}const input=document.createElement('textarea');input.value='{}';action.onchange=()=>{const p=job.negotiation?.current;if(['propose_settlement','counter_settlement'].includes(action.value))input.value=JSON.stringify({expected_revision:p?.escrow_version||0,worker_amount:p?.worker_amount||'0',expires_at:Math.min(Math.floor(Date.now()/1000)+3600,job.agreement?.terms.escrow.resolution_deadline||0),reason:'Explain completed work and proposed compensation',claims:[]},null,2);if(['accept_settlement','approve_cancellation'].includes(action.value)&&p)input.value=JSON.stringify({expected_revision:p.escrow_version,proposal_hash:p.proposal_hash},null,2);if(action.value==='execute_cancellation')input.value=JSON.stringify({instruction_id:job.subflows?.find(s=>s.kind==='cancellation')?.instruction?.id||''},null,2);};const key=document.createElement('input');key.value=crypto.randomUUID();key.title='Keep this key when retrying the same action';const run=document.createElement('button');run.textContent='Run action';run.onclick=async()=>{try{await api('/api/a423/jobs/'+job.id+(action.value==='approve_cancellation'?'/cancellation/accept':'/actions'),action.value==='approve_cancellation'?JSON.parse(input.value):{action:action.value,input:JSON.parse(input.value)},key.value);await load();}catch(e){show(e);}};
10
+ async function load(){credential=$('token').value;const data=await api('/api/state');$('jobs').replaceChildren();for(const job of data.a423.jobs){const article=document.createElement('article');const title=document.createElement('h2');title.textContent=job.id+' · '+job.state+(job.safety?.blocked?' · SAFETY HOLD':'')+(job.pending?' · '+job.pending.action+' pending':'');article.append(title);if(job.profit){const economics=document.createElement('p');const p=job.profit;economics.textContent=p.cost_basis_raw!==undefined?'Raw token units · Cost: '+p.cost_basis_raw+' · Target payout: '+p.target_payout+' · Minimum: '+p.min_payout+' · Current: '+p.projected_payout_raw+' · '+(p.receipt_confirmed?'Realized':'Projected')+' profit: '+(p.receipt_confirmed?p.realized_profit_raw:p.projected_profit_raw)+' · Margin bps: '+(p.receipt_confirmed?p.realized_margin_bps:p.projected_margin_bps)+' · '+p.target_status:'Proposed payout: '+(p.proposed_payout_raw??'—')+' · Confirmed payout: '+(p.realized_payout_raw??'pending');article.append(economics);}const info=document.createElement('pre');info.textContent=JSON.stringify({epoch:job.epoch,pre_funding_cancellations:job.pre_funding_cancellations,profit:job.profit,work_status:job.work_status,escrow_ledger:job.escrow_ledger,negotiation:job.negotiation,subflows:job.subflows,active_subflow_id:job.active_subflow_id,safety:job.safety,participants:job.participants,agreement:job.agreement,submission:job.submission,evidence:job.evidence},null,2);article.append(info);
11
+ const action=document.createElement('select');for(const value of ['quote','accept','fund','start','submit','verify','release','refund','dispute','resolve','timeout','propose_settlement','counter_settlement','approve_cancellation','execute_cancellation','accept_settlement','cancel','decline','reopen','expire']){const o=document.createElement('option');o.textContent=value;action.append(o);}const input=document.createElement('textarea');input.value='{}';action.onchange=()=>{input.value=JSON.stringify({expected_epoch:job.epoch||0},null,2);if(action.value==='reopen')input.value=JSON.stringify({cancellation_id:job.pre_funding_cancellations?.at(-1)?.id},null,2);const p=job.negotiation?.current;if(['propose_settlement','counter_settlement'].includes(action.value))input.value=JSON.stringify({expected_revision:p?.escrow_version||0,worker_amount:p?.worker_amount||'0',expires_at:Math.min(Math.floor(Date.now()/1000)+3600,job.agreement?.terms.escrow.resolution_deadline||0),reason:'Explain completed work and proposed compensation',claims:[]},null,2);if(['accept_settlement','approve_cancellation'].includes(action.value)&&p)input.value=JSON.stringify({expected_revision:p.escrow_version,proposal_hash:p.proposal_hash},null,2);if(action.value==='execute_cancellation')input.value=JSON.stringify({instruction_id:job.subflows?.find(s=>s.kind==='cancellation')?.instruction?.id||''},null,2);};const key=document.createElement('input');key.value=crypto.randomUUID();key.title='Keep this key when retrying the same action';const run=document.createElement('button');run.textContent='Run action';run.onclick=async()=>{try{await api('/api/a423/jobs/'+job.id+(action.value==='approve_cancellation'?'/cancellation/accept':'/actions'),action.value==='approve_cancellation'?{expected_epoch:job.epoch||0,...JSON.parse(input.value)}:{action:action.value,input:{expected_epoch:job.epoch||0,...JSON.parse(input.value)}},key.value);await load();}catch(e){show(e);}};
12
12
  const consent=document.createElement('button');consent.textContent='Sign worker consent with wallet';consent.onclick=async()=>{try{if(!window.ethereum)throw Error('EIP-1193 wallet required');const typed=await api('/api/a423-worker-consent/'+job.id);const accounts=await window.ethereum.request({method:'eth_requestAccounts'});if(accounts[0].toLowerCase()!==typed.value.worker.toLowerCase())throw Error('Choose the agreed worker account');const signature=await window.ethereum.request({method:'eth_signTypedData_v4',params:[accounts[0],JSON.stringify({domain:typed.domain,primaryType:'Terms',types:{EIP712Domain:[{name:'name',type:'string'},{name:'version',type:'string'},{name:'chainId',type:'uint256'},{name:'verifyingContract',type:'address'}],...typed.types},message:typed.value})]});input.value=JSON.stringify({worker_signature:signature},null,2);action.value='fund';show('Worker signed. Share the public signature with the authenticated payer to fund.');}catch(e){show(e);}};
13
13
  const receipts=document.createElement('button');receipts.textContent='Fresh JSON receipt';receipts.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423-receipts/'+job.id),null,2);}catch(e){show(e);}};const pdf=document.createElement('button');pdf.textContent='Fresh PDF';pdf.onclick=async()=>{try{const r=await fetch('/api/a423-receipts/'+job.id+'.pdf',{headers:{authorization:'Bearer '+credential}});if(!r.ok)throw Error('Evidence unavailable');const url=URL.createObjectURL(await r.blob());const a=document.createElement('a');a.href=url;a.download=job.id+'.pdf';a.click();setTimeout(()=>URL.revokeObjectURL(url),10000);}catch(e){show(e);}};
14
- const closing=document.createElement('button');closing.textContent='Sign current closing split';closing.onclick=async()=>{try{if(!window.ethereum)throw Error('EIP-1193 wallet required');const typed=await api('/api/a423-settlement-consent/'+job.id);const accounts=await window.ethereum.request({method:'eth_requestAccounts'});const address=accounts[0].toLowerCase(),terms=job.agreement.terms.escrow;const field=address===terms.payer.toLowerCase()?'payer_signature':address===terms.worker.toLowerCase()?'worker_signature':null;if(!field)throw Error('Choose the agreed payer or worker wallet');if(!confirm('Close this job: '+typed.value.workerAmount+' raw units to worker, '+typed.value.payerAmount+' refunded. Review the claim and wallet prompt.'))return;const signature=await window.ethereum.request({method:'eth_signTypedData_v4',params:[accounts[0],JSON.stringify({domain:typed.domain,primaryType:'ClosingSettlement',types:{EIP712Domain:[{name:'name',type:'string'},{name:'version',type:'string'},{name:'chainId',type:'uint256'},{name:'verifyingContract',type:'address'}],...typed.types},message:typed.value})]});let previous={};try{previous=JSON.parse(input.value);}catch{}input.value=JSON.stringify({...previous,expected_revision:typed.value.escrowVersion,proposal_hash:typed.value.proposalHash,[field]:signature},null,2);action.value='approve_cancellation';show('Signed closing split. Combine both wallet signatures, then Run action to save acceptance and forward to escrow.');}catch(e){show(e);}};const cancellation=document.createElement('button');cancellation.textContent='Confirmed cancellation receipt';cancellation.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423/jobs/'+job.id+'/cancellation/receipt'),null,2);}catch(e){show(e);}};const instruction=document.createElement('button');instruction.textContent='Accepted cancellation JSON';instruction.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423/jobs/'+job.id+'/cancellation/instruction'),null,2);}catch(e){show(e);}};article.append(action,input,key,run,consent,closing,instruction,cancellation,receipts,pdf);$('jobs').append(article);}show('Loaded '+data.a423.jobs.length+' workflows');}
14
+ const closing=document.createElement('button');closing.textContent='Sign current closing split';closing.onclick=async()=>{try{if(!window.ethereum)throw Error('EIP-1193 wallet required');const typed=await api('/api/a423-settlement-consent/'+job.id);const accounts=await window.ethereum.request({method:'eth_requestAccounts'});const address=accounts[0].toLowerCase(),terms=job.agreement.terms.escrow;const field=address===terms.payer.toLowerCase()?'payer_signature':address===terms.worker.toLowerCase()?'worker_signature':null;if(!field)throw Error('Choose the agreed payer or worker wallet');if(!confirm('Close this job: '+typed.value.workerAmount+' raw units to worker, '+typed.value.payerAmount+' refunded. Review the claim and wallet prompt.'))return;const signature=await window.ethereum.request({method:'eth_signTypedData_v4',params:[accounts[0],JSON.stringify({domain:typed.domain,primaryType:'ClosingSettlement',types:{EIP712Domain:[{name:'name',type:'string'},{name:'version',type:'string'},{name:'chainId',type:'uint256'},{name:'verifyingContract',type:'address'}],...typed.types},message:typed.value})]});let previous={};try{previous={expected_epoch:job.epoch||0,...JSON.parse(input.value)};}catch{}input.value=JSON.stringify({...previous,expected_revision:typed.value.escrowVersion,proposal_hash:typed.value.proposalHash,[field]:signature},null,2);action.value='approve_cancellation';show('Signed closing split. Combine both wallet signatures, then Run action to save acceptance and forward to escrow.');}catch(e){show(e);}};const cancellation=document.createElement('button');cancellation.textContent='Confirmed cancellation receipt';cancellation.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423/jobs/'+job.id+'/cancellation/receipt'),null,2);}catch(e){show(e);}};const instruction=document.createElement('button');instruction.textContent='Accepted cancellation JSON';instruction.onclick=async()=>{try{info.textContent=JSON.stringify(await api('/api/a423/jobs/'+job.id+'/cancellation/instruction'),null,2);}catch(e){show(e);}};article.append(action,input,key,run,consent,closing,instruction,cancellation,receipts,pdf);$('jobs').append(article);}show('Loaded '+data.a423.jobs.length+' workflows');}
15
15
  $('refresh').onclick=()=>load().catch(show);$('createButton').onclick=async()=>{try{credential=$('token').value;await api('/api/a423/jobs',JSON.parse($('create').value));await load();}catch(e){show(e);}};
16
16
  </script></html>
@@ -0,0 +1,33 @@
1
+ // In-memory workflow and policy example. No on-chain transactions or keys.
2
+ import {createMemoryStore} from '@allorc/a423-sdk';
3
+ import {createWorkflow} from '@allorc/a423-sdk/workflow';
4
+ import {createSettlementPolicy} from '@allorc/a423-sdk/policy';
5
+
6
+ const workflow = createWorkflow({store: createMemoryStore()});
7
+ const id = 'research-001';
8
+ await workflow.create({
9
+ id,
10
+ participants: {requester: 'buyer', worker: 'researcher', verifier: 'reviewer'},
11
+ }, 'buyer');
12
+
13
+ let job = await workflow.act(id, 'quote', {
14
+ terms: {description: 'Prepare a research brief', amount_raw: '200'},
15
+ }, {actor: 'researcher', idempotencyKey: 'quote-1'});
16
+
17
+ const acceptance = {
18
+ revision: job.agreement.revision,
19
+ agreement_hash: job.agreement.hash,
20
+ };
21
+ await workflow.act(id, 'accept', acceptance, {
22
+ actor: 'buyer', idempotencyKey: 'buyer-accept-1',
23
+ });
24
+ job = await workflow.act(id, 'accept', acceptance, {
25
+ actor: 'researcher', idempotencyKey: 'worker-accept-1',
26
+ });
27
+
28
+ const plan = createSettlementPolicy().evaluate({
29
+ cost_basis_raw: '100', budget_raw: '300', escrow_raw: '300',
30
+ });
31
+ console.log(job.state); // accepted; no funding has occurred
32
+ console.log(plan.target_payout); // 200: 50% gross-margin target
33
+ console.log(plan.recommended_payout);// 200: fits budget and escrow caps
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@allorc/a423-sdk",
3
- "version": "0.6.0",
4
- "description": "Experimental Next.js-friendly agent job SDK with typed HTTP client and pluggable storage, authentication and settlement",
3
+ "version": "0.8.1",
4
+ "description": "Experimental backend SDK for agent job agreements, durable workflows, ERC-20 escrow, negotiated settlement and configurable profit policies",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
7
7
  "main": "./src/index.cjs",
@@ -41,6 +41,11 @@
41
41
  "types": "./src/operations/index.d.ts",
42
42
  "import": "./src/operations/index.mjs",
43
43
  "require": "./src/operations/index.cjs"
44
+ },
45
+ "./policy": {
46
+ "types": "./src/policy/index.d.ts",
47
+ "import": "./src/policy/index.mjs",
48
+ "require": "./src/policy/index.cjs"
44
49
  }
45
50
  },
46
51
  "files": [
@@ -75,7 +80,11 @@
75
80
  "src/closing.cjs",
76
81
  "SETTLEMENT.md",
77
82
  "src/cancellation.cjs",
78
- "CANCELLATION.md"
83
+ "CANCELLATION.md",
84
+ "src/policy",
85
+ "PROFIT-POLICY.md",
86
+ "examples/quickstart.mjs",
87
+ "examples/README.md"
79
88
  ],
80
89
  "engines": {
81
90
  "node": ">=22.13"
@@ -201,7 +201,14 @@ function createEscrowAdapters({provider,chainId,contractAddress,tokenAddress,sig
201
201
  return observe(ctx,t,'fund','fund',[t,ctx.input.worker_signature],'Funded',(s,e)=>{check(Number(s.state)>0,'missing funded job');check(e.args.amount===t.amount && getAddress(e.args.payer)===t.payer && getAddress(e.args.worker)===t.worker && e.args.agreementHash===t.agreementHash,'funding event mismatch');},reconcile);
202
202
  }),
203
203
  start:adapter(async ctx=>{const t=await context(ctx.job);const block=await provider.getBlock('latest');const safeNumber=block.number-confirmations+1;check(safeNumber>=0,'insufficient confirmation depth');const safe=await provider.getBlock(safeNumber);const confirmed=await contract.getJob(t.jobId,{blockTag:safeNumber});match(confirmed,t);state(confirmed,1);const s=await contract.getJob(t.jobId,{blockTag:block.number});match(s,t);state(s,1);check(block.timestamp<=t.workDeadline,'work deadline passed');return {status:'confirmed',evidence:{chain_id:String(chainId),contract:config.contractAddress,job_id:t.jobId,block_number:safe.number,block_hash:safe.hash,confirmations,amount:t.amount.toString(),agreement_hash:t.agreementHash}};}),
204
- submit:adapter(async(ctx,r)=>{const t=await context(ctx.job);const hash=bytes32(ctx.input.result_hash);return observe(ctx,t,'submit','submit',[t.jobId,hash],'Submitted',(s,e)=>{check(Number(s.state)>=2,'missing submission');check(s.resultHash===hash && e.args.resultHash===hash,'artifact commitment mismatch');},r);}),
204
+ submit:Object.assign(adapter(async(ctx,r)=>{const t=await context(ctx.job);const hash=bytes32(ctx.input.result_hash);return observe(ctx,t,'submit','submit',[t.jobId,hash],'Submitted',(s,e)=>{check(Number(s.state)>=2,'missing submission');check(s.resultHash===hash && e.args.resultHash===hash,'artifact commitment mismatch');},r);}),{rejectPending:async ctx=>{
205
+ const t=await context(ctx.job);let malformed=false;try{bytes32(ctx.input.result_hash);}catch{malformed=true;}
206
+ check(malformed&&!ctx.input.transaction_hash&&!ctx.operation.observation?.reference&&!ctx.operation.observation?.transaction_hash&&!ctx.operation.observation?.evidence?.transaction_hash,'Only syntactically impossible submissions without external transaction references can be rejected');
207
+ check(typeof transactions.list==='function','Transaction journal inventory required');
208
+ check(!(await transactions.list()).some(record=>record.id===ctx.operation.id||record.id.startsWith(ctx.operation.id+':')),'Journaled intent exists; reconcile instead');
209
+ const snapshot=await contract.getJob(t.jobId);match(snapshot,t);check(snapshot.resultHash===ZeroHash,'On-chain submission exists; reconcile instead');
210
+ return {status:'rejected',never_executable:true,reason:'Malformed result_hash cannot encode a submit transaction',checked_chain_id:String(chainId)};
211
+ }}),
205
212
  verify:adapter(async(ctx,r)=>{const t=await context(ctx.job);const hash=bytes32(ctx.job.submission?.result_hash);if(!r && !await transactions.get(ctx.operation.id+':verify')){check(typeof validateArtifact==='function','validateArtifact callback required');if(await validateArtifact({job:structuredClone(ctx.job),resultHash:hash})!==true)return {status:'rejected',reason:'Artifact verification failed'};}return observe(ctx,t,'verify','verify',[t.jobId,hash],'Verified',(s,e)=>{check(Number(s.state)>=3,'missing verification');check(s.resultHash===hash && e.args.resultHash===hash,'verified artifact mismatch');},r);}),
206
213
  release:adapter(async(ctx,r)=>{const t=await context(ctx.job);return observe(ctx,t,'release','release',[t.jobId],'Settled',(s,e)=>{state(s,5);settled(s,e,t);check(e.args.workerAmount===t.amount && Number(e.args.reason)===0,'not a full release');},r);}),
207
214
  refund:adapter(async(ctx,r)=>{const t=await context(ctx.job);return observe(ctx,t,'refund','refundExpired',[t.jobId],'Settled',(s,e)=>{state(s,6);settled(s,e,t);check(e.args.payerAmount===t.amount && Number(e.args.reason)===1,'not an expiry refund');},r);}),
@@ -16,7 +16,7 @@ export interface TransactionStore {
16
16
  /** Atomically reserve a nonce per chain/address; same operationId always returns same nonce. */
17
17
  reserveNonce(scope: string, minimum: number, operationId: string): Promise<number>;
18
18
  }
19
- export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult> }
19
+ export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult>;rejectPending?(context: AdapterContext): Promise<AdapterResult> }
20
20
  export interface EscrowAdapterOptions extends EscrowConfiguration {
21
21
  provider: Provider;
22
22
  /** Requires populateTransaction + offline signTransaction support; no silent wallet approval. */
@@ -16,7 +16,7 @@ export interface TransactionStore {
16
16
  /** Atomically reserve a nonce per chain/address; same operationId always returns same nonce. */
17
17
  reserveNonce(scope: string, minimum: number, operationId: string): Promise<number>;
18
18
  }
19
- export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult> }
19
+ export interface EscrowAdapter { execute(context: AdapterContext): Promise<AdapterResult>; reconcile(context: AdapterContext): Promise<AdapterResult>;rejectPending?(context: AdapterContext): Promise<AdapterResult> }
20
20
  export interface EscrowAdapterOptions extends EscrowConfiguration {
21
21
  provider: Provider;
22
22
  /** Requires populateTransaction + offline signTransaction support; no silent wallet approval. */
@@ -5,7 +5,7 @@ const {validateEscrowEvidence}=require('../operations/index.cjs');
5
5
  function check(ok,message){if(!ok)throw Error(message);}
6
6
  function createEscrowRegistry(configurations){
7
7
  const map=new Map();for(const c of configurations){const key=c.chainId+':'+getAddress(c.contractAddress).toLowerCase();check(!map.has(key),'Duplicate escrow configuration');map.set(key,createEscrowAdapters(c));}
8
- const actions=['fund','start','submit','verify','release','refund','dispute','resolve','timeout','propose_settlement','counter_settlement','accept_settlement','approve_cancellation','execute_cancellation'];const result={};for(const action of actions){result[action]={};for(const method of ['execute','reconcile'])result[action][method]=ctx=>{const e=ctx.job.agreement?.terms.escrow;const adapter=map.get(e?.chain_id+':'+getAddress(e?.contract).toLowerCase());check(adapter,'Escrow not registered');return adapter[action][method](ctx);};}return result;
8
+ const actions=['fund','start','submit','verify','release','refund','dispute','resolve','timeout','propose_settlement','counter_settlement','accept_settlement','approve_cancellation','execute_cancellation'];const result={};for(const action of actions){result[action]={};for(const method of ['execute','reconcile','rejectPending'])result[action][method]=ctx=>{const e=ctx.job.agreement?.terms.escrow;const adapter=map.get(e?.chain_id+':'+getAddress(e?.contract).toLowerCase());check(adapter,'Escrow not registered');check(typeof adapter[action][method]==='function','Recovery unsupported for this action');return adapter[action][method](ctx);};}return result;
9
9
  }
10
10
  /** Fresh historical evidence verification. Returns invalid, insufficient_confirmations, or valid; RPC failures throw. */
11
11
  function createEvidenceRevalidator({provider,chainId,contractAddress,tokenAddress,confirmations}={}){
@@ -1,18 +1,21 @@
1
1
  'use strict';
2
2
  const {createWorkflowHandler}=require('../workflow.cjs');
3
- function createChatTools({workflow,store,queue,actor}={}){
3
+ function createChatTools({workflow,store,queue,actor,profitFor,policyPromptFor}={}){
4
4
  const {enqueueRunningJobs}=require('./index.cjs');
5
5
  const act=(action)=>async({job_id,input={},idempotency_key})=>workflow.act(job_id,action,input,{actor,idempotencyKey:idempotency_key});
6
6
  return {
7
+ list_jobs:async()=>Promise.all((await store.list({actor,limit:1000})).map(async job=>profitFor?{...job,profit:await profitFor(job,actor)}:job)),
8
+ inspect_settlement_policy:async({job_id})=>{const job=await workflow.get(job_id,actor);if(!profitFor)throw Error('Settlement policy not configured');return {profit:await profitFor(job,actor),prompt:policyPromptFor?policyPromptFor(job,actor):null};},
9
+ reconcile_job:async({job_id})=>workflow.reconcile(job_id,actor),
7
10
  send_job_offer:async({id,worker_id,verifier_id,arbitrator_id,terms,metadata={}})=>{
8
11
  let job=await workflow.create({id,participants:{requester:actor,worker:worker_id,verifier:verifier_id,arbitrator:arbitrator_id},metadata},actor);
9
12
  if(terms)job=await workflow.act(job.id,'quote',{terms},{actor,idempotencyKey:'initial_quote'});return job;
10
13
  },
11
- propose_quote:act('quote'),accept_quote:act('accept'),fund_escrow:act('fund'),submit_job:act('submit'),verify_job:act('verify'),release_escrow:act('release'),refund_escrow:act('refund'),dispute_job:act('dispute'),resolve_job:act('resolve'),timeout_job:act('timeout'),propose_settlement:act('propose_settlement'),counter_settlement:act('counter_settlement'),accept_settlement:act('accept_settlement'),accept_cancellation:async({job_id,input={},idempotency_key})=>workflow.acceptCancellation(job_id,input,{actor,idempotencyKey:idempotency_key}),execute_cancellation:act('execute_cancellation'),request_cancellation:act('propose_settlement'),cancel_job:act('cancel'),
14
+ propose_quote:act('quote'),accept_quote:act('accept'),fund_escrow:act('fund'),submit_job:act('submit'),verify_job:act('verify'),release_escrow:act('release'),refund_escrow:act('refund'),dispute_job:act('dispute'),resolve_job:act('resolve'),timeout_job:act('timeout'),propose_settlement:act('propose_settlement'),counter_settlement:act('counter_settlement'),accept_settlement:act('accept_settlement'),accept_cancellation:async({job_id,input={},idempotency_key})=>workflow.acceptCancellation(job_id,input,{actor,idempotencyKey:idempotency_key}),execute_cancellation:act('execute_cancellation'),request_cancellation:act('propose_settlement'),cancel_job:act('cancel'),decline_job:act('decline'),reopen_job:act('reopen'),
12
15
  work_on_job:async(input)=>{const job=await act('start')(input);if(!job.pending&&job.state==='running')await enqueueRunningJobs({store,queue});return job;}
13
16
  };
14
17
  }
15
- function createApplicationHandler({workflow,store,queue,authenticate,revalidate,consentFor,settlementConsentFor,legacyState=async()=>({}),authorityHandler,forwardCancellations=true,basePath='/api/a423'}={}){
18
+ function createApplicationHandler({workflow,store,queue,authenticate,revalidate,consentFor,settlementConsentFor,profitFor,legacyState=async()=>({}),authorityHandler,forwardCancellations=true,basePath='/api/a423'}={}){
16
19
  const {createEventStream,freshReceipts,renderReceiptPdf}=require('./index.cjs');
17
20
  const protocol=createWorkflowHandler({workflow,authenticate,basePath,forwardCancellations,revalidateCancellationReceipt:async({job,receipt})=>{
18
21
  const {validateCancellationReceipt}=require('../cancellation.cjs');validateCancellationReceipt(job,receipt);
@@ -27,11 +30,13 @@ function createApplicationHandler({workflow,store,queue,authenticate,revalidate,
27
30
  const actor=await authenticate(request);if(!actor)return Response.json({error:'unauthenticated'},{status:401});
28
31
  try{
29
32
  if(path==='/api/state'){
30
- const jobs=await Promise.all((await store.list({actor,limit:1000})).map(async job=>({...job,safety:await store.getSafety(job.id)})));const wallet_transactions=jobs.flatMap(j=>j.evidence.filter(e=>e.evidence.transaction_hash).map(e=>({job_id:j.id,action:e.action,...e.evidence})));
33
+ const jobs=await Promise.all((await store.list({actor,limit:1000})).map(async job=>({...job,profit:profitFor?await profitFor(job,actor):null,safety:await store.getSafety(job.id)})));const wallet_transactions=jobs.flatMap(j=>j.evidence.filter(e=>e.evidence.transaction_hash).map(e=>({job_id:j.id,action:e.action,...e.evidence})));
31
34
  return Response.json({...await legacyState(actor),a423:{jobs,wallet_transactions}},{headers:{'cache-control':'no-store'}});
32
35
  }
36
+ const profit=/^\/api\/a423-profit\/([A-Za-z0-9_-]{1,128})$/.exec(path);
37
+ if(profit){const job=await workflow.get(profit[1],actor);if(!profitFor)return Response.json({error:'POLICY_NOT_CONFIGURED'},{status:404});return Response.json(await profitFor(job,actor),{headers:{'cache-control':'no-store'}});}
33
38
  const receipt=/^\/api\/a423-receipts\/([A-Za-z0-9_-]{1,128})(\.pdf)?$/.exec(path);
34
- if(receipt){const data=await freshReceipts({workflow,id:receipt[1],actor,revalidate});if(receipt[2])return new Response(Buffer.from(await renderReceiptPdf(data)),{headers:{'content-type':'application/pdf','cache-control':'no-store'}});return Response.json(data,{headers:{'cache-control':'no-store'}});}
39
+ if(receipt){const data=await freshReceipts({workflow,id:receipt[1],actor,revalidate});if(profitFor)data.profit=await profitFor(await workflow.get(receipt[1],actor),actor);if(receipt[2])return new Response(Buffer.from(await renderReceiptPdf(data)),{headers:{'content-type':'application/pdf','cache-control':'no-store'}});return Response.json(data,{headers:{'cache-control':'no-store'}});}
35
40
  const closing=/^\/api\/a423-settlement-consent\/([A-Za-z0-9_-]{1,128})$/.exec(path);
36
41
  if(closing){const job=await workflow.get(closing[1],actor);if(![job.participants.requester,job.participants.worker].includes(actor))return Response.json({error:'settlement party required'},{status:403});if(!settlementConsentFor)throw Error('Settlement consent not configured');const typed=await settlementConsentFor(job);return Response.json(JSON.parse(JSON.stringify(typed,(_,v)=>typeof v==='bigint'?v.toString():v)),{headers:{'cache-control':'no-store'}});}
37
42
  const consent=/^\/api\/a423-worker-consent\/([A-Za-z0-9_-]{1,128})$/.exec(path);
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
  const {createWorkflow,hashAgreement}=require('../workflow.cjs');
3
3
  const {createCredentialAuthenticator,createAuthorityHandler}=require('../authority/index.cjs');
4
- function createEscrowApplication({storage,authority,escrows,signerFor,addressFor,limitsFor,approveSigning,validateArtifact,execute,keeperEnabled=false,forwardCancellations=true,allowAuthorityRequest=async()=>false,requireIndependentArbitrator=true}={}){
4
+ function createEscrowApplication({storage,authority,escrows,signerFor,addressFor,limitsFor,approveSigning,validateArtifact,execute,settlementPolicy,keeperEnabled=false,forwardCancellations=true,allowAuthorityRequest=async()=>false,requireIndependentArbitrator=true}={}){
5
5
  const {createEscrowRegistry,createEvidenceRevalidator,escrowTerms,workerConsent,settlementConsent,escrowTransitions}=require('../escrow/index.cjs');
6
6
  const {validateTask,validateEscrowEvidence,createSpendingAuthorizer,createExpiryAdapter,createApplicationHandler,createChatTools}=require('./index.cjs');
7
7
  if(!storage||!authority||!Array.isArray(escrows)||!escrows.length||typeof approveSigning!=='function'||typeof limitsFor!=='function')throw new TypeError('Explicit storage, authority, escrow configurations and spending/signing approvals required');
8
+ const profit=settlementPolicy?require('../policy/index.cjs').createProfitSettlementService({...settlementPolicy,kv:storage.kv}):null;
8
9
  const registry=new Map(escrows.map(c=>[String(c.chainId)+':'+c.contractAddress.toLowerCase(),c]));
9
10
  const configFor=job=>{const e=job.agreement?.terms.escrow;const c=registry.get(e?.chain_id+':'+e?.contract?.toLowerCase());if(!c)throw Error('Unregistered escrow');return c;};
10
11
  const checkPolicy=async context=>{
@@ -17,17 +18,18 @@ function createEscrowApplication({storage,authority,escrows,signerFor,addressFor
17
18
  const authorizeTransaction=createSpendingAuthorizer({spending:storage.spending,limitsFor,authorize:checkPolicy});
18
19
  const adapters=createEscrowRegistry(escrows.map(c=>({...c,signerFor,addressFor,transactions:storage.transactions,authorizeTransaction,validateArtifact})));
19
20
  const workflow=createWorkflow({store:storage.jobs,adapters:{...adapters,expire:createExpiryAdapter()},transitions:escrowTransitions,
20
- authorize:async({actor})=>authority.isActive(actor),
21
+ prepareInput:profit?async context=>{if(await authority.isActive(context.actor)!==true){const {A423Error}=require('../index.cjs');throw new A423Error('FORBIDDEN','Active identity required',403);}return profit.prepare(context);}:undefined,
22
+ authorize:async context=>await authority.isActive(context.actor)===true&&(context.phase==='reconcile'||!profit||await profit.authorize(context)),
21
23
  validateTerms:terms=>{validateTask(terms.task);const job={id:'terms_validation',agreement:{terms,hash:hashAgreement(terms)}};escrowTerms(job,configFor(job));},
22
24
  validateEvidence:({evidence})=>{if(evidence.transaction_hash)validateEscrowEvidence(evidence);}
23
25
  });
24
26
  const validators=new Map(escrows.map(c=>[String(c.chainId)+':'+c.contractAddress.toLowerCase(),createEvidenceRevalidator(c)]));
25
27
  const revalidate=(job,evidence)=>validators.get(String(configFor(job).chainId)+':'+configFor(job).contractAddress.toLowerCase())(job,evidence);
26
28
  const authenticate=createCredentialAuthenticator({authority});
27
- const applicationOptions={forwardCancellations,workflow,store:storage.jobs,queue:storage.queue,authenticate,revalidate,consentFor:async job=>workerConsent(job,configFor(job)),settlementConsentFor:async job=>settlementConsent(job,configFor(job)),authorityHandler:createAuthorityHandler({authority,allowRequest:allowAuthorityRequest})};
29
+ const applicationOptions={profitFor:profit?(job,actor)=>profit.project(job,actor,{revalidate}):undefined,forwardCancellations,workflow,store:storage.jobs,queue:storage.queue,authenticate,revalidate,consentFor:async job=>workerConsent(job,configFor(job)),settlementConsentFor:async job=>settlementConsent(job,configFor(job)),authorityHandler:createAuthorityHandler({authority,allowRequest:allowAuthorityRequest})};
28
30
  const workerOptions={authority,actorFor:async job=>job.pending?.actor||job.participants.requester,allowExecute:keeperEnabled,
29
31
  execute,beforeExecute:async job=>{await workflow.assertSafe(job.id,job.participants.worker);if(!await authority.isActive(job.participants.worker))return false;const result=await adapters.start.execute({job,input:{},operation:{actor:job.participants.worker}});return result.status==='confirmed';}
30
32
  };
31
- return {workflow,applicationOptions,workerOptions,handler:createApplicationHandler(applicationOptions),chatTools:actor=>createChatTools({workflow,store:storage.jobs,queue:storage.queue,actor})};
33
+ return {profit,workflow,applicationOptions,workerOptions,handler:createApplicationHandler(applicationOptions),chatTools:actor=>createChatTools({workflow,store:storage.jobs,queue:storage.queue,actor,profitFor:applicationOptions.profitFor,policyPromptFor:profit?(job,actor)=>profit.prompt({role:actor===job.participants.worker?'worker':'requester'}):undefined})};
32
34
  }
33
35
  module.exports={createEscrowApplication};
@@ -23,9 +23,9 @@ export function freshReceipts(options: {workflow: Workflow;id: string;actor: str
23
23
  export function renderReceiptPdf(receipt: object): Promise<Uint8Array>;
24
24
  export function planLegacyMigration(records: Array<{id: string;state: string}>): Array<{id: string;state: string;automatic: false;reason: string}>;
25
25
  export function migrateUnfundedLegacy(options: {records: Array<{id: string;state: string}>;workflow: Workflow;actor: string;map(source: any): Promise<Parameters<Workflow['create']>[0]>}): Promise<WorkflowJob[]>;
26
- export function createChatTools(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;actor: string}): Record<string,(input: any) => Promise<WorkflowJob>>;
27
- export function createApplicationHandler(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;authenticate(request: Request): Promise<string | null>;revalidate: Revalidator;consentFor?(job: WorkflowJob): Promise<object>;settlementConsentFor?(job: WorkflowJob): Promise<object>;legacyState?(actor: string): Promise<object>;authorityHandler?(request: Request): Promise<Response>;forwardCancellations?: boolean;basePath?: string}): (request: Request) => Promise<Response>;
26
+ export function createChatTools(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;actor: string;profitFor?(job: WorkflowJob,actor: string): Promise<object | null>;policyPromptFor?(job: WorkflowJob,actor: string): string}): Record<string,(input: any) => Promise<any>>;
27
+ export function createApplicationHandler(options: {workflow: Workflow;store: SqliteJobStore;queue: DurableQueue;authenticate(request: Request): Promise<string | null>;revalidate: Revalidator;consentFor?(job: WorkflowJob): Promise<object>;settlementConsentFor?(job: WorkflowJob): Promise<object>;profitFor?(job: WorkflowJob,actor: string): Promise<object | null>;legacyState?(actor: string): Promise<object>;authorityHandler?(request: Request): Promise<Response>;forwardCancellations?: boolean;basePath?: string}): (request: Request) => Promise<Response>;
28
28
 
29
- export function createEscrowApplication(options: {storage: import('../storage/index.js').SqliteStorage;authority: import('../authority/index.js').Authority;escrows: Array<any>;signerFor(actor: string): Promise<any>;addressFor(actor: string): Promise<string>;limitsFor(context: SigningContext): Promise<{perTransaction: string;daily: string;total: string}>;approveSigning(context: SigningContext): Promise<boolean>;validateArtifact(context: {job: WorkflowJob;resultHash: string}): Promise<boolean>;execute?(context: {job: WorkflowJob;idempotencyKey: string;signal: AbortSignal}): Promise<{result_hash: string;[key: string]: Json}>;keeperEnabled?: boolean;forwardCancellations?: boolean;allowAuthorityRequest?(request: Request): Promise<boolean>;requireIndependentArbitrator?: boolean}): {workflow: Workflow;applicationOptions: Parameters<typeof createApplicationHandler>[0];workerOptions: any;handler: (request: Request) => Promise<Response>;chatTools(actor: string): ReturnType<typeof createChatTools>};
29
+ export function createEscrowApplication(options: {storage: import('../storage/index.js').SqliteStorage;authority: import('../authority/index.js').Authority;escrows: Array<any>;signerFor(actor: string): Promise<any>;addressFor(actor: string): Promise<string>;limitsFor(context: SigningContext): Promise<{perTransaction: string;daily: string;total: string}>;approveSigning(context: SigningContext): Promise<boolean>;validateArtifact(context: {job: WorkflowJob;resultHash: string}): Promise<boolean>;execute?(context: {job: WorkflowJob;idempotencyKey: string;signal: AbortSignal}): Promise<{result_hash: string;[key: string]: Json}>;settlementPolicy?: Omit<import('../policy/index.js').ProfitServiceOptions, 'kv'>;keeperEnabled?: boolean;forwardCancellations?: boolean;allowAuthorityRequest?(request: Request): Promise<boolean>;requireIndependentArbitrator?: boolean}): {profit: ReturnType<typeof import('../policy/index.js').createProfitSettlementService> | null;workflow: Workflow;applicationOptions: Parameters<typeof createApplicationHandler>[0];workerOptions: any;handler: (request: Request) => Promise<Response>;chatTools(actor: string): ReturnType<typeof createChatTools>};
30
30
 
31
31
  export function freshCancellationReceipt(options: {workflow: Workflow;id: string;actor: string;revalidate: Revalidator}): Promise<import('../workflow.js').CancellationReceipt>;
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+ const {hashAgreement}=require('../workflow.cjs');
3
+ const BPS=10000n,MAX=(1n<<256n)-1n;
4
+ function check(ok,message){if(!ok)throw new TypeError(message);}
5
+ function raw(value,name){check(typeof value==='string'&&/^(0|[1-9][0-9]*)$/.test(value),name+' must be an unsigned raw-unit integer string');const n=BigInt(value);check(n<=MAX,name+' exceeds uint256');return n;}
6
+ const min=(a,b)=>a<b?a:b,ceil=(a,b)=>(a+b-1n)/b;
7
+ const floor=(a,b)=>a>=0n?a/b:-ceil(-a,b);
8
+ function margin(cost,payout){return payout===0n?null:(floor((payout-cost)*BPS,payout)).toString();}
9
+ function createSettlementPolicy(options={}){
10
+ check(options&&Object.getPrototypeOf(options)===Object.prototype,'Policy thresholds must be a plain object');
11
+ const keys=['target_margin_bps','min_margin_bps','max_margin_bps','require_counter'];
12
+ check(Object.keys(options).every(key=>keys.includes(key)),'Unknown policy threshold; use target_margin_bps, min_margin_bps, max_margin_bps or require_counter');
13
+ const {target_margin_bps=5000,min_margin_bps=1000,max_margin_bps=5000,require_counter=true}=options;
14
+ for(const v of [target_margin_bps,min_margin_bps,max_margin_bps])check(Number.isSafeInteger(v)&&v>=0&&v<10000,'Margins must be integer basis points between 0 and 9999');
15
+ check(min_margin_bps<=target_margin_bps&&target_margin_bps<=max_margin_bps,'Require minimum <= target <= maximum');check(typeof require_counter==='boolean','require_counter must be boolean');
16
+ const configuration={target_margin_bps,min_margin_bps,max_margin_bps,require_counter};
17
+ function evaluate({cost_basis_raw,budget_raw,escrow_raw,offer_raw,negotiation_round=0}){
18
+ const cost=raw(cost_basis_raw,'cost_basis_raw'),budget=raw(budget_raw,'budget_raw'),escrow=raw(escrow_raw,'escrow_raw'),affordable=min(budget,escrow);
19
+ check(Number.isSafeInteger(negotiation_round)&&negotiation_round>=0,'negotiation_round must be nonnegative');
20
+ const target=ceil(cost*BPS,BPS-BigInt(target_margin_bps)),minimum=ceil(cost*BPS,BPS-BigInt(min_margin_bps)),maximum=ceil(cost*BPS,BPS-BigInt(max_margin_bps));
21
+ const recommended=min(target,affordable),offer=offer_raw===undefined?null:raw(offer_raw,'offer_raw');
22
+ const viability=cost===0n?'no_cost':affordable<cost?'below_cost':affordable<minimum?'below_minimum':affordable<target?'below_target':'target_affordable';
23
+ const offerIssue=offer===null?null:offer>affordable?'above_budget_or_escrow':offer>maximum?'above_maximum':offer<cost?'below_cost':offer<minimum?'below_minimum':null;
24
+ const requires=cost>0n&&(affordable<minimum||['below_cost','below_minimum'].includes(offerIssue));
25
+ let decision=cost===0n?'refund':requires?'owner_approval_required':offer===null?'propose':offerIssue?'counter':require_counter&&negotiation_round===0?'counter':'accept';
26
+ if(cost===0n&&offer!==null&&offer!==0n)decision='counter';
27
+ return {version:'a423.profit_policy.v1',...configuration,cost_basis_raw,budget_raw,escrow_raw,affordable:affordable.toString(),target_payout:target.toString(),min_payout:minimum.toString(),max_payout:maximum.toString(),recommended_payout:recommended.toString(),expected_profit:(recommended-cost).toString(),expected_margin_bps:margin(cost,recommended),viability,decision,requires_owner_approval:requires,offer_issue:offerIssue,offer_raw:offer?.toString()??null,offer_profit_raw:offer===null?null:(offer-cost).toString(),offer_margin_bps:offer===null?null:margin(cost,offer),negotiation_round};
28
+ }
29
+ return Object.freeze({configuration:Object.freeze(configuration),evaluate});
30
+ }
31
+ function profitProjection({evaluation,payout_raw,confirmed=false}){
32
+ check(typeof confirmed==='boolean','confirmed must be boolean');const payout=raw(payout_raw,'payout_raw'),cost=raw(evaluation.cost_basis_raw,'cost_basis_raw');
33
+ const projected={projected_payout_raw:payout_raw,projected_profit_raw:(payout-cost).toString(),projected_margin_bps:margin(cost,payout),target_status:payout>=BigInt(evaluation.target_payout)?'target_met':'below_target'};
34
+ return {...evaluation,...projected,advisory:true,receipt_confirmed:confirmed,realized_payout_raw:confirmed?payout_raw:null,realized_profit_raw:confirmed?projected.projected_profit_raw:null,realized_margin_bps:confirmed?projected.projected_margin_bps:null};
35
+ }
36
+ function settlementPolicyPrompt({role='worker',policy=createSettlementPolicy(),override}={}){
37
+ check(['worker','requester'].includes(role),'Unknown policy role');if(override!==undefined){check(typeof override==='string','Prompt override must be text');return override;}
38
+ const c=policy.configuration;
39
+ const shared='Use list_jobs to discover unknown IDs; never invent IDs. Reconcile OPERATION_PENDING before another action. Both parties approve the exact revision/hash. Acceptance is not payment; require a freshly confirmed receipt. The runner submits artifacts. Simulated costs are estimates, not invoices.';
40
+ return (role==='worker'?`Profit is the primary objective. Inspect cost basis before quoting or proposing a closing split. Target ${c.target_margin_bps} basis points gross margin on revenue, minimum ${c.min_margin_bps}, maximum ${c.max_margin_bps}. Use the policy recommendation within budget and escrow caps. Never silently accept below cost or minimum; obtain explicit owner approval. ${c.require_counter?'Counter before accepting the first buyer offer. ':''}No cost means a full refund. Publicly share progress, work claims and proposed amounts only; never disclose internal cost, token counts or margin calculations.`:'Evaluate proposals against completed work and the original budget. Do not demand zero solely because work is incomplete; use progress and claims. Never exceed the original budget. Internal worker cost/margins are private.')+' '+shared;
41
+ }
42
+ function createProfitSettlementService({kv,costBasisFor,budgetFor,policy,thresholds,approveException,canViewPrivate=async()=>false}={}){
43
+ check(policy===undefined||thresholds===undefined,'Supply either thresholds or policy, not both');
44
+ policy=policy===undefined?createSettlementPolicy(thresholds):policy;
45
+ check(policy&&typeof policy.evaluate==='function'&&policy.configuration,'A valid SettlementPolicy is required');
46
+ // Capture and validate configuration at initialization, before any job or cost lookup.
47
+ const configuration=createSettlementPolicy(policy.configuration).configuration;
48
+ check(kv&&typeof kv.create==='function'&&typeof costBasisFor==='function'&&typeof budgetFor==='function','Durable private KV, costBasisFor and budgetFor required');
49
+ const key=job=>hashAgreement([job.id,job.agreement?.hash,job.epoch||0]);
50
+ async function basis(job,freeze=false){
51
+ check(job.agreement?.terms.escrow,'Escrow agreement required');const id=key(job),stored=await kv.get('a423_profit_basis',id);if(stored)return stored;
52
+ const [cost,budget]=await Promise.all([costBasisFor(structuredClone(job)),budgetFor(structuredClone(job))]);
53
+ const snapshot={cost_basis_raw:cost,budget_raw:budget,escrow_raw:job.agreement.terms.escrow.amount,configuration:{...configuration},frozen_at:new Date().toISOString()};
54
+ policy.evaluate(snapshot);
55
+ if(!freeze)return snapshot;await kv.create('a423_profit_basis',id,snapshot);return await kv.get('a423_profit_basis',id);
56
+ }
57
+ async function evaluate(job,{freeze=false,offer_raw}={}){const snapshot=await basis(job,freeze);const history=job.negotiation?.history||[],firstBuyer=history.findIndex(p=>p.proposer_id===job.participants.requester);const round=firstBuyer<0?0:history.filter((p,i)=>i>firstBuyer&&p.proposer_id===job.participants.worker).length;return createSettlementPolicy(snapshot.configuration).evaluate({...snapshot,offer_raw,negotiation_round:round});}
58
+ async function prepare({job,actor,action,input}){
59
+ if(!['propose_settlement','counter_settlement'].includes(action))return input;
60
+ const e=await evaluate(job,{freeze:true});return {...input,worker_amount:input.worker_amount===undefined?(actor===job.participants.worker?e.recommended_payout:'0'):input.worker_amount};
61
+ }
62
+ async function authorize({job,actor,action,input}){
63
+ const workerProposal=['propose_settlement','counter_settlement'].includes(action)&&actor===job.participants.worker;
64
+ const acceptance=['accept_settlement','approve_cancellation','execute_cancellation'].includes(action);
65
+ if(!workerProposal&&!acceptance)return true;
66
+ // Adopting already executed chain evidence is observation, not authorization to spend.
67
+ if(['accept_settlement','execute_cancellation'].includes(action)&&/^0x[0-9a-fA-F]{64}$/.test(input.transaction_hash||''))return true;
68
+ const payout=workerProposal?input.worker_amount:job.negotiation?.current?.worker_amount;
69
+ if(payout===undefined)return true; // The protocol adapter rejects malformed or absent proposals.
70
+ const e=await evaluate(job,{freeze:true,offer_raw:payout});
71
+ if(['above_budget_or_escrow','above_maximum'].includes(e.offer_issue))return false;
72
+ if(e.requires_owner_approval){
73
+ if(typeof approveException!=='function')return false;
74
+ const approval=await approveException({job:structuredClone(job),actor,action,payout_raw:payout,evaluation:structuredClone(e),proposal_hash:job.negotiation?.current?.proposal_hash||null});
75
+ if(approval?.approved!==true||typeof approval.approval_id!=='string'||!approval.approval_id.trim())return false;
76
+ await kv.create('a423_profit_approvals',hashAgreement([key(job),action,payout,job.negotiation?.current?.proposal_hash||null,approval.approval_id]),{job_id:job.id,agreement_hash:job.agreement.hash,action,actor,proposal_hash:job.negotiation?.current?.proposal_hash||null,approval_id:approval.approval_id,payout_raw:payout,at:new Date().toISOString()});
77
+ }
78
+ if(acceptance&&job.negotiation?.current?.proposer_id!==job.participants.worker&&e.require_counter&&e.cost_basis_raw!=='0'&&e.negotiation_round===0)return false;
79
+ return true;
80
+ }
81
+ async function project(job,actor,{revalidate}={}){
82
+ if(!job.agreement?.terms.escrow)return null;
83
+ const proof=[...job.evidence].reverse().find(item=>item.evidence.event==='Settled')?.evidence;
84
+ let confirmed=false,evidence_status=proof?'unavailable':'not_settled';
85
+ if(proof&&revalidate){try{const result=await revalidate(job,proof);confirmed=result.status==='valid';evidence_status=result.status;}catch{evidence_status='unavailable';}}
86
+ const payout=confirmed?proof.worker_paid:job.negotiation?.current?.worker_amount||'0';
87
+ // Never call the cost provider or disclose its output to the requester.
88
+ if(actor!==job.participants.worker&&await canViewPrivate({job:structuredClone(job),actor})!==true)return {advisory:true,proposed_payout_raw:job.negotiation?.current?.worker_amount||null,realized_payout_raw:confirmed?payout:null,receipt_confirmed:confirmed,evidence_status};
89
+ const e=await evaluate(job);const frozen=!!await kv.get('a423_profit_basis',key(job));
90
+ return {...profitProjection({evaluation:e,payout_raw:payout,confirmed}),basis_frozen:frozen,evidence_status};
91
+ }
92
+ return {configuration,evaluate,prepare,authorize,project,prompt:options=>settlementPolicyPrompt({...options,policy:createSettlementPolicy(configuration)})};
93
+ }
94
+ module.exports={createSettlementPolicy,profitProjection,settlementPolicyPrompt,createProfitSettlementService};
@@ -0,0 +1,23 @@
1
+ import type {WorkflowJob,Json} from '../workflow.js';
2
+ import type {KeyValueStore} from '../storage/index.js';
3
+ export interface PolicyConfiguration {target_margin_bps?: number;min_margin_bps?: number;max_margin_bps?: number;require_counter?: boolean}
4
+ export interface PolicyInput {cost_basis_raw: string;budget_raw: string;escrow_raw: string;offer_raw?: string;negotiation_round?: number}
5
+ export interface PolicyEvaluation extends Required<PolicyConfiguration> {
6
+ version: 'a423.profit_policy.v1';cost_basis_raw: string;budget_raw: string;escrow_raw: string;affordable: string;target_payout: string;min_payout: string;max_payout: string;recommended_payout: string;expected_profit: string;expected_margin_bps: string | null;viability: 'no_cost'|'below_cost'|'below_minimum'|'below_target'|'target_affordable';decision: 'refund'|'propose'|'counter'|'accept'|'owner_approval_required';requires_owner_approval: boolean;offer_issue: string | null;offer_raw: string | null;offer_profit_raw: string | null;offer_margin_bps: string | null;negotiation_round: number;
7
+ }
8
+ export interface SettlementPolicy {readonly configuration: Readonly<Required<PolicyConfiguration>>;evaluate(input: PolicyInput): PolicyEvaluation}
9
+ export type CostBasisProvider=(job: WorkflowJob)=>Promise<string>;
10
+ export interface ProfitProjection extends PolicyEvaluation {advisory: true;receipt_confirmed: boolean;projected_payout_raw: string;projected_profit_raw: string;projected_margin_bps: string | null;target_status: 'target_met'|'below_target';realized_payout_raw: string | null;realized_profit_raw: string | null;realized_margin_bps: string | null}
11
+ export interface PolicyContext {job: WorkflowJob;actor: string;action: string;input: any}
12
+ export interface ProfitServiceOptions {kv: KeyValueStore;costBasisFor: CostBasisProvider;budgetFor(job: WorkflowJob): Promise<string>;policy?: SettlementPolicy;thresholds?: PolicyConfiguration;approveException?(context: {job: WorkflowJob;actor: string;action: string;payout_raw: string;evaluation: PolicyEvaluation;proposal_hash: string | null}): Promise<{approved: boolean;approval_id?: string} | null>;canViewPrivate?(context: {job: WorkflowJob;actor: string}): Promise<boolean>}
13
+ export function createSettlementPolicy(options?: PolicyConfiguration): SettlementPolicy;
14
+ export function profitProjection(input: {evaluation: PolicyEvaluation;payout_raw: string;confirmed?: boolean}): ProfitProjection;
15
+ export function settlementPolicyPrompt(options?: {role?: 'worker'|'requester';policy?: SettlementPolicy;override?: string}): string;
16
+ export function createProfitSettlementService(options: ProfitServiceOptions): {
17
+ readonly configuration: Readonly<Required<PolicyConfiguration>>;
18
+ evaluate(job: WorkflowJob,options?: {freeze?: boolean;offer_raw?: string}): Promise<PolicyEvaluation>;
19
+ prepare(context: PolicyContext): Promise<Json>;
20
+ authorize(context: PolicyContext): Promise<boolean>;
21
+ project(job: WorkflowJob,actor: string,options?: {revalidate?(job: WorkflowJob,evidence: any): Promise<{status: string}>}): Promise<(ProfitProjection & {basis_frozen: boolean;evidence_status: string}) | {advisory: true;proposed_payout_raw: string | null;realized_payout_raw: string | null;receipt_confirmed: boolean;evidence_status: string} | null>;
22
+ prompt(options?: {role?: 'worker'|'requester';override?: string}): string;
23
+ };
@@ -0,0 +1,2 @@
1
+ import api from './index.cjs';
2
+ export const {createSettlementPolicy,profitProjection,settlementPolicyPrompt,createProfitSettlementService}=api;
package/src/workflow.cjs CHANGED
@@ -24,6 +24,8 @@ const RULES = {
24
24
  verify: { from: ['submitted'], roles: ['verifier'], to: 'verified', effect: true },
25
25
  release: { from: ['verified'], roles: ['verifier'], to: 'paid', effect: true },
26
26
  refund: { from: ['funded', 'running'], roles: ['requester'], to: 'refunded', effect: true },
27
+ decline: { from: ['draft', 'quoted', 'accepted'], roles: ['worker'], to: 'cancelled' },
28
+ reopen: { from: ['cancelled'], roles: ['requester', 'worker'], to: 'draft' },
27
29
  cancel: { from: ['draft', 'quoted', 'accepted'], roles: ['requester'], to: 'cancelled' },
28
30
  expire: { from: ['draft', 'quoted', 'accepted'], roles: ['requester', 'worker'], to: 'expired', effect: true },
29
31
  dispute: { from: ['submitted', 'verified'], roles: ['requester', 'worker', 'verifier'], to: 'disputed' },
@@ -31,7 +33,7 @@ const RULES = {
31
33
  };
32
34
  /** Pure canonical JSON SHA-256. This is an integrity commitment, not authentication. */
33
35
  function hashAgreement(terms) { return digest(terms); }
34
- function createWorkflow({ store, adapters = {}, authorize, validateTerms, validateEvidence, transitions = {} } = {}) {
36
+ function createWorkflow({ store, adapters = {}, authorize, prepareInput, validateTerms, validateEvidence, transitions = {} } = {}) {
35
37
  if (!store || ['get', 'create', 'compareAndSet'].some(k => typeof store[k] !== 'function')) throw new TypeError('Supply an atomic workflow store');
36
38
  const rules = copy(RULES);
37
39
  for (const [action, rule] of Object.entries(transitions)) {
@@ -52,9 +54,9 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
52
54
  if (!job || !Object.values(job.participants).includes(actor)) error('NOT_FOUND', 'Job not found', 404);
53
55
  return copy(job);
54
56
  }
55
- async function allowed(job, action, actor, input, rule) {
57
+ async function allowed(job, action, actor, input, rule, phase='execute') {
56
58
  if (!rule.roles.some(role => job.participants[role] === actor)) error('FORBIDDEN', 'Actor cannot perform this action', 403);
57
- if (authorize && await authorize({ job: copy(job), action, actor, input: copy(input) }) !== true) error('FORBIDDEN', 'Application policy rejected action', 403);
59
+ if (authorize && await authorize({ job: copy(job), action, actor, input: copy(input), phase }) !== true) error('FORBIDDEN', 'Application policy rejected action', 403);
58
60
  }
59
61
  async function complete(job, result) {
60
62
  const pending = job.pending;
@@ -126,7 +128,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
126
128
  if (participants.requester === participants.worker || actor !== participants.requester) error('FORBIDDEN', 'Requester must create the job and differ from worker', 403);
127
129
  canonical(metadata); canonical(participants);
128
130
  const at = stamp();
129
- const job = { subflows:[],active_subflow_id:null,work_status:'not_started',negotiation:null,escrow_ledger:null,id, protocol: 'a423.workflow.v1', version: 1, state: 'draft', participants: copy(participants), metadata: copy(metadata), agreement: null, submission: null, pending: null, operations: {}, evidence: [], events: [{ sequence: 1, type: 'created', actor_id: actor, at, data: {} }], created_at: at, updated_at: at };
131
+ const job = { epoch:0,agreement_revision:0,pre_funding_cancellations:[],subflows:[],active_subflow_id:null,work_status:'not_started',negotiation:null,escrow_ledger:null,id, protocol: 'a423.workflow.v1', version: 1, state: 'draft', participants: copy(participants), metadata: copy(metadata), agreement: null, submission: null, pending: null, operations: {}, evidence: [], events: [{ sequence: 1, type: 'created', actor_id: actor, at, data: {} }], created_at: at, updated_at: at };
130
132
  if (authorize && await authorize({ job: copy(job), action: 'create', actor, input: {} }) !== true) error('FORBIDDEN', 'Application policy rejected creation', 403);
131
133
  if (!await store.create(job)) error('CONFLICT', 'Job ID already exists');
132
134
  return copy(job);
@@ -140,6 +142,8 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
140
142
  if(safety?.blocked) error('SAFETY_HOLD','Evidence requires reconciliation before further actions',409);
141
143
  const rule = Object.hasOwn(rules,action) ? rules[action] : null;
142
144
  if (!rule) error('UNKNOWN_ACTION', 'Unknown workflow action', 400);
145
+ if(!rule.roles.some(role=>job.participants[role]===actor))error('FORBIDDEN','Actor cannot perform this action',403);
146
+ if(prepareInput){input=await prepareInput({job:copy(job),action,actor,input:copy(input)});canonical(input);}
143
147
  await allowed(job, action, actor, input, rule);
144
148
  const key = digest([actor, idempotencyKey]);
145
149
  const requestHash = digest([action, input]);
@@ -156,10 +160,30 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
156
160
  if(action==='approve_cancellation'&&cancellation?.instruction){const candidate=require('./cancellation.cjs').createCancellationInstruction(job,input);if(input.expected_revision!==job.negotiation.current.escrow_version||input.proposal_hash!==job.negotiation.current.proposal_hash||hashAgreement(candidate)!==hashAgreement(cancellation.instruction))error('CANCELLATION_CONFLICT','Another instruction is already accepted',409);return job;}
157
161
  if (!rule.from.includes(job.state)) error('INVALID_STATE', `Cannot ${action} from ${job.state}`);
158
162
  let next = copy(job);
163
+ const fundingStarted=Object.values(job.operations).some(op=>op.result?.evidence?.event==='Funded') || job.evidence.some(e=>e.action==='fund') || job.events.some(e=>e.type==='fund.requested') || !!job.escrow_ledger;
164
+ if(['cancel','decline','reopen'].includes(action)&&fundingStarted)error('FUNDING_STARTED','Funding activity must be reconciled and closed; this job cannot be reopened',409);
165
+ if((job.epoch||0)>0&&['quote','accept','fund','cancel','decline'].includes(action)&&input?.expected_epoch!==job.epoch)error('STALE_EPOCH','Reload the job and supply its current expected_epoch',409);
166
+ if(['cancel','decline'].includes(action)){
167
+ if(input?.reason!==undefined&&(typeof input.reason!=='string'||!input.reason.trim()||input.reason.length>4000))error('INVALID_INPUT','Cancellation reason must be nonempty text, at most 4000 characters',400);
168
+ const record={id:randomUUID(),epoch:job.epoch||0,action,actor_id:actor,prior_state:job.state,accepted_by:copy(job.agreement?.accepted_by||[]),agreement:copy(job.agreement),reason:input?.reason||null,at:stamp(),reopen_approvals:[],reopened_at:null};
169
+ next.pre_funding_cancellations=[...(job.pre_funding_cancellations||[]),record];next.work_status='stopped';
170
+ }
171
+ if(action==='reopen'){
172
+ const history=copy(job.pre_funding_cancellations||[]),record=history.at(-1);
173
+ if(!record||record.reopened_at||input?.cancellation_id!==record.id)error('CANCELLATION_MISMATCH','Supply the current pre-funding cancellation_id',409);
174
+ const required=[...new Set([job.participants.requester,record.actor_id,...([job.participants.requester,job.participants.worker].every(id=>record.accepted_by.includes(id))?[job.participants.worker]:[])])];
175
+ if(!required.includes(actor))error('FORBIDDEN','This actor is not a required reopening approver',403);
176
+ record.reopen_approvals=[...new Set([...record.reopen_approvals,actor])];next.pre_funding_cancellations=history;
177
+ if(!required.every(id=>record.reopen_approvals.includes(id))){next.operations[key]={request_hash:requestHash,status:'confirmed',result:{status:'confirmed'}};return save(job,next,'reopen.approved',actor,{cancellation_id:record.id,required_approvers:required});}
178
+ record.reopened_at=stamp();next.epoch=(job.epoch||0)+1;next.agreement_revision=Math.max(job.agreement_revision||0,job.agreement?.revision||0);next.agreement=null;next.work_status='not_started';
179
+ }
159
180
  if (action === 'quote') {
160
181
  if (!input || !input.terms || typeof input.terms !== 'object' || Array.isArray(input.terms)) error('INVALID_INPUT', 'Supply a terms object', 400);
161
182
  if (validateTerms) await validateTerms(copy(input.terms));
162
- next.agreement = { revision: (job.agreement?.revision || 0) + 1, terms: copy(input.terms), hash: hashAgreement(input.terms), accepted_by: [] };
183
+ const terms=copy(input.terms);
184
+ if((job.epoch||0)>0)terms._a423_reopen={job_id:job.id,epoch:job.epoch};
185
+ next.agreement_revision=Math.max(job.agreement_revision||0,job.agreement?.revision||0)+1;
186
+ next.agreement = { revision: next.agreement_revision, terms, hash: hashAgreement(terms), accepted_by: [] };
163
187
  }
164
188
  if (action === 'accept') {
165
189
  if (!input || input.agreement_hash !== job.agreement.hash || input.revision !== job.agreement.revision) error('AGREEMENT_MISMATCH', 'Accept the current agreement hash and revision');
@@ -176,6 +200,20 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
176
200
  next.operations[key] = { request_hash: requestHash, status: 'confirmed', result: { status: 'confirmed' } };
177
201
  return save(job, next, action, actor, input);
178
202
  },
203
+ async rejectPendingOperation(id,{operation_id,reason},{actor}={}) {
204
+ const job=await get(id,actor),pending=job.pending;
205
+ if((store.getSafety?await store.getSafety(id):null)?.blocked)error('SAFETY_HOLD','Evidence requires reconciliation before recovery',409);
206
+ if(!pending||pending.id!==operation_id)error('PENDING_MISMATCH','Supply the current pending operation ID',409);
207
+ if(typeof reason!=='string'||!reason.trim()||reason.length>4000)error('INVALID_INPUT','Recovery reason required',400);
208
+ const rule=rules[pending.action];if(!rule)error('RECOVERY_UNSUPPORTED','Unknown pending action',409);
209
+ await allowed(job,pending.action,actor,pending.input,rule);
210
+ if(pending.request_hash!==digest([pending.action,pending.input]))error('RECOVERY_UNSAFE','Pending input integrity mismatch; manual investigation required',409);
211
+ const inspect=adapters[pending.action]?.rejectPending;
212
+ if(typeof inspect!=='function')error('RECOVERY_UNSUPPORTED','Adapter does not prove this operation is never executable',409);
213
+ const proof=await inspect({job:copy(job),input:copy(pending.input),operation:copy(pending)});
214
+ if(proof?.status!=='rejected'||proof.never_executable!==true)error('RECOVERY_UNSAFE','Operation might execute; reconcile instead',409);
215
+ return complete(job,{status:'rejected',reason,recovery:{operation_id,proof:copy(proof)}});
216
+ },
179
217
  async resume(id, actor) {
180
218
  const job=await get(id,actor);
181
219
  if(!job.pending)return job;
@@ -187,7 +225,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
187
225
  async reconcile(id, actor) {
188
226
  const job = await get(id, actor);
189
227
  if (!job.pending) return job;
190
- await allowed(job, job.pending.action, actor, job.pending.input, rules[job.pending.action]);
228
+ await allowed(job, job.pending.action, actor, job.pending.input, rules[job.pending.action],'reconcile');
191
229
  return execute(job, true);
192
230
  },
193
231
  async cancellation(id,actor){return require('./cancellation.cjs').getCancellationSubflow(await get(id,actor));},
@@ -202,7 +240,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
202
240
  },
203
241
  async receipts(id, actor) {
204
242
  const job = await get(id, actor);
205
- return { protocol: job.protocol, job_id: id, state: job.state, agreement: job.agreement, submission: job.submission, subflows:job.subflows||[],active_subflow_id:job.active_subflow_id||null,work_status:job.work_status,negotiation:job.negotiation,escrow_ledger:job.escrow_ledger,evidence: job.evidence, events: job.events, pending: job.pending };
243
+ return { epoch:job.epoch||0,pre_funding_cancellations:job.pre_funding_cancellations||[],protocol: job.protocol, job_id: id, state: job.state, agreement: job.agreement, submission: job.submission, subflows:job.subflows||[],active_subflow_id:job.active_subflow_id||null,work_status:job.work_status,negotiation:job.negotiation,escrow_ledger:job.escrow_ledger,evidence: job.evidence, events: job.events, pending: job.pending };
206
244
  }
207
245
  };
208
246
  return workflow;