@allorc/a423-sdk 0.5.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.
@@ -1,2 +1,2 @@
1
1
  import api from './index.cjs';
2
- export const { validateTask, validateEscrowEvidence, createSpendingAuthorizer, runMaintenanceOnce, createExpiryAdapter, runExecutionOnce, enqueueRunningJobs, createEventStream, createWebhookDispatcher, watchEvidenceOnce, freshReceipts, renderReceiptPdf, planLegacyMigration, migrateUnfundedLegacy, createChatTools, createApplicationHandler, createEscrowApplication } = api;
2
+ export const { validateTask, validateEscrowEvidence, createSpendingAuthorizer, runMaintenanceOnce, createExpiryAdapter, runExecutionOnce, enqueueRunningJobs, createEventStream, createWebhookDispatcher, watchEvidenceOnce, freshReceipts, renderReceiptPdf, planLegacyMigration, migrateUnfundedLegacy, createChatTools, createApplicationHandler, createEscrowApplication, freshCancellationReceipt } = api;
@@ -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;
@@ -62,11 +64,13 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
62
64
  canonical(result);
63
65
  if (result.status === 'pending') {
64
66
  const next = copy(job); next.pending.observation = copy(result);
67
+ require('./cancellation.cjs').updateCancellationSubflow(job,next,{action:pending.action,actor:pending.actor,phase:'pending',evidence:result,at:stamp()});
65
68
  return save(job, next, pending.action + '.pending', pending.actor, result);
66
69
  }
67
70
  if (result.status === 'rejected') {
68
71
  const next = copy(job); next.pending = null;
69
72
  next.operations[pending.key] = { request_hash: pending.request_hash, status: 'rejected', result: copy(result) };
73
+ require('./cancellation.cjs').updateCancellationSubflow(job,next,{action:pending.action,actor:pending.actor,phase:'rejected',error:result.reason,at:stamp()});
70
74
  return save(job, next, pending.action + '.rejected', pending.actor, result);
71
75
  }
72
76
  if (!result.evidence || typeof result.evidence !== 'object' || Array.isArray(result.evidence)) error('INVALID_ADAPTER_RESULT', 'Confirmed effects require structured evidence', 502);
@@ -83,7 +87,12 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
83
87
  next.negotiation={status:'open',origin_state:job.negotiation?.origin_state||job.state,current:record,history:[...(job.negotiation?.history||[]),record]};
84
88
  next.work_status='paused';eventData={...eventData,proposal:record};
85
89
  }
86
- if(pending.action==='accept_settlement'){
90
+ if(pending.action==='approve_cancellation'){
91
+ const instruction=require('./cancellation.cjs').validateCancellationInstruction(job,result.evidence.instruction);
92
+ next.negotiation={...copy(job.negotiation),status:'accepted',acceptance_basis:'dual_wallet_signatures'};next.work_status='paused';
93
+ eventData={...eventData,instruction_id:instruction.id};
94
+ }
95
+ if(['accept_settlement','execute_cancellation'].includes(pending.action)){
87
96
  const p=job.negotiation?.current;
88
97
  if(!p || result.evidence.settlement_reason!==4 || result.evidence.worker_paid!==p.worker_amount || result.evidence.proposal_hash!==p.proposal_hash || result.evidence.escrow_version!==p.escrow_version)error('INVALID_ADAPTER_RESULT','Closing settlement evidence mismatch',502);
89
98
  next.negotiation={...copy(job.negotiation),status:'settled',accepted_by:pending.actor,settled_at:stamp()};next.work_status='stopped';
@@ -99,6 +108,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
99
108
  const closed=[5,6,7].includes(e.state);
100
109
  next.escrow_ledger={funded_amount:e.amount,worker_paid:e.worker_paid,payer_refunded:closed?(BigInt(e.amount)-BigInt(e.worker_paid)).toString():'0',locked_amount:closed?'0':e.amount,escrow_version:e.escrow_version??0};
101
110
  }
111
+ require('./cancellation.cjs').updateCancellationSubflow(job,next,{action:pending.action,actor:pending.actor,phase:'confirmed',evidence:result.evidence,instruction:result.evidence.instruction,at:stamp()});
102
112
  return save(job, next, pending.action + '.confirmed', pending.actor, eventData);
103
113
  }
104
114
  async function execute(job, reconcile = false) {
@@ -110,7 +120,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
110
120
  const result = await fn({ job: copy(job), input: copy(pending.input), operation: copy(pending) });
111
121
  return complete(job, result);
112
122
  }
113
- return {
123
+ const workflow={
114
124
  async create({ id = randomUUID(), participants, metadata = {} }, actor) {
115
125
  identifier(id); identifier(actor);
116
126
  if (!participants || !participants.requester || !participants.worker || !participants.verifier) throw new TypeError('Supply requester, worker and verifier participants');
@@ -118,7 +128,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
118
128
  if (participants.requester === participants.worker || actor !== participants.requester) error('FORBIDDEN', 'Requester must create the job and differ from worker', 403);
119
129
  canonical(metadata); canonical(participants);
120
130
  const at = stamp();
121
- const job = { 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 };
122
132
  if (authorize && await authorize({ job: copy(job), action: 'create', actor, input: {} }) !== true) error('FORBIDDEN', 'Application policy rejected creation', 403);
123
133
  if (!await store.create(job)) error('CONFLICT', 'Job ID already exists');
124
134
  return copy(job);
@@ -132,6 +142,8 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
132
142
  if(safety?.blocked) error('SAFETY_HOLD','Evidence requires reconciliation before further actions',409);
133
143
  const rule = Object.hasOwn(rules,action) ? rules[action] : null;
134
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);}
135
147
  await allowed(job, action, actor, input, rule);
136
148
  const key = digest([actor, idempotencyKey]);
137
149
  const requestHash = digest([action, input]);
@@ -143,12 +155,35 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
143
155
  if (job.pending.key !== key || job.pending.request_hash !== requestHash) error('OPERATION_PENDING', 'Reconcile or retry the pending operation first');
144
156
  return execute(job);
145
157
  }
158
+ const cancellation=require('./cancellation.cjs').getCancellationSubflow(job);
159
+ if(['propose_settlement','counter_settlement'].includes(action)&&cancellation?.instruction)error('CANCELLATION_ACCEPTED','An accepted instruction must be executed or resolved before renegotiating',409);
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;}
146
161
  if (!rule.from.includes(job.state)) error('INVALID_STATE', `Cannot ${action} from ${job.state}`);
147
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
+ }
148
180
  if (action === 'quote') {
149
181
  if (!input || !input.terms || typeof input.terms !== 'object' || Array.isArray(input.terms)) error('INVALID_INPUT', 'Supply a terms object', 400);
150
182
  if (validateTerms) await validateTerms(copy(input.terms));
151
- 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: [] };
152
187
  }
153
188
  if (action === 'accept') {
154
189
  if (!input || input.agreement_hash !== job.agreement.hash || input.revision !== job.agreement.revision) error('AGREEMENT_MISMATCH', 'Accept the current agreement hash and revision');
@@ -157,6 +192,7 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
157
192
  if (rule.effect) {
158
193
  if (!adapters[action]) error('ADAPTER_REQUIRED', `Configure ${action} adapter`, 501);
159
194
  next.pending = { id: `a423:${id}:${key}`, key, action, actor, input: copy(input), request_hash: requestHash, target: rule.to, created_at: stamp(), observation: null };
195
+ require('./cancellation.cjs').updateCancellationSubflow(job,next,{action,actor,phase:'requested',input,at:stamp()});
160
196
  job = await save(job, next, action + '.requested', actor);
161
197
  return execute(job);
162
198
  }
@@ -164,6 +200,20 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
164
200
  next.operations[key] = { request_hash: requestHash, status: 'confirmed', result: { status: 'confirmed' } };
165
201
  return save(job, next, action, actor, input);
166
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
+ },
167
217
  async resume(id, actor) {
168
218
  const job=await get(id,actor);
169
219
  if(!job.pending)return job;
@@ -175,16 +225,28 @@ function createWorkflow({ store, adapters = {}, authorize, validateTerms, valida
175
225
  async reconcile(id, actor) {
176
226
  const job = await get(id, actor);
177
227
  if (!job.pending) return job;
178
- 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');
179
229
  return execute(job, true);
180
230
  },
231
+ async cancellation(id,actor){return require('./cancellation.cjs').getCancellationSubflow(await get(id,actor));},
232
+ async cancellationInstruction(id,actor){const job=await get(id,actor),subflow=require('./cancellation.cjs').getCancellationSubflow(job);if(!subflow?.instruction)error('INSTRUCTION_UNAVAILABLE','No accepted cancellation instruction',409);return require('./cancellation.cjs').validateCancellationInstruction(job,subflow.instruction);},
233
+ async cancellationReceipt(id,actor){const job=await get(id,actor),subflow=require('./cancellation.cjs').getCancellationSubflow(job);if(subflow?.status!=='completed'||!subflow.receipt)error('RECEIPT_UNAVAILABLE','Cancellation payment is not confirmed',409);return require('./cancellation.cjs').validateCancellationReceipt(job,subflow.receipt);},
234
+ async acceptCancellation(id,input,{actor,idempotencyKey,forward=true}={}){
235
+ if(typeof forward!=='boolean')throw new TypeError('forward must be a boolean');
236
+ let job=await workflow.act(id,'approve_cancellation',input,{actor,idempotencyKey});
237
+ const subflow=require('./cancellation.cjs').getCancellationSubflow(job);
238
+ if(!forward||job.pending||!subflow?.instruction||subflow.status!=='accepted')return job;
239
+ return workflow.act(id,'execute_cancellation',{instruction_id:subflow.instruction.id},{actor,idempotencyKey:'cancel_forward_'+digest(subflow.instruction.id)});
240
+ },
181
241
  async receipts(id, actor) {
182
242
  const job = await get(id, actor);
183
- return { protocol: job.protocol, job_id: id, state: job.state, agreement: job.agreement, submission: job.submission, 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 };
184
244
  }
185
245
  };
246
+ return workflow;
186
247
  }
187
- function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423', maxBodyBytes = 65536, onError } = {}) {
248
+ function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423', maxBodyBytes = 65536, onError, forwardCancellations = true, revalidateCancellationReceipt } = {}) {
249
+ if(typeof forwardCancellations!=='boolean')throw new TypeError('forwardCancellations must be a boolean');
188
250
  if (!workflow || typeof authenticate !== 'function' || !/^\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+$/.test(basePath)) throw new TypeError('Supply workflow, authenticate and valid basePath');
189
251
  if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes < 1 || maxBodyBytes > 1048576) throw new TypeError('Invalid maxBodyBytes');
190
252
  const respond = (value, status = 200) => Response.json(value, { status, headers: { 'cache-control': 'no-store' } });
