flow-capture-hosted-apply.mjs
109 lines 3.9 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * Hosted Flow capture Hub-complete apply (CAPTURE-HOSTED-APPLY-KN-b / CHA-C2).
3 *
4 * After an admin / permitted-evaluator approve on the hosted canister, the bridge
5 * applies the capture proposal to `hub_flow_store.json` using the SAME
6 * `precheckApprovedCaptureProposal` + `applyCaptureProposal` pair as self-hosted
7 * Hub approve (`hub/server.mjs`) — no hosted-only precheck fork (CHA-C3).
8 *
9 * Lives in a sibling module (not `flow-capture-hosted-proposal.mjs`) because
10 * importing `flow-capture.mjs` from that module would create the load cycle
11 * flow-capture → proposals-store → hub-proposal-personal-self-apply →
12 * flow-capture-hosted-proposal. Nothing in that graph imports this module.
13 *
14 * T5 / personal self-apply stays refuse-all (`SELF_APPLY_NOT_ADMITTED` — SD-23);
15 * this helper is only reachable through Hub-complete approve authority.
16 *
17 * @see docs/CAPTURE-HOSTED-APPLY-FREEZE.md §CHA.2
18 * @see lib/task/task-hosted-proposal.mjs applyApprovedTaskProposalFromCanister
19 */
20
21 import {
22 precheckApprovedCaptureProposal,
23 applyCaptureProposal,
24 } from './flow-capture.mjs';
25 import {
26 fetchCanisterProposalForCapture,
27 normalizeCanisterProposalForCapturePrecheck,
28 } from './flow-capture-hosted-proposal.mjs';
29
30 /**
31 * Apply an approved canister capture proposal to the bridge flow store.
32 *
33 * Ordered per CHA-C2: fetch → capture-classify (400) → approved gate (409) →
34 * shared precheck (refusal passthrough, no store mutate) → shared apply.
35 * The canister proposal `body` string is passed through intact (CHA-C10) —
36 * `precheckApprovedCaptureProposal` parses it; empty/missing body refuses
37 * `FLOW_DRAFT_INVALID` without mutating the store.
38 *
39 * Blob hydrate/persist is the caller's job (bridge route wraps this in
40 * `withExternalProtocolBlobSync` — CHA-C3 cold-lambda candidate visibility).
41 *
42 * @param {{
43 * dataDir: string,
44 * canisterUrl: string,
45 * headers: Record<string, string>,
46 * proposalId: string,
47 * requireApproved?: boolean,
48 * }} opts
49 * @returns {Promise<{ ok: true, payload: Record<string, unknown> } | { ok: false, status: number, code: string, error: string }>}
50 */
51 export async function applyApprovedCaptureProposalFromCanister(opts) {
52 const proposalId = String(opts.proposalId || '').trim();
53 if (!opts.canisterUrl || !proposalId) {
54 return { ok: false, status: 400, code: 'BAD_REQUEST', error: 'canisterUrl and proposalId required' };
55 }
56
57 const fetched = await fetchCanisterProposalForCapture({
58 canisterUrl: opts.canisterUrl,
59 headers: opts.headers,
60 proposalId,
61 });
62 if (!fetched.ok) {
63 return { ok: false, status: 502, code: 'BAD_GATEWAY', error: 'Canister proposal fetch failed' };
64 }
65
66 const proposal = normalizeCanisterProposalForCapturePrecheck(fetched.proposal);
67 if (!proposal) {
68 return { ok: false, status: 400, code: 'BAD_REQUEST', error: 'Not a flow capture proposal' };
69 }
70
71 if (opts.requireApproved !== false && proposal.status !== 'approved') {
72 return {
73 ok: false,
74 status: 409,
75 code: 'CONFLICT',
76 error: 'Proposal must be approved before capture apply',
77 };
78 }
79
80 const precheck = precheckApprovedCaptureProposal(opts.dataDir, proposal);
81 if (!precheck.ok) {
82 return {
83 ok: false,
84 status: precheck.status,
85 code: precheck.code,
86 error: precheck.error,
87 };
88 }
89
90 const result = applyCaptureProposal(opts.dataDir, precheck);
91 return {
92 ok: true,
93 payload: {
94 applied: true,
95 proposal_id: proposalId,
96 vault_id: precheck.vaultId,
97 proposal_kind: precheck.proposalKind,
98 candidate_id: precheck.candidateId,
99 apply_result: result.applied,
100 ...(result.applied === 'promote'
101 ? { flow_id: result.flow_id, scope: result.scope }
102 : {}),
103 ...(result.applied === 'merge'
104 ? { merge_into_flow_id: result.merge_into_flow_id }
105 : {}),
106 ...(result.applied === 'dismiss' ? { dismissed: true } : {}),
107 },
108 };
109 }
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago