@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.
Files changed (39) hide show
  1. package/README.md +15 -13
  2. package/package.json +10 -4
  3. package/src/action-proposals/policy.ts +54 -0
  4. package/src/action-proposals/reconcile.ts +87 -0
  5. package/src/action-proposals/store.ts +863 -0
  6. package/src/action-proposals/topic-budget.ts +84 -0
  7. package/src/action-proposals/topic-chain.ts +272 -0
  8. package/src/action-proposals/topic-effects.ts +44 -0
  9. package/src/action-proposals/topic-errors.ts +79 -0
  10. package/src/action-proposals/topic-history.ts +267 -0
  11. package/src/action-proposals/topic-matrix.ts +292 -0
  12. package/src/action-proposals/topic-prepare.ts +299 -0
  13. package/src/action-proposals/topic-store.ts +999 -0
  14. package/src/action-proposals/topic-verify.ts +279 -0
  15. package/src/action-proposals/verify.ts +223 -0
  16. package/src/channels/auth.ts +6 -1
  17. package/src/channels/contract.ts +4 -0
  18. package/src/channels/turns.ts +1 -0
  19. package/src/core/env.ts +89 -5
  20. package/src/core/main-agent-types.ts +8 -0
  21. package/src/core/main-agent.ts +2 -0
  22. package/src/core/runtime-context.ts +12 -0
  23. package/src/delivery/plan.ts +43 -2
  24. package/src/delivery/schema.ts +25 -1
  25. package/src/delivery/types.ts +13 -2
  26. package/src/do/ambient.ts +6 -0
  27. package/src/do/contracts.ts +39 -0
  28. package/src/do/user-oracle-do.ts +1253 -7
  29. package/src/hub-actions/client.ts +163 -0
  30. package/src/hub-actions/request.ts +110 -0
  31. package/src/hub-actions/store.ts +485 -0
  32. package/src/matrix/gateway-do.ts +176 -0
  33. package/src/matrix/reply-parts.ts +8 -2
  34. package/src/plugin-api/types.ts +54 -0
  35. package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
  36. package/src/plugins/action-proposals/evidence.ts +67 -0
  37. package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
  38. package/src/plugins/index.ts +6 -0
  39. package/src/shell/app.ts +195 -1
@@ -0,0 +1,163 @@
1
+ import {
2
+ assertHubReferenceUrl,
3
+ parseCapabilities,
4
+ parseActionOptions,
5
+ parseOperation,
6
+ parseOutcome,
7
+ type ActionOperationV1,
8
+ type ActionCapabilitiesV1,
9
+ type ActionOutcomeV1,
10
+ type ActionReferenceV1,
11
+ type HubActionOptionsV2,
12
+ } from '@ixo/action-protocol';
13
+
14
+ export interface HubActionStatus {
15
+ reference: ActionReferenceV1;
16
+ ownerDid: string;
17
+ revision: number;
18
+ digest: string;
19
+ reviewStatus: string;
20
+ operationStatus: string;
21
+ outcome: ActionOutcomeV1 | null;
22
+ }
23
+
24
+ export interface HubActionTransport {
25
+ origin: string;
26
+ key: string;
27
+ fetcher?: { fetch(request: Request): Promise<Response> };
28
+ }
29
+
30
+ /** The envelope version this runtime parses (`ixo.auth-outcome/2`, `ixo.auth-capabilities/2`). */
31
+ const HUB_PROTOCOL_QUERY = 'version=2';
32
+
33
+ function isRecord(value: unknown): value is Record<string, unknown> {
34
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+
37
+ export class HubActionClient {
38
+ private readonly origin: string;
39
+
40
+ constructor(private readonly transport: HubActionTransport) {
41
+ const url = new URL(transport.origin);
42
+ if (
43
+ !(
44
+ url.protocol === 'https:' ||
45
+ (url.protocol === 'http:' && url.hostname === 'localhost')
46
+ ) ||
47
+ url.pathname !== '/' ||
48
+ url.search ||
49
+ url.hash ||
50
+ url.username ||
51
+ url.password ||
52
+ !transport.key
53
+ )
54
+ throw new Error('Hub action transport requires a secure origin and key');
55
+ this.origin = url.origin;
56
+ }
57
+
58
+ private async send(
59
+ path: string,
60
+ body?: unknown,
61
+ timeoutMs = 10_000,
62
+ ): Promise<unknown> {
63
+ const request = new Request(new URL(path, this.origin), {
64
+ method: body === undefined ? 'GET' : 'POST',
65
+ headers: {
66
+ 'x-channels-service-key': this.transport.key,
67
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
68
+ },
69
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
70
+ signal: AbortSignal.timeout(timeoutMs),
71
+ redirect: 'manual',
72
+ });
73
+ const response = await (this.transport.fetcher
74
+ ? this.transport.fetcher.fetch(request)
75
+ : fetch(request));
76
+ if (!response.ok)
77
+ throw new Error(`Hub action request returned ${response.status}`);
78
+ return response.json();
79
+ }
80
+
81
+ async request(input: {
82
+ bindingId: string;
83
+ bindingRevision: number;
84
+ requestKey: string;
85
+ expiresAt: number;
86
+ operation: ActionOperationV1;
87
+ }): Promise<ActionReferenceV1> {
88
+ // `version=2` on the three operation calls: the Hub answers v1 (P1
89
+ // kinds only, v1-filtered outcomes, a P2 POST refused) without it, and
90
+ // stores the version per action for the outcomes it pushes.
91
+ const response = await this.send(
92
+ `/api/internal/actions/operations?${HUB_PROTOCOL_QUERY}`,
93
+ {
94
+ ...input,
95
+ operation: parseOperation(input.operation),
96
+ },
97
+ );
98
+ if (!response || typeof response !== 'object' || !('reference' in response))
99
+ throw new Error('Hub action response has no reference');
100
+ return assertHubReferenceUrl(response.reference, this.origin);
101
+ }
102
+
103
+ async capabilities(): Promise<ActionCapabilitiesV1> {
104
+ return parseCapabilities(
105
+ await this.send(
106
+ `/api/internal/actions/operations/capabilities?${HUB_PROTOCOL_QUERY}`,
107
+ ),
108
+ );
109
+ }
110
+
111
+ async options(input: {
112
+ bindingId: string;
113
+ bindingRevision: number;
114
+ }): Promise<HubActionOptionsV2> {
115
+ const query = new URLSearchParams({
116
+ bindingId: input.bindingId,
117
+ bindingRevision: String(input.bindingRevision),
118
+ version: '2',
119
+ });
120
+ const options = parseActionOptions(
121
+ await this.send(
122
+ `/api/internal/actions/operations/options?${query.toString()}`,
123
+ ),
124
+ );
125
+ if (options.version !== 'ixo.auth-options/2')
126
+ throw new Error('Hub action options require version 2');
127
+ return options;
128
+ }
129
+
130
+ /** `timeoutMs`: the poll path passes a short one (a slow Hub must not hold a poll). */
131
+ async status(id: string, timeoutMs = 10_000): Promise<HubActionStatus> {
132
+ if (!/^[A-Za-z0-9_-]{32}$/.test(id))
133
+ throw new Error('Invalid Hub action id');
134
+ const raw = await this.send(
135
+ `/api/internal/actions/operations/${id}?${HUB_PROTOCOL_QUERY}`,
136
+ undefined,
137
+ timeoutMs,
138
+ );
139
+ if (!isRecord(raw)) throw new Error('Invalid Hub action status');
140
+ const value = raw;
141
+ const reference = assertHubReferenceUrl(value.reference, this.origin);
142
+ if (
143
+ reference.id !== id ||
144
+ typeof value.ownerDid !== 'string' ||
145
+ !value.ownerDid.startsWith('did:ixo:') ||
146
+ !Number.isSafeInteger(value.revision) ||
147
+ typeof value.digest !== 'string' ||
148
+ !/^[a-f0-9]{64}$/.test(value.digest) ||
149
+ typeof value.reviewStatus !== 'string' ||
150
+ typeof value.operationStatus !== 'string'
151
+ )
152
+ throw new Error('Invalid Hub action status');
153
+ return {
154
+ reference,
155
+ ownerDid: value.ownerDid,
156
+ revision: Number(value.revision),
157
+ digest: value.digest,
158
+ reviewStatus: value.reviewStatus,
159
+ operationStatus: value.operationStatus,
160
+ outcome: value.outcome == null ? null : parseOutcome(value.outcome),
161
+ };
162
+ }
163
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ canonicalJson,
3
+ type ActionCapabilitiesV1,
4
+ type ActionOperationV1,
5
+ type ActionReferenceV1,
6
+ } from '@ixo/action-protocol';
7
+ import type { HubActionClient } from './client';
8
+ import { HubActionRefused, type HubActionOrigin } from './store';
9
+
10
+ export interface PreparedHubActionRequest extends HubActionOrigin {
11
+ provider: string;
12
+ operation: ActionOperationV1;
13
+ }
14
+
15
+ export function assertOperationEnabled(
16
+ capabilities: ActionCapabilitiesV1,
17
+ operation: ActionOperationV1,
18
+ ownerDid: string,
19
+ ): void {
20
+ if (!capabilities.operations.includes(operation.kind))
21
+ throw new Error('Hub operation is not enabled for this oracle');
22
+ if (operation.kind === 'form.submit.v1') {
23
+ const form = capabilities.forms.find(
24
+ (item) => item.formId === operation.formId,
25
+ );
26
+ if (
27
+ !form ||
28
+ form.destination !== operation.destination ||
29
+ form.declaration !== operation.declaration ||
30
+ canonicalJson(form.fields) !==
31
+ canonicalJson(
32
+ operation.fields.map(
33
+ ({ value: _value, source: _source, ...field }) => field,
34
+ ),
35
+ )
36
+ )
37
+ throw new Error('Hub form does not match the registered target');
38
+ } else if (operation.kind === 'ixo.bank.send.v1') {
39
+ if (
40
+ capabilities.network !== operation.network ||
41
+ !capabilities.transfer ||
42
+ BigInt(operation.amountBaseUnits) >
43
+ BigInt(capabilities.transfer.maxAmountUixo) ||
44
+ BigInt(operation.feeBaseUnits) > BigInt(capabilities.transfer.maxFeeUixo)
45
+ )
46
+ throw new Error("Hub transfer is outside this oracle's capability");
47
+ } else if (operation.kind === 'resource.read.v1') {
48
+ if (
49
+ operation.ownerDid !== ownerDid ||
50
+ !capabilities.resourceOrigin ||
51
+ new URL(operation.url).origin !== capabilities.resourceOrigin
52
+ )
53
+ throw new Error("Hub resource is outside this owner's capability");
54
+ }
55
+ }
56
+
57
+ /** The host verifies the channel binding before invoking this transport. */
58
+ export async function prepareHubAction(
59
+ client: Pick<HubActionClient, 'capabilities' | 'request'>,
60
+ store: {
61
+ reserve(
62
+ origin: HubActionOrigin,
63
+ operation: ActionOperationV1,
64
+ proposedExpiry: number,
65
+ ): Promise<number>;
66
+ record(
67
+ origin: HubActionOrigin,
68
+ reference: ActionReferenceV1,
69
+ ): Promise<ActionReferenceV1>;
70
+ byRequestKey?(requestKey: string): Promise<ActionReferenceV1 | null>;
71
+ },
72
+ input: PreparedHubActionRequest,
73
+ nowMs = Date.now(),
74
+ ): Promise<ActionReferenceV1> {
75
+ const capabilities = await client.capabilities();
76
+ assertOperationEnabled(capabilities, input.operation, input.ownerDid);
77
+ const expiresAt = await store.reserve(
78
+ input,
79
+ input.operation,
80
+ Math.floor(nowMs / 1000) + 15 * 60,
81
+ );
82
+ // A retry of a key whose reservation lapsed: the Hub refuses a past
83
+ // expiry before its idempotency lookup, so a POST could never succeed.
84
+ // The recorded reference (the first POST's) is the answer if there is
85
+ // one; otherwise the request is over.
86
+ if (expiresAt <= Math.floor(nowMs / 1000)) {
87
+ const recorded = await store.byRequestKey?.(input.requestKey);
88
+ if (recorded) return recorded;
89
+ throw new HubActionRefused(
90
+ 'expired',
91
+ 'This review request expired before it reached Auth Hub; ask the user to confirm again.',
92
+ );
93
+ }
94
+ const reference = await client.request({
95
+ bindingId: input.bindingId,
96
+ bindingRevision: input.bindingRevision,
97
+ requestKey: input.requestKey,
98
+ expiresAt,
99
+ operation: input.operation,
100
+ });
101
+ if (
102
+ reference.accountId !== capabilities.accountId ||
103
+ reference.bindingId !== input.bindingId ||
104
+ reference.bindingRevision !== input.bindingRevision
105
+ )
106
+ throw new Error(
107
+ 'Hub action reference does not match the authenticated account',
108
+ );
109
+ return store.record(input, reference);
110
+ }