@@ -192,10 +254,10 @@ function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423',
192
254
  try {
193
255
  const path = new URL(request.url).pathname;
194
256
  if (!path.startsWith(basePath + '/')) error('NOT_FOUND', 'Route not found', 404);
195
- const match = /^\/jobs(?:\/([A-Za-z0-9_-]{1,128})(?:\/(actions|reconcile|receipts))?)?$/.exec(path.slice(basePath.length));
257
+ const match = /^\/jobs(?:\/([A-Za-z0-9_-]{1,128})(?:\/(actions|reconcile|receipts|subflows|cancellation(?:\/(?:accept|execute|instruction|receipt))?))?)?$/.exec(path.slice(basePath.length));
196
258
  if (!match) error('NOT_FOUND', 'Route not found', 404);
197
259
  const [, id, action] = match;
198
- const method = !id || action === 'actions' || action === 'reconcile' ? 'POST' : 'GET';
260
+ const method = !id || ['actions','reconcile','cancellation/accept','cancellation/execute'].includes(action) ? 'POST' : 'GET';
199
261
  if (request.method !== method) { const r = respond({ error: { code: 'METHOD_NOT_ALLOWED', message: `Use ${method}` } }, 405); r.headers.set('allow', method); return r; }
200
262
  const actor = await authenticate(request);
201
263
  if (!actor) error('UNAUTHENTICATED', 'Authentication required', 401);
@@ -208,6 +270,16 @@ function createWorkflowHandler({ workflow, authenticate, basePath = '/api/a423',
208
270
  try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { error('INVALID_INPUT', 'Invalid JSON', 400); }
209
271
  }
210
272
  if (!id) return respond({ job: await workflow.create(body, actor) }, 201);
273
+ if(action==='subflows'){const job=await workflow.get(id,actor);return respond({job_id:id,subflows:job.subflows||[],active_subflow_id:job.active_subflow_id||null});}
274
+ if(action==='cancellation')return respond({job_id:id,subflow:await workflow.cancellation(id,actor)});
275
+ if(action==='cancellation/instruction')return respond({instruction:await workflow.cancellationInstruction(id,actor)});
276
+ if(action==='cancellation/receipt'){
277
+ const receipt=await workflow.cancellationReceipt(id,actor);
278
+ if(revalidateCancellationReceipt)await revalidateCancellationReceipt({job:await workflow.get(id,actor),receipt,actor});
279
+ return respond({receipt});
280
+ }
281
+ if(action==='cancellation/accept')return respond({job:await workflow.acceptCancellation(id,body,{actor,idempotencyKey:request.headers.get('idempotency-key'),forward:forwardCancellations})});
282
+ if(action==='cancellation/execute')return respond({job:await workflow.act(id,'execute_cancellation',body,{actor,idempotencyKey:request.headers.get('idempotency-key')})});
211
283
  if (action === 'actions') return respond({ job: await workflow.act(id, body.action, body.input ?? {}, { actor, idempotencyKey: request.headers.get('idempotency-key') }) });
212
284
  if (action === 'reconcile') return respond({ job: await workflow.reconcile(id, actor) });
213
285
  if (action === 'receipts') return respond(await workflow.receipts(id, actor));
@@ -240,12 +312,24 @@ function createWorkflowClient({ baseUrl, headers, fetch: fetchImpl = globalThis.
240
312
  return data;
241
313
  }
242
314
  const path = id => '/jobs/' + identifier(id);
243
- return {
315
+ const workflow={
244
316
  async create(input) { return (await request('/jobs',input)).job; },
245
317
  async get(id) { return (await request(path(id))).job; },
246
318
  async act(id,action,input,{idempotencyKey}={}) { identifier(action);identifier(idempotencyKey);return (await request(path(id)+'/actions',{action,input},idempotencyKey)).job; },
247
319
  async reconcile(id) { return (await request(path(id)+'/reconcile',{})).job; },
320
+ async cancellation(id){return (await request(path(id)+'/cancellation')).subflow;},
321
+ async cancellationInstruction(id){return (await request(path(id)+'/cancellation/instruction')).instruction;},
322
+ async cancellationReceipt(id){return (await request(path(id)+'/cancellation/receipt')).receipt;},
323
+ async subflows(id){return (await request(path(id)+'/subflows')).subflows;},
324
+ async acceptCancellation(id,input,{idempotencyKey}={}){identifier(idempotencyKey);return (await request(path(id)+'/cancellation/accept',input,idempotencyKey)).job;},
325
+ async executeCancellation(id,input,{idempotencyKey}={}){identifier(idempotencyKey);return (await request(path(id)+'/cancellation/execute',input,idempotencyKey)).job;},
248
326
  async receipts(id) { return request(path(id)+'/receipts'); }
249
327
  };
328
+ return workflow;
250
329
  }
251
330
  module.exports.createWorkflowClient = createWorkflowClient;
331
+
332
+ module.exports.getCancellationSubflow=(job)=>require('./cancellation.cjs').getCancellationSubflow(job);
333
+ module.exports.createCancellationInstruction=(job,proofs)=>require('./cancellation.cjs').createCancellationInstruction(job,proofs);
334
+ module.exports.validateCancellationInstruction=(job,instruction)=>require('./cancellation.cjs').validateCancellationInstruction(job,instruction);
335
+ module.exports.validateCancellationReceipt=(job,receipt)=>require('./cancellation.cjs').validateCancellationReceipt(job,receipt);
package/src/workflow.d.ts CHANGED
@@ -4,9 +4,25 @@ export interface Operation { id: string; key: string; action: string; actor: str
4
4
  export interface AdapterResult { status: 'pending' | 'confirmed' | 'rejected'; evidence?: { [key: string]: Json }; [key: string]: Json | undefined }
5
5
  export interface SettlementClaim {kind: 'work' | 'expense' | 'other';description: string;amount?: string;evidence?: string[]}
6
6
  export interface SettlementProposal {version: 'a423.closing.v1';job_id: string;agreement_hash: string;escrow_version: number;worker_amount: string;payer_amount: string;expires_at: number;reason: string;claims: SettlementClaim[];closes_job: true;proposal_hash: string;proposer_id?: string;at?: string}
7
- export interface Negotiation {status: 'open' | 'disputed' | 'settled';origin_state: string;current: SettlementProposal;history: SettlementProposal[];accepted_by?: string;settled_at?: string}
7
+ export interface Negotiation {status: 'open' | 'accepted' | 'disputed' | 'settled';acceptance_basis?: 'dual_wallet_signatures';origin_state: string;current: SettlementProposal;history: SettlementProposal[];accepted_by?: string;settled_at?: string}
8
8
  export interface EscrowLedger {funded_amount: string;worker_paid: string;payer_refunded: string;locked_amount: string;escrow_version: number}
9
+ export interface CancellationInstruction {
10
+ id: string;version: 'a423.cancellation_instruction.v1';purpose: 'close_job';funds_status: 'not_executed';job_id: string;subflow_id: string;agreement_hash: string;chain_id: string;escrow_contract: string;token: string;onchain_job_id: string;escrow_version: number;proposal_hash: string;expires_at: number;
11
+ payer: {agent_id: string;address: string;amount: string};worker: {agent_id: string;address: string;amount: string};proposal: SettlementProposal;
12
+ authorization: {scheme: 'eip712_closing_settlement_v1';payer_signature: string;worker_signature: string};
13
+ }
14
+ export interface CancellationReceipt {
15
+ id: string;version: 'a423.cancellation_receipt.v1';status: 'confirmed';job_id: string;subflow_id: string;agreement_hash: string;outcome: string;instruction_id: string | null;prior_instruction_id: string | null;
16
+ allocations: {worker: {address: string;amount: string};payer: {address: string;amount: string}};remaining_locked: '0';evidence: {[key: string]: Json};
17
+ }
18
+ export interface CancellationSubflow {
19
+ id: string;kind: 'cancellation';parent_job_id: string;status: 'proposal_pending'|'proposal_rejected'|'negotiating'|'acceptance_pending'|'accepted'|'execution_pending'|'execution_failed'|'completed'|'disputed';started_at: string;updated_at: string;proposal_revision: number;instruction: CancellationInstruction | null;receipt: CancellationReceipt | null;events: Array<{sequence: number;type: string;actor_id: string;at: string;data: Json}>;accepted_at?: string;approved_by?: string[];completed_at?: string;execution_reference?: string | null;closing_action?: string;last_error?: string | null;
20
+ }
21
+ export interface CancellationApproval {expected_revision: number;proposal_hash: string;payer_signature: string;worker_signature: string}
22
+ export interface PreFundingCancellation {id: string;epoch: number;action: 'cancel'|'decline';actor_id: string;prior_state: string;accepted_by: string[];agreement: Agreement | null;reason: string | null;at: string;reopen_approvals: string[];reopened_at: string | null}
9
23
  export interface WorkflowJob {
24
+ epoch?: number;agreement_revision?: number;pre_funding_cancellations?: PreFundingCancellation[];
25
+ subflows?: CancellationSubflow[];active_subflow_id?: string | null;
10
26
  work_status?: string;negotiation?: Negotiation | null;escrow_ledger?: EscrowLedger | null;
11
27
  id: string; protocol: 'a423.workflow.v1'; version: number; state: string;
12
28
  participants: { requester: string; worker: string; verifier: string; [role: string]: string };
@@ -27,9 +43,10 @@ export type Effect = (context: AdapterContext) => Promise<AdapterResult>;
27
43
  export interface Transition { from: string[]; roles: string[]; to: string; effect?: boolean }
28
44
  export interface WorkflowOptions {
29
45
  store: WorkflowStore;
30
- adapters?: Record<string, Effect | { execute: Effect; reconcile?: Effect }>;
46
+ adapters?: Record<string, Effect | { execute: Effect; reconcile?: Effect; rejectPending?: Effect }>;
31
47
  /** Return true to allow; authenticated actor ID must be supplied by your application. */
32
- authorize?(context: { job: WorkflowJob; action: string; actor: string; input: Json }): boolean | Promise<boolean>;
48
+ authorize?(context: { job: WorkflowJob; action: string; actor: string; input: Json;phase?: 'execute'|'reconcile' }): boolean | Promise<boolean>;
49
+ prepareInput?(context: {job: WorkflowJob;action: string;actor: string;input: Json}): Promise<Json>;
33
50
  /** Throw to reject application-specific terms. */
34
51
  validateEvidence?(context: {job: WorkflowJob;action: string;evidence: {[key: string]: Json}}): void | Promise<void>;
35
52
  validateTerms?(terms: { [key: string]: Json }): void | Promise<void>;
@@ -41,15 +58,20 @@ export interface Workflow {
41
58
  get(id: string, actor: string): Promise<WorkflowJob>;
42
59
  assertSafe(id: string, actor: string): Promise<WorkflowJob>;
43
60
  act(id: string, action: string, input: Json, options: { actor: string; idempotencyKey: string }): Promise<WorkflowJob>;
61
+ rejectPendingOperation(id: string,input: {operation_id: string;reason: string},options: {actor: string}): Promise<WorkflowJob>;
44
62
  resume(id: string, actor: string): Promise<WorkflowJob>;
45
63
  reconcile(id: string, actor: string): Promise<WorkflowJob>;
46
- receipts(id: string, actor: string): Promise<Pick<WorkflowJob, 'protocol' | 'state' | 'agreement' | 'submission' | 'evidence' | 'events' | 'pending' | 'work_status' | 'negotiation' | 'escrow_ledger'> & { job_id: string }>;
64
+ cancellation(id: string, actor: string): Promise<CancellationSubflow | null>;
65
+ cancellationInstruction(id: string, actor: string): Promise<CancellationInstruction>;
66
+ cancellationReceipt(id: string, actor: string): Promise<CancellationReceipt>;
67
+ acceptCancellation(id: string,input: CancellationApproval,options: {actor: string;idempotencyKey: string;forward?: boolean}): Promise<WorkflowJob>;
68
+ receipts(id: string, actor: string): Promise<Pick<WorkflowJob, 'epoch' | 'pre_funding_cancellations' | 'protocol' | 'state' | 'agreement' | 'submission' | 'evidence' | 'events' | 'pending' | 'work_status' | 'negotiation' | 'escrow_ledger' | 'subflows' | 'active_subflow_id'> & { job_id: string }>;
47
69
  }
48
70
  export function hashAgreement(terms: Json): string;
49
71
  export function createWorkflow(options: WorkflowOptions): Workflow;
50
72
  export function createWorkflowHandler(options: {
51
73
  workflow: Workflow; authenticate(request: Request): string | null | Promise<string | null>;
52
- basePath?: string; maxBodyBytes?: number; onError?(error: unknown): void | Promise<void>;
74
+ basePath?: string; maxBodyBytes?: number; forwardCancellations?: boolean; revalidateCancellationReceipt?(context: {job: WorkflowJob;receipt: CancellationReceipt;actor: string}): Promise<void>; onError?(error: unknown): void | Promise<void>;
53
75
  }): (request: Request) => Promise<Response>;
54
76
  export interface WorkflowClient {
55
77
  /** Supply a stable id so creation retries can be resolved with get(id). */
@@ -57,9 +79,20 @@ export interface WorkflowClient {
57
79
  get(id: string): Promise<WorkflowJob>;
58
80
  act(id: string, action: string, input: Json, options: { idempotencyKey: string }): Promise<WorkflowJob>;
59
81
  reconcile(id: string): Promise<WorkflowJob>;
82
+ cancellation(id: string): Promise<CancellationSubflow | null>;
83
+ cancellationInstruction(id: string): Promise<CancellationInstruction>;
84
+ cancellationReceipt(id: string): Promise<CancellationReceipt>;
85
+ subflows(id: string): Promise<CancellationSubflow[]>;
86
+ acceptCancellation(id: string,input: CancellationApproval,options: {idempotencyKey: string}): Promise<WorkflowJob>;
87
+ executeCancellation(id: string,input: {instruction_id: string;transaction_hash?: string},options: {idempotencyKey: string}): Promise<WorkflowJob>;
60
88
  receipts(id: string): ReturnType<Workflow['receipts']>;
61
89
  }
62
90
  export function createWorkflowClient(options: {
63
91
  baseUrl: string; headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
64
92
  fetch?: typeof globalThis.fetch; timeoutMs?: number;
65
93
  }): WorkflowClient;
94
+
95
+ export function getCancellationSubflow(job: WorkflowJob): CancellationSubflow | null;
96
+ export function createCancellationInstruction(job: WorkflowJob,proofs: {payer_signature: string;worker_signature: string}): CancellationInstruction;
97
+ export function validateCancellationInstruction(job: WorkflowJob,instruction: CancellationInstruction): CancellationInstruction;
98
+ export function validateCancellationReceipt(job: WorkflowJob,receipt: CancellationReceipt): CancellationReceipt;
package/src/workflow.mjs CHANGED
@@ -1,2 +1,2 @@
1
1
  import workflow from './workflow.cjs';
2
- export const { createWorkflow, createWorkflowHandler, createWorkflowClient, hashAgreement } = workflow;
2
+ export const { createWorkflow, createWorkflowHandler, createWorkflowClient, hashAgreement, getCancellationSubflow, createCancellationInstruction, validateCancellationInstruction, validateCancellationReceipt } = workflow;