task-approve-hosted.mjs
119 lines 3.6 KB
Raw
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 11 days ago
1 /**
2 * Gateway hook: after canister proposal approve, apply bridge task indexes (2G hosted parity).
3 */
4
5 import { proposalIdFromApprovePath } from '../../lib/muse-thin-bridge.mjs';
6 import {
7 fetchCanisterProposalForTask,
8 normalizeCanisterProposalForTaskPrecheck,
9 } from '../../lib/task/task-hosted-proposal.mjs';
10
11 const PROPOSAL_APPROVE_RE = /^\/api\/v1\/proposals\/[^/]+\/approve\/?$/;
12
13 /**
14 * @param {Record<string, unknown>} proposal
15 * @returns {boolean}
16 */
17 function isTaskProposal(proposal) {
18 return normalizeCanisterProposalForTaskPrecheck(proposal) != null;
19 }
20
21 /**
22 * @param {{
23 * method: string,
24 * pathOnly: string,
25 * upstreamStatus: number,
26 * canisterUrl: string,
27 * bridgeUrl: string,
28 * authorization: string|undefined,
29 * vaultId: string,
30 * effectiveUserId: string,
31 * actorUserId: string,
32 * canisterAuthHeaders: () => Record<string, string>,
33 * }} ctx
34 * @returns {Promise<{ applied: boolean, error?: string, code?: string, payload?: Record<string, unknown> }|null>}
35 */
36 export async function maybeApplyHostedTaskAfterApprove(ctx) {
37 if (ctx.method !== 'POST' || !PROPOSAL_APPROVE_RE.test(ctx.pathOnly)) return null;
38 if (ctx.upstreamStatus < 200 || ctx.upstreamStatus >= 300) return null;
39
40 const proposalId = proposalIdFromApprovePath(ctx.pathOnly);
41 if (!proposalId || !ctx.bridgeUrl || !ctx.canisterUrl) return null;
42
43 const headers = {
44 ...ctx.canisterAuthHeaders(),
45 'X-User-Id': ctx.effectiveUserId,
46 'X-Actor-Id': ctx.actorUserId,
47 'X-Vault-Id': ctx.vaultId,
48 };
49
50 const fetched = await fetchCanisterProposalForTask({
51 canisterUrl: ctx.canisterUrl,
52 headers,
53 proposalId,
54 });
55 if (!fetched.ok) return null;
56 if (!isTaskProposal(fetched.proposal)) return null;
57
58 const bridgeRes = await fetch(
59 `${ctx.bridgeUrl.replace(/\/$/, '')}/api/v1/tasks/proposals/${encodeURIComponent(proposalId)}/apply-approved`,
60 {
61 method: 'POST',
62 headers: {
63 Accept: 'application/json',
64 'Content-Type': 'application/json',
65 ...(ctx.authorization ? { Authorization: ctx.authorization } : {}),
66 'X-Vault-Id': ctx.vaultId,
67 },
68 body: JSON.stringify({}),
69 },
70 );
71
72 const text = await bridgeRes.text();
73 /** @type {Record<string, unknown>} */
74 let json = {};
75 try {
76 json = text ? JSON.parse(text) : {};
77 } catch {
78 json = {};
79 }
80
81 if (!bridgeRes.ok) {
82 return {
83 applied: false,
84 error: typeof json.error === 'string' ? json.error : text.slice(0, 200),
85 code: typeof json.code === 'string' ? json.code : 'TASK_APPLY_FAILED',
86 };
87 }
88
89 return {
90 applied: true,
91 payload: json && typeof json === 'object' ? /** @type {Record<string, unknown>} */ (json) : {},
92 };
93 }
94
95 /**
96 * Merge task apply outcome into canister approve JSON for the Hub client.
97 *
98 * @param {string} responseText
99 * @param {{ applied: boolean, error?: string, code?: string, payload?: Record<string, unknown> }|null} applyOutcome
100 * @returns {string}
101 */
102 export function mergeTaskApplyIntoApproveResponse(responseText, applyOutcome) {
103 if (!applyOutcome) return responseText;
104 try {
105 const body = JSON.parse(responseText);
106 if (!body || typeof body !== 'object') return responseText;
107 body.task_index_applied = applyOutcome.applied;
108 if (applyOutcome.applied && applyOutcome.payload) {
109 body.task_apply = applyOutcome.payload;
110 }
111 if (!applyOutcome.applied) {
112 body.task_apply_error = applyOutcome.error ?? 'Task index apply failed';
113 body.task_apply_code = applyOutcome.code ?? 'TASK_APPLY_FAILED';
114 }
115 return JSON.stringify(body);
116 } catch {
117 return responseText;
118 }
119 }
File History 1 commit
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 11 days ago