@ixo/oracle-runtime-workers 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -13
- package/package.json +10 -4
- package/src/action-proposals/policy.ts +54 -0
- package/src/action-proposals/reconcile.ts +87 -0
- package/src/action-proposals/store.ts +863 -0
- package/src/action-proposals/topic-budget.ts +84 -0
- package/src/action-proposals/topic-chain.ts +272 -0
- package/src/action-proposals/topic-effects.ts +44 -0
- package/src/action-proposals/topic-errors.ts +79 -0
- package/src/action-proposals/topic-history.ts +267 -0
- package/src/action-proposals/topic-matrix.ts +292 -0
- package/src/action-proposals/topic-prepare.ts +299 -0
- package/src/action-proposals/topic-store.ts +999 -0
- package/src/action-proposals/topic-verify.ts +279 -0
- package/src/action-proposals/verify.ts +223 -0
- package/src/channels/auth.ts +6 -1
- package/src/channels/contract.ts +4 -0
- package/src/channels/turns.ts +1 -0
- package/src/core/env.ts +89 -5
- package/src/core/main-agent-types.ts +8 -0
- package/src/core/main-agent.ts +2 -0
- package/src/core/runtime-context.ts +12 -0
- package/src/delivery/plan.ts +43 -2
- package/src/delivery/schema.ts +25 -1
- package/src/delivery/types.ts +13 -2
- package/src/do/ambient.ts +6 -0
- package/src/do/contracts.ts +39 -0
- package/src/do/user-oracle-do.ts +1253 -7
- package/src/hub-actions/client.ts +163 -0
- package/src/hub-actions/request.ts +110 -0
- package/src/hub-actions/store.ts +485 -0
- package/src/matrix/gateway-do.ts +176 -0
- package/src/matrix/reply-parts.ts +8 -2
- package/src/plugin-api/types.ts +54 -0
- package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
- package/src/plugins/action-proposals/evidence.ts +67 -0
- package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
- package/src/plugins/index.ts +6 -0
- package/src/shell/app.ts +195 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
operationJsonSchema,
|
|
3
|
+
parseOperation,
|
|
4
|
+
type ActionReferenceV1,
|
|
5
|
+
} from '@ixo/action-protocol';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
8
|
+
import type {
|
|
9
|
+
PluginManifest,
|
|
10
|
+
PluginTool,
|
|
11
|
+
RuntimeContext,
|
|
12
|
+
} from '../../plugin-api/types';
|
|
13
|
+
|
|
14
|
+
const configSchema = z.object({
|
|
15
|
+
HUB_ACTIONS_ENABLED: z.enum(['true', 'false']).default('false'),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const operationSchema = z.fromJSONSchema({
|
|
19
|
+
...operationJsonSchema,
|
|
20
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
21
|
+
});
|
|
22
|
+
const requestSchema = z.strictObject({ operation: operationSchema });
|
|
23
|
+
const statusSchema = z.strictObject({
|
|
24
|
+
actionId: z.string().regex(/^[A-Za-z0-9_-]{32}$/),
|
|
25
|
+
});
|
|
26
|
+
const discoverSchema = z.strictObject({});
|
|
27
|
+
|
|
28
|
+
export type HubActionRequestResult =
|
|
29
|
+
| { status: 'prepared'; actionReference: ActionReferenceV1; summary: string }
|
|
30
|
+
| {
|
|
31
|
+
status: 'unavailable' | 'expired';
|
|
32
|
+
reason: string;
|
|
33
|
+
/** The earlier action that blocks this one (an unconfirmed email). */
|
|
34
|
+
priorActionId?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The idempotency key the Hub sees: a fixed-length digest of the run and
|
|
39
|
+
* tool call (the Hub allows 120 characters; a run id alone is 72).
|
|
40
|
+
*/
|
|
41
|
+
async function hubRequestKey(runId: string, toolCallId: string) {
|
|
42
|
+
const bytes = new Uint8Array(
|
|
43
|
+
await crypto.subtle.digest(
|
|
44
|
+
'SHA-256',
|
|
45
|
+
new TextEncoder().encode(`${runId}:${toolCallId}`),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function requestHubAction(
|
|
52
|
+
input: unknown,
|
|
53
|
+
ctx: RuntimeContext,
|
|
54
|
+
): Promise<HubActionRequestResult> {
|
|
55
|
+
const parsed = requestSchema.safeParse(input);
|
|
56
|
+
if (!parsed.success)
|
|
57
|
+
return { status: 'unavailable', reason: 'An operation is required.' };
|
|
58
|
+
if (ctx.config.HUB_ACTIONS_ENABLED !== 'true' || !ctx.hubActions)
|
|
59
|
+
return {
|
|
60
|
+
status: 'unavailable',
|
|
61
|
+
reason: 'Hub actions are not enabled for this oracle.',
|
|
62
|
+
};
|
|
63
|
+
const channel = ctx.session.channel;
|
|
64
|
+
if (
|
|
65
|
+
ctx.session.client !== 'channel' ||
|
|
66
|
+
!channel ||
|
|
67
|
+
!ctx.session.runId ||
|
|
68
|
+
!ctx.toolCallId
|
|
69
|
+
)
|
|
70
|
+
return {
|
|
71
|
+
status: 'unavailable',
|
|
72
|
+
reason: 'A verified channel turn is required.',
|
|
73
|
+
};
|
|
74
|
+
let operation;
|
|
75
|
+
try {
|
|
76
|
+
operation = parseOperation(parsed.data.operation);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// The protocol's own words ("Invalid fromAddress"): the model can fix
|
|
79
|
+
// the field instead of guessing.
|
|
80
|
+
return {
|
|
81
|
+
status: 'unavailable',
|
|
82
|
+
reason: `The requested operation is unsupported or incomplete${error instanceof Error && error.message ? ` (${error.message})` : ''}.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let reference: ActionReferenceV1;
|
|
86
|
+
try {
|
|
87
|
+
reference = await ctx.hubActions.request({
|
|
88
|
+
ownerDid: ctx.user.did,
|
|
89
|
+
sessionId: ctx.session.id,
|
|
90
|
+
runId: ctx.session.runId,
|
|
91
|
+
requestId: ctx.session.requestId,
|
|
92
|
+
provider: channel.provider,
|
|
93
|
+
bindingId: channel.bindingId,
|
|
94
|
+
bindingRevision: channel.bindingRevision,
|
|
95
|
+
requestKey: await hubRequestKey(ctx.session.runId, ctx.toolCallId),
|
|
96
|
+
operation,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// A cap reached, a request that expired, a run no user message
|
|
100
|
+
// started: the runtime's own words, so the model can tell the user.
|
|
101
|
+
// Anything else stays an error.
|
|
102
|
+
if (
|
|
103
|
+
error instanceof Error &&
|
|
104
|
+
error.name === 'HubActionRefused' &&
|
|
105
|
+
'kind' in error
|
|
106
|
+
)
|
|
107
|
+
return {
|
|
108
|
+
status: error.kind === 'expired' ? 'expired' : 'unavailable',
|
|
109
|
+
reason: error.message,
|
|
110
|
+
...('priorActionId' in error && typeof error.priorActionId === 'string'
|
|
111
|
+
? { priorActionId: error.priorActionId }
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
status: 'prepared',
|
|
118
|
+
actionReference: reference,
|
|
119
|
+
summary: reference.title,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function hubActionStatus(
|
|
124
|
+
input: unknown,
|
|
125
|
+
ctx: RuntimeContext,
|
|
126
|
+
): Promise<unknown> {
|
|
127
|
+
const parsed = statusSchema.safeParse(input);
|
|
128
|
+
if (
|
|
129
|
+
!parsed.success ||
|
|
130
|
+
!ctx.hubActions ||
|
|
131
|
+
ctx.config.HUB_ACTIONS_ENABLED !== 'true'
|
|
132
|
+
)
|
|
133
|
+
return { status: 'unavailable' };
|
|
134
|
+
return {
|
|
135
|
+
status: 'known',
|
|
136
|
+
outcome: await ctx.hubActions.status(ctx.user.did, parsed.data.actionId),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function hubActionCapabilities(
|
|
141
|
+
_input: unknown,
|
|
142
|
+
ctx: RuntimeContext,
|
|
143
|
+
): Promise<unknown> {
|
|
144
|
+
if (
|
|
145
|
+
ctx.config.HUB_ACTIONS_ENABLED !== 'true' ||
|
|
146
|
+
ctx.session.client !== 'channel' ||
|
|
147
|
+
!ctx.session.channel ||
|
|
148
|
+
!ctx.hubActions
|
|
149
|
+
)
|
|
150
|
+
return { status: 'unavailable' };
|
|
151
|
+
return ctx.hubActions.capabilities();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function hubActionOptions(
|
|
155
|
+
_input: unknown,
|
|
156
|
+
ctx: RuntimeContext,
|
|
157
|
+
): Promise<unknown> {
|
|
158
|
+
const channel = ctx.session.channel;
|
|
159
|
+
if (
|
|
160
|
+
ctx.config.HUB_ACTIONS_ENABLED !== 'true' ||
|
|
161
|
+
ctx.session.client !== 'channel' ||
|
|
162
|
+
!channel ||
|
|
163
|
+
!ctx.hubActions
|
|
164
|
+
)
|
|
165
|
+
return { status: 'unavailable' };
|
|
166
|
+
return ctx.hubActions.options({
|
|
167
|
+
ownerDid: ctx.user.did,
|
|
168
|
+
provider: channel.provider,
|
|
169
|
+
bindingId: channel.bindingId,
|
|
170
|
+
bindingRevision: channel.bindingRevision,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const manifest: PluginManifest = {
|
|
175
|
+
title: 'Auth Hub action review',
|
|
176
|
+
summary:
|
|
177
|
+
'Prepare a registered operation for owner review and read its verified status.',
|
|
178
|
+
whenToUse: [
|
|
179
|
+
'The user requests a registered form submission or a transfer.',
|
|
180
|
+
'The user asks to review an exact private resource or to sign a statement.',
|
|
181
|
+
'The user asks to connect Gmail or Calendar, send a reviewed email or add an event.',
|
|
182
|
+
'The user asks for document access or to save a private note.',
|
|
183
|
+
],
|
|
184
|
+
whenNotToUse: [
|
|
185
|
+
'Signing, executing, or treating a resource open as acceptance.',
|
|
186
|
+
],
|
|
187
|
+
tags: ['actions', 'channels'],
|
|
188
|
+
category: 'integration',
|
|
189
|
+
visibility: 'on-demand',
|
|
190
|
+
stability: 'experimental',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export class HubActionsPlugin extends OraclePlugin {
|
|
194
|
+
readonly name = 'hub-actions';
|
|
195
|
+
readonly version = '0.1.0';
|
|
196
|
+
readonly manifest = manifest;
|
|
197
|
+
override readonly configSchema = configSchema;
|
|
198
|
+
/** The flag alone is not enough: the owner object needs the Hub's URL and this oracle's key. */
|
|
199
|
+
override autoDetect(env: Record<string, unknown>): boolean {
|
|
200
|
+
return (
|
|
201
|
+
env.HUB_ACTIONS_ENABLED === 'true' &&
|
|
202
|
+
typeof env.AUTH_HUB_URL === 'string' &&
|
|
203
|
+
env.AUTH_HUB_URL.length > 0 &&
|
|
204
|
+
typeof env.AUTH_HUB_CHANNEL_SERVICE_KEY === 'string' &&
|
|
205
|
+
env.AUTH_HUB_CHANNEL_SERVICE_KEY.length > 0
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
override getTools(): PluginTool[] {
|
|
209
|
+
return [
|
|
210
|
+
{
|
|
211
|
+
name: 'hub_action_capabilities',
|
|
212
|
+
description:
|
|
213
|
+
"Read this authenticated oracle account's enabled operation kinds and exact registered form field schema before proposing a Hub action.",
|
|
214
|
+
schema: discoverSchema,
|
|
215
|
+
effect: 'read',
|
|
216
|
+
handler: hubActionCapabilities,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'hub_action_options',
|
|
220
|
+
description:
|
|
221
|
+
'Read this authenticated owner’s confirmed Gmail and Calendar connections, requestable documents and action resources. Use each returned service, identifier and label before preparing a related Hub review. A Gmail profile connection does not authorize sending.',
|
|
222
|
+
schema: discoverSchema,
|
|
223
|
+
effect: 'read',
|
|
224
|
+
handler: hubActionOptions,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'request_hub_action',
|
|
228
|
+
description:
|
|
229
|
+
'Prepare a supported operation for the authenticated channel owner to review in Auth Hub. Never approves, signs, or executes. For email, propose one plain-text message to one recipient using a confirmed Gmail send connection. Supply only the operation candidate; the runtime supplies identity and turn binding. Never prepare a replacement email after an unknown send without the user checking Gmail’s Sent folder and asking again.',
|
|
230
|
+
schema: requestSchema,
|
|
231
|
+
effect: 'write',
|
|
232
|
+
handler: requestHubAction,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'hub_action_status',
|
|
236
|
+
description:
|
|
237
|
+
'Read the verified status of a Hub action already prepared for this owner. Never executes.',
|
|
238
|
+
schema: statusSchema,
|
|
239
|
+
effect: 'read',
|
|
240
|
+
handler: hubActionStatus,
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ActionProposalsPlugin } from './action-proposals/action-proposals.plugin';
|
|
2
|
+
import { HubActionsPlugin } from './hub-actions/hub-actions.plugin';
|
|
3
|
+
export { ActionProposalsPlugin } from './action-proposals/action-proposals.plugin';
|
|
4
|
+
export { HubActionsPlugin } from './hub-actions/hub-actions.plugin';
|
|
1
5
|
/**
|
|
2
6
|
* Bundled Workers-runtime plugins — ports of the Node runtime's MCP / fetch
|
|
3
7
|
* plugins. Each is gated the same way as on Node: `autoDetect` on its env
|
|
@@ -66,6 +70,8 @@ export const tasksPlugin = TasksPlugin;
|
|
|
66
70
|
* instances instead of using these singletons.
|
|
67
71
|
*/
|
|
68
72
|
export const BUNDLED_WORKERS_PLUGINS = [
|
|
73
|
+
new ActionProposalsPlugin(),
|
|
74
|
+
new HubActionsPlugin(),
|
|
69
75
|
memoryPlugin,
|
|
70
76
|
sandboxPlugin,
|
|
71
77
|
firecrawlPlugin,
|
package/src/shell/app.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionProposalWireSchema,
|
|
3
|
+
TopicReceiptReferencesSchema,
|
|
4
|
+
BeginTopicStepSchema,
|
|
5
|
+
BeginTopicStepResultSchema,
|
|
6
|
+
} from '@ixo/ixo-transaction/topic-proposals';
|
|
7
|
+
import {
|
|
8
|
+
ActionProposalClaimSchema,
|
|
9
|
+
ActionProposalIdSchema,
|
|
10
|
+
ActionProposalReconcileSchema,
|
|
11
|
+
} from '@ixo/ixo-transaction/action-proposals';
|
|
12
|
+
import { ActionProposalConflict } from '../action-proposals/store';
|
|
1
13
|
import {
|
|
2
14
|
authenticateChannel,
|
|
3
15
|
assertActiveChannelBindingCached,
|
|
@@ -65,6 +77,23 @@ import {
|
|
|
65
77
|
type FeedbackShellOptions,
|
|
66
78
|
} from '../feedback/submit';
|
|
67
79
|
import { z } from 'zod';
|
|
80
|
+
import { parseOutcome } from '@ixo/action-protocol';
|
|
81
|
+
|
|
82
|
+
async function equalOutcomeKeys(
|
|
83
|
+
provided: string | null,
|
|
84
|
+
expected: string | undefined,
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
if (!provided || !expected || expected.length < 32 || provided.length > 512)
|
|
87
|
+
return false;
|
|
88
|
+
const digest = async (value: string) =>
|
|
89
|
+
new Uint8Array(
|
|
90
|
+
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)),
|
|
91
|
+
);
|
|
92
|
+
const [a, b] = await Promise.all([digest(provided), digest(expected)]);
|
|
93
|
+
let difference = 0;
|
|
94
|
+
for (let i = 0; i < a.length; i++) difference |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
95
|
+
return difference === 0;
|
|
96
|
+
}
|
|
68
97
|
|
|
69
98
|
const DebugEventBody = z.object({
|
|
70
99
|
type: z
|
|
@@ -217,6 +246,7 @@ export function rateLimitKey(
|
|
|
217
246
|
| 'user'
|
|
218
247
|
| 'poll'
|
|
219
248
|
| 'channel'
|
|
249
|
+
| 'action'
|
|
220
250
|
| 'artifact'
|
|
221
251
|
| 'socket'
|
|
222
252
|
| 'status'
|
|
@@ -424,6 +454,38 @@ export function createShell(
|
|
|
424
454
|
);
|
|
425
455
|
});
|
|
426
456
|
|
|
457
|
+
app.use('/api/internal/actions/outcomes', limitBody(64 * 1024));
|
|
458
|
+
app.post('/api/internal/actions/outcomes', async (c) => {
|
|
459
|
+
if (
|
|
460
|
+
c.env.HUB_ACTIONS_ENABLED !== 'true' ||
|
|
461
|
+
!(await equalOutcomeKeys(
|
|
462
|
+
c.req.header('x-auth-hub-outcome-key') ?? null,
|
|
463
|
+
c.env.AUTH_HUB_ACTION_OUTCOME_KEY,
|
|
464
|
+
))
|
|
465
|
+
)
|
|
466
|
+
return c.json({ code: 'unauthorized' }, 401);
|
|
467
|
+
const body = await c.req.raw.text();
|
|
468
|
+
if (body.length > 64_000) return c.json({ code: 'request_too_large' }, 413);
|
|
469
|
+
try {
|
|
470
|
+
const outcome = parseOutcome(JSON.parse(body));
|
|
471
|
+
await userStub(c.env, outcome.ownerDid).recordHubActionOutcome(outcome);
|
|
472
|
+
return c.json({
|
|
473
|
+
accepted: true,
|
|
474
|
+
eventId: outcome.eventId,
|
|
475
|
+
accountId: outcome.reference.accountId,
|
|
476
|
+
actionId: outcome.reference.id,
|
|
477
|
+
revision: outcome.authorizedRevision,
|
|
478
|
+
digest: outcome.authorizedDigest,
|
|
479
|
+
sequence: outcome.sequence,
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.warn(
|
|
483
|
+
`[hub-actions] outcome refused: ${error instanceof Error ? error.message : String(error)}`,
|
|
484
|
+
);
|
|
485
|
+
return c.json({ code: 'outcome_refused' }, 409);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
427
489
|
app.post('/channels/turn', async (c) => {
|
|
428
490
|
try {
|
|
429
491
|
if (parseChannelServiceDids(c.env.CHANNEL_SERVICE_DID).length === 0)
|
|
@@ -478,6 +540,8 @@ export function createShell(
|
|
|
478
540
|
outcome.status,
|
|
479
541
|
);
|
|
480
542
|
const result = outcome.result;
|
|
543
|
+
if (result.actionOutcomes?.length || result.hubActionOutcomes?.length)
|
|
544
|
+
await assertActiveChannelBindingCached(identity, c.env);
|
|
481
545
|
return c.json(
|
|
482
546
|
result,
|
|
483
547
|
['queued', 'running', 'recovering'].includes(result.status) ? 202 : 200,
|
|
@@ -538,8 +602,15 @@ export function createShell(
|
|
|
538
602
|
if (!limiter) return next();
|
|
539
603
|
const auth = c.get('auth') as AuthResult | undefined;
|
|
540
604
|
if (!auth) return next();
|
|
605
|
+
// The chat lists' status poll and the card routes (Portal's review
|
|
606
|
+
// page) each have their own bucket: polling a card or the run list
|
|
607
|
+
// while chatting must not push turns into 429.
|
|
541
608
|
const scope =
|
|
542
|
-
c.req.method === 'GET' && c.req.path === '/runs/active'
|
|
609
|
+
c.req.method === 'GET' && c.req.path === '/runs/active'
|
|
610
|
+
? 'poll'
|
|
611
|
+
: c.req.path.startsWith('/action-proposals/')
|
|
612
|
+
? 'action'
|
|
613
|
+
: 'user';
|
|
543
614
|
const { success } = await limiter.limit({
|
|
544
615
|
key: rateLimitKey(c.env, scope, auth.userDid),
|
|
545
616
|
});
|
|
@@ -555,6 +626,129 @@ export function createShell(
|
|
|
555
626
|
return next();
|
|
556
627
|
});
|
|
557
628
|
|
|
629
|
+
// What the owner object answers a proposal command with: the card; a
|
|
630
|
+
// conflict (the card or the authority it was sealed on changed: the Portal
|
|
631
|
+
// re-reads and retries, 409); or an authority read that failed and may
|
|
632
|
+
// succeed later (503, with a generic message).
|
|
633
|
+
const ProposalReply = z.discriminatedUnion('ok', [
|
|
634
|
+
z.object({ ok: z.literal(true), proposal: ActionProposalWireSchema }),
|
|
635
|
+
z.object({
|
|
636
|
+
ok: z.literal(false),
|
|
637
|
+
error: z.string(),
|
|
638
|
+
kind: z.enum(['conflict', 'unavailable']).default('conflict'),
|
|
639
|
+
}),
|
|
640
|
+
]);
|
|
641
|
+
const refusalStatus = (kind: 'conflict' | 'unavailable') =>
|
|
642
|
+
kind === 'unavailable' ? 503 : 409;
|
|
643
|
+
|
|
644
|
+
app.get('/action-proposals/:proposalId', async (c) => {
|
|
645
|
+
c.header('Cache-Control', 'private, no-store');
|
|
646
|
+
c.header('Vary', 'Authorization');
|
|
647
|
+
const parsed = ActionProposalIdSchema.safeParse(c.req.param('proposalId'));
|
|
648
|
+
if (!parsed.success) return c.json({ error: 'Invalid proposal ID' }, 400);
|
|
649
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
650
|
+
const proposal = await userStub(c.env, identity.userDid).actionProposal(
|
|
651
|
+
identity,
|
|
652
|
+
parsed.data,
|
|
653
|
+
);
|
|
654
|
+
if (!proposal) return c.json({ error: 'Proposal not found' }, 404);
|
|
655
|
+
const result = ProposalReply.parse(JSON.parse(proposal));
|
|
656
|
+
return result.ok
|
|
657
|
+
? c.json({ proposal: result.proposal })
|
|
658
|
+
: c.json({ error: result.error }, refusalStatus(result.kind));
|
|
659
|
+
});
|
|
660
|
+
for (const operation of ['claim', 'reject', 'revoke', 'reconcile'] as const) {
|
|
661
|
+
app.post(
|
|
662
|
+
`/action-proposals/:proposalId/${operation}`,
|
|
663
|
+
limitBody(4096),
|
|
664
|
+
async (c) => {
|
|
665
|
+
c.header('Cache-Control', 'private, no-store');
|
|
666
|
+
c.header('Vary', 'Authorization');
|
|
667
|
+
const id = ActionProposalIdSchema.safeParse(c.req.param('proposalId'));
|
|
668
|
+
const body = await c.req.json().catch(() => null);
|
|
669
|
+
const parsed = (
|
|
670
|
+
operation === 'reconcile'
|
|
671
|
+
? z.union([
|
|
672
|
+
ActionProposalReconcileSchema,
|
|
673
|
+
TopicReceiptReferencesSchema,
|
|
674
|
+
])
|
|
675
|
+
: ActionProposalClaimSchema
|
|
676
|
+
).safeParse(body);
|
|
677
|
+
if (!id.success || !parsed.success)
|
|
678
|
+
return c.json({ error: 'Invalid proposal command' }, 400);
|
|
679
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
680
|
+
try {
|
|
681
|
+
const command =
|
|
682
|
+
operation === 'reconcile'
|
|
683
|
+
? 'v' in parsed.data
|
|
684
|
+
? {
|
|
685
|
+
kind: 'topic-reconcile' as const,
|
|
686
|
+
input: TopicReceiptReferencesSchema.parse(parsed.data),
|
|
687
|
+
}
|
|
688
|
+
: {
|
|
689
|
+
kind: operation,
|
|
690
|
+
...ActionProposalReconcileSchema.parse(parsed.data),
|
|
691
|
+
}
|
|
692
|
+
: {
|
|
693
|
+
kind: operation,
|
|
694
|
+
...ActionProposalClaimSchema.parse(parsed.data),
|
|
695
|
+
};
|
|
696
|
+
const proposal = await userStub(
|
|
697
|
+
c.env,
|
|
698
|
+
identity.userDid,
|
|
699
|
+
).actionProposal(identity, id.data, command);
|
|
700
|
+
if (!proposal) return c.json({ error: 'Proposal not found' }, 404);
|
|
701
|
+
const result = ProposalReply.parse(JSON.parse(proposal));
|
|
702
|
+
return result.ok
|
|
703
|
+
? c.json({ proposal: result.proposal })
|
|
704
|
+
: c.json({ error: result.error }, refusalStatus(result.kind));
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (
|
|
707
|
+
error instanceof ActionProposalConflict ||
|
|
708
|
+
(error instanceof Error && error.name === 'ActionProposalConflict')
|
|
709
|
+
)
|
|
710
|
+
return c.json({ error: error.message }, 409);
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
app.post(
|
|
718
|
+
'/action-proposals/:proposalId/begin-step',
|
|
719
|
+
limitBody(4096),
|
|
720
|
+
async (c) => {
|
|
721
|
+
c.header('Cache-Control', 'private, no-store');
|
|
722
|
+
c.header('Vary', 'Authorization');
|
|
723
|
+
const id = ActionProposalIdSchema.safeParse(c.req.param('proposalId'));
|
|
724
|
+
const body = BeginTopicStepSchema.safeParse(
|
|
725
|
+
await c.req.json().catch(() => null),
|
|
726
|
+
);
|
|
727
|
+
if (!id.success || !body.success)
|
|
728
|
+
return c.json({ error: 'Invalid topic step command' }, 400);
|
|
729
|
+
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|
|
730
|
+
const raw = await userStub(c.env, identity.userDid).beginTopicStep(
|
|
731
|
+
identity,
|
|
732
|
+
id.data,
|
|
733
|
+
body.data,
|
|
734
|
+
);
|
|
735
|
+
if (!raw) return c.json({ error: 'Proposal not found' }, 404);
|
|
736
|
+
const response = z
|
|
737
|
+
.discriminatedUnion('ok', [
|
|
738
|
+
z.object({ ok: z.literal(true), result: BeginTopicStepResultSchema }),
|
|
739
|
+
z.object({
|
|
740
|
+
ok: z.literal(false),
|
|
741
|
+
error: z.string(),
|
|
742
|
+
kind: z.enum(['conflict', 'unavailable']).default('conflict'),
|
|
743
|
+
}),
|
|
744
|
+
])
|
|
745
|
+
.parse(JSON.parse(raw));
|
|
746
|
+
return response.ok
|
|
747
|
+
? c.json(response.result)
|
|
748
|
+
: c.json({ error: response.error }, refusalStatus(response.kind));
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
|
|
558
752
|
// --- sessions ----------------------------------------------------------------
|
|
559
753
|
app.post('/sessions', async (c) => {
|
|
560
754
|
const identity = identityOf(c.get('auth'), c.req.raw.headers);
|