@ixo/oracle-runtime-workers 0.13.0 → 0.15.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 +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
} from '@ixo/editor/core';
|
|
22
22
|
import { canToType, getActionByCan, typeToCan } from '@ixo/editor/core';
|
|
23
23
|
import { FlowError } from './errors';
|
|
24
|
+
import { rewriteRefTokens, wholeRef } from './ref-syntax';
|
|
24
25
|
import type { Condition, FlowSpecInput, FlowStep } from './types';
|
|
25
26
|
|
|
26
27
|
/** The compiler's block-id convention (verified: `generateBlockId`). */
|
|
@@ -49,8 +50,6 @@ const EVALUATOR_TO_OP: Record<string, Condition['is']> = Object.fromEntries(
|
|
|
49
50
|
]),
|
|
50
51
|
);
|
|
51
52
|
|
|
52
|
-
const REF_PATTERN = /^\{\{\s*(.+?)\s*\}\}$/;
|
|
53
|
-
|
|
54
53
|
/**
|
|
55
54
|
* Data refs cross an id boundary. The agent works in STEP ids
|
|
56
55
|
* ("fill-form.output.answers.did"), but the flow engine resolves a `$ref` by
|
|
@@ -84,17 +83,14 @@ function blockRefToStepRef(ref: string): string {
|
|
|
84
83
|
* are filled downstream against the same block-id-keyed runtime state; they must
|
|
85
84
|
* therefore carry the block id, or every "{{...}}" resolves to an empty string.
|
|
86
85
|
* Non-output tokens (handlebars helpers like `{{#if}}`, `trigger.payload.*`)
|
|
87
|
-
* contain no ".output." and pass through untouched.
|
|
86
|
+
* contain no ".output." and pass through untouched. The scan is linear in the
|
|
87
|
+
* string length (ref-syntax.ts), because it runs on document content.
|
|
88
88
|
*/
|
|
89
89
|
function rewriteEmbeddedRefs(
|
|
90
90
|
text: string,
|
|
91
91
|
mapRef: (ref: string) => string,
|
|
92
92
|
): string {
|
|
93
|
-
return text
|
|
94
|
-
/(\{\{\s*)([^{}]+?)(\s*\}\})/g,
|
|
95
|
-
(_full, open: string, ref: string, close: string) =>
|
|
96
|
-
`${open}${mapRef(ref)}${close}`,
|
|
97
|
-
);
|
|
93
|
+
return rewriteRefTokens(text, mapRef);
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
/** Resolve a friendly action name (registry `type`) to its `can` ability string. */
|
|
@@ -130,8 +126,8 @@ function isRuntimeRef(value: unknown): value is { $ref: string } {
|
|
|
130
126
|
*/
|
|
131
127
|
function friendlyValueToNb(value: unknown): unknown {
|
|
132
128
|
if (typeof value === 'string') {
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
129
|
+
const ref = wholeRef(value);
|
|
130
|
+
if (ref) return { $ref: stepRefToBlockRef(ref) };
|
|
135
131
|
return value.includes('{{')
|
|
136
132
|
? rewriteEmbeddedRefs(value, stepRefToBlockRef)
|
|
137
133
|
: value;
|
|
@@ -204,6 +200,7 @@ export function buildConditionsProp(conditions: Condition[]): string {
|
|
|
204
200
|
name: `Condition from ${c.fromStep}`,
|
|
205
201
|
sourceBlockId: stepIdToBlockId(c.fromStep),
|
|
206
202
|
sourceBlockType: 'action',
|
|
203
|
+
source: c.source,
|
|
207
204
|
rule: {
|
|
208
205
|
type: 'property_value',
|
|
209
206
|
property: c.field,
|
|
@@ -243,6 +240,12 @@ export function parseConditionsProp(
|
|
|
243
240
|
const friendlyOp = EVALUATOR_TO_OP[String(rule.operator)];
|
|
244
241
|
if (!friendlyOp) continue;
|
|
245
242
|
out.push({
|
|
243
|
+
// An untagged stored condition is evaluated against runtime output, so
|
|
244
|
+
// it reads as one and round-trips through update_step unchanged in meaning.
|
|
245
|
+
source:
|
|
246
|
+
entry.source === 'configured_input'
|
|
247
|
+
? 'configured_input'
|
|
248
|
+
: 'runtime_output',
|
|
246
249
|
fromStep: resolveStep(sourceBlockId),
|
|
247
250
|
field: String(rule.property),
|
|
248
251
|
is: friendlyOp,
|
|
@@ -9,6 +9,22 @@
|
|
|
9
9
|
* same shape augmented with a read-only `status`.
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
+
import {
|
|
13
|
+
MAX_CONDITIONS_PER_STEP,
|
|
14
|
+
MAX_DESCRIPTION_CHARS,
|
|
15
|
+
MAX_DID_CHARS,
|
|
16
|
+
MAX_FLOW_SPEC_BYTES,
|
|
17
|
+
MAX_FLOW_STEPS,
|
|
18
|
+
MAX_NAME_CHARS,
|
|
19
|
+
MAX_SKILLS_PER_STEP,
|
|
20
|
+
MAX_STEP_ID_CHARS,
|
|
21
|
+
MAX_STEP_INPUTS,
|
|
22
|
+
MAX_STEP_INPUTS_BYTES,
|
|
23
|
+
MAX_STEP_SPEC_BYTES,
|
|
24
|
+
jsonByteLength,
|
|
25
|
+
secretLiteralMessage,
|
|
26
|
+
secretLiteralPaths,
|
|
27
|
+
} from './input-policy';
|
|
12
28
|
|
|
13
29
|
/**
|
|
14
30
|
* Condition operators, aligned 1:1 with the FE condition evaluator's
|
|
@@ -26,11 +42,69 @@ export const CONDITION_OPERATORS = [
|
|
|
26
42
|
'isNotEmpty',
|
|
27
43
|
] as const;
|
|
28
44
|
|
|
45
|
+
const nameSchema = z
|
|
46
|
+
.string()
|
|
47
|
+
.max(MAX_NAME_CHARS, `At most ${MAX_NAME_CHARS} characters.`);
|
|
48
|
+
const descriptionSchema = z
|
|
49
|
+
.string()
|
|
50
|
+
.max(MAX_DESCRIPTION_CHARS, `At most ${MAX_DESCRIPTION_CHARS} characters.`);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A step id. It is embedded in `{{<id>.output.<field>}}` references and in
|
|
54
|
+
* block ids, so it may not contain whitespace or braces.
|
|
55
|
+
*/
|
|
56
|
+
export const stepIdSchema = z
|
|
57
|
+
.string()
|
|
58
|
+
.min(1)
|
|
59
|
+
.max(
|
|
60
|
+
MAX_STEP_ID_CHARS,
|
|
61
|
+
`A step id is at most ${MAX_STEP_ID_CHARS} characters.`,
|
|
62
|
+
)
|
|
63
|
+
.regex(/^[^\s{}]+$/, 'A step id may not contain spaces or braces.');
|
|
64
|
+
|
|
65
|
+
/** A DID (`did:<method>:<id>`). */
|
|
66
|
+
export const didSchema = z
|
|
67
|
+
.string()
|
|
68
|
+
.max(MAX_DID_CHARS, `A DID is at most ${MAX_DID_CHARS} characters.`)
|
|
69
|
+
.regex(/^did:[a-z0-9]+:\S+$/, 'Must be a DID, e.g. "did:ixo:ixo1…".');
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A step's inputs: at most {@link MAX_STEP_INPUTS} names and
|
|
73
|
+
* {@link MAX_STEP_INPUTS_BYTES} UTF-8 bytes of JSON, and no secret (PIN,
|
|
74
|
+
* mnemonic, password, token, API key) other than a step-output reference.
|
|
75
|
+
* Whether that step exists is checked where the flow is known.
|
|
76
|
+
*/
|
|
77
|
+
export const stepInputsSchema = z
|
|
78
|
+
.record(z.string().max(MAX_NAME_CHARS), z.unknown())
|
|
79
|
+
.superRefine((inputs, issue) => {
|
|
80
|
+
if (Object.keys(inputs).length > MAX_STEP_INPUTS)
|
|
81
|
+
issue.addIssue({
|
|
82
|
+
code: 'custom',
|
|
83
|
+
message: `A step may have at most ${MAX_STEP_INPUTS} inputs.`,
|
|
84
|
+
});
|
|
85
|
+
if (jsonByteLength(inputs) > MAX_STEP_INPUTS_BYTES)
|
|
86
|
+
issue.addIssue({
|
|
87
|
+
code: 'custom',
|
|
88
|
+
message: `A step's inputs may be at most ${MAX_STEP_INPUTS_BYTES} bytes of JSON.`,
|
|
89
|
+
});
|
|
90
|
+
const secrets = secretLiteralPaths(inputs);
|
|
91
|
+
if (secrets.length > 0)
|
|
92
|
+
issue.addIssue({
|
|
93
|
+
code: 'custom',
|
|
94
|
+
message: secretLiteralMessage(secrets),
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
29
98
|
export const conditionSchema = z.object({
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
.describe(
|
|
33
|
-
|
|
99
|
+
source: z
|
|
100
|
+
.enum(['configured_input', 'runtime_output'])
|
|
101
|
+
.describe(
|
|
102
|
+
'Read authored inputs or successful runtime output. Required for newly authored conditions.',
|
|
103
|
+
),
|
|
104
|
+
fromStep: stepIdSchema.describe(
|
|
105
|
+
'Id of the upstream step whose value is checked.',
|
|
106
|
+
),
|
|
107
|
+
field: nameSchema.describe('Field on the upstream step to inspect.'),
|
|
34
108
|
is: z.enum(CONDITION_OPERATORS).describe('Comparison operator.'),
|
|
35
109
|
value: z
|
|
36
110
|
.unknown()
|
|
@@ -38,28 +112,36 @@ export const conditionSchema = z.object({
|
|
|
38
112
|
.describe('Value to compare against (omit for isEmpty/isNotEmpty).'),
|
|
39
113
|
});
|
|
40
114
|
|
|
115
|
+
export const semanticGateSchema = z
|
|
116
|
+
.object({
|
|
117
|
+
version: z.literal(1),
|
|
118
|
+
decision: z.literal('flow.gate.semantic'),
|
|
119
|
+
criterion: z.string().min(1).max(4000),
|
|
120
|
+
rubric: z.string().min(1).max(4000),
|
|
121
|
+
inputFields: z.array(z.string().min(1).max(512)).min(1).max(50),
|
|
122
|
+
})
|
|
123
|
+
.strict();
|
|
124
|
+
|
|
41
125
|
export const hookSchema = z.object({
|
|
42
126
|
type: z.enum(['sendEmail', 'addLinkedEntity', 'sendMatrixDM']),
|
|
43
|
-
config: z.record(z.string(), z.unknown()),
|
|
127
|
+
config: z.record(z.string().max(MAX_NAME_CHARS), z.unknown()),
|
|
44
128
|
});
|
|
45
129
|
|
|
46
130
|
export const dueSchema = z.object({
|
|
47
|
-
at:
|
|
48
|
-
within:
|
|
49
|
-
.string()
|
|
131
|
+
at: nameSchema.optional().describe('Absolute due date (ISO 8601).'),
|
|
132
|
+
within: nameSchema
|
|
50
133
|
.optional()
|
|
51
134
|
.describe(
|
|
52
135
|
'Duration from when the step becomes active (ISO 8601 duration).',
|
|
53
136
|
),
|
|
54
|
-
afterCommitment:
|
|
55
|
-
.string()
|
|
137
|
+
afterCommitment: nameSchema
|
|
56
138
|
.optional()
|
|
57
139
|
.describe('Duration from commitment (ISO 8601 duration).'),
|
|
58
140
|
});
|
|
59
141
|
|
|
60
142
|
export const onEventSchema = z.object({
|
|
61
|
-
fromStep:
|
|
62
|
-
event:
|
|
143
|
+
fromStep: stepIdSchema.describe('Upstream step that emits the event.'),
|
|
144
|
+
event: nameSchema.describe('Event name the upstream step emits.'),
|
|
63
145
|
});
|
|
64
146
|
|
|
65
147
|
/**
|
|
@@ -68,18 +150,15 @@ export const onEventSchema = z.object({
|
|
|
68
150
|
* is only attached on read (see FlowStepRead).
|
|
69
151
|
*/
|
|
70
152
|
export const flowStepSchema = z.object({
|
|
71
|
-
id:
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
action: z
|
|
76
|
-
.string()
|
|
153
|
+
id: stepIdSchema.describe(
|
|
154
|
+
'Stable, human-readable step id, e.g. "load-batches".',
|
|
155
|
+
),
|
|
156
|
+
action: nameSchema
|
|
77
157
|
.min(1)
|
|
78
158
|
.describe('Action name from list_actions (e.g. "qi/email.send").'),
|
|
79
|
-
title:
|
|
80
|
-
description:
|
|
81
|
-
phase:
|
|
82
|
-
.string()
|
|
159
|
+
title: nameSchema.optional(),
|
|
160
|
+
description: descriptionSchema.optional(),
|
|
161
|
+
phase: nameSchema
|
|
83
162
|
.min(1)
|
|
84
163
|
.optional()
|
|
85
164
|
.describe(
|
|
@@ -92,11 +171,11 @@ export const flowStepSchema = z.object({
|
|
|
92
171
|
'Whether only a person may run this step or a suitably skilled, authorised Flow Agent may run it.',
|
|
93
172
|
),
|
|
94
173
|
|
|
95
|
-
inputs:
|
|
96
|
-
.record(z.string(), z.unknown())
|
|
174
|
+
inputs: stepInputsSchema
|
|
97
175
|
.optional()
|
|
98
176
|
.describe(
|
|
99
|
-
'Inputs for the action. A value may be a reference to an upstream output, written as "{{step-id.output.field}}".'
|
|
177
|
+
'Inputs for the action. A value may be a reference to an upstream output, written as "{{step-id.output.field}}". ' +
|
|
178
|
+
'Never put a PIN, mnemonic, password, token or API key here: the portal collects those when the user runs the step.',
|
|
100
179
|
),
|
|
101
180
|
form: z
|
|
102
181
|
.record(z.string(), z.unknown())
|
|
@@ -106,7 +185,8 @@ export const flowStepSchema = z.object({
|
|
|
106
185
|
),
|
|
107
186
|
|
|
108
187
|
after: z
|
|
109
|
-
.array(
|
|
188
|
+
.array(stepIdSchema)
|
|
189
|
+
.max(MAX_FLOW_STEPS)
|
|
110
190
|
.optional()
|
|
111
191
|
.describe(
|
|
112
192
|
'Order this step after the named steps. Pair with an input reference for a real data dependency. This is ordering only, not an auto-trigger.',
|
|
@@ -114,12 +194,21 @@ export const flowStepSchema = z.object({
|
|
|
114
194
|
runWhen: conditionSchema
|
|
115
195
|
.optional()
|
|
116
196
|
.describe(
|
|
117
|
-
'Gate this step on an upstream
|
|
197
|
+
'Gate this step on an upstream configured input or runtime output.',
|
|
118
198
|
),
|
|
119
199
|
conditions: z
|
|
120
200
|
.array(conditionSchema)
|
|
201
|
+
.max(
|
|
202
|
+
MAX_CONDITIONS_PER_STEP,
|
|
203
|
+
`A step may have at most ${MAX_CONDITIONS_PER_STEP} conditions.`,
|
|
204
|
+
)
|
|
121
205
|
.optional()
|
|
122
206
|
.describe('Multiple activation gates (all must pass).'),
|
|
207
|
+
semanticGate: semanticGateSchema
|
|
208
|
+
.optional()
|
|
209
|
+
.describe(
|
|
210
|
+
'Additional versioned semantic gate; cannot override deterministic conditions or authority.',
|
|
211
|
+
),
|
|
123
212
|
onEvent: onEventSchema
|
|
124
213
|
.optional()
|
|
125
214
|
.describe(
|
|
@@ -132,12 +221,13 @@ export const flowStepSchema = z.object({
|
|
|
132
221
|
.describe('When the step runs. Default "manual".'),
|
|
133
222
|
due: dueSchema.optional(),
|
|
134
223
|
|
|
135
|
-
assignTo:
|
|
136
|
-
.string()
|
|
224
|
+
assignTo: didSchema
|
|
137
225
|
.optional()
|
|
138
|
-
.describe(
|
|
139
|
-
|
|
140
|
-
|
|
226
|
+
.describe(
|
|
227
|
+
'The DID assigned to this step. Assigning AUTHORISES that DID to run the step (it is written to the ' +
|
|
228
|
+
"step's authorised actors) — only assign a DID the user named and confirmed.",
|
|
229
|
+
),
|
|
230
|
+
commitTo: nameSchema
|
|
141
231
|
.optional()
|
|
142
232
|
.describe('Commitment window (ISO 8601 duration).'),
|
|
143
233
|
|
|
@@ -146,7 +236,11 @@ export const flowStepSchema = z.object({
|
|
|
146
236
|
.optional()
|
|
147
237
|
.describe('Lifecycle hooks: event name -> hooks.'),
|
|
148
238
|
skills: z
|
|
149
|
-
.array(
|
|
239
|
+
.array(nameSchema)
|
|
240
|
+
.max(
|
|
241
|
+
MAX_SKILLS_PER_STEP,
|
|
242
|
+
`A step may require at most ${MAX_SKILLS_PER_STEP} skills.`,
|
|
243
|
+
)
|
|
150
244
|
.optional()
|
|
151
245
|
.describe(
|
|
152
246
|
'Governed skill ids required for agent-capable execution. The first is used for Flow Agent matching.',
|
|
@@ -157,19 +251,50 @@ export const flowStepSchema = z.object({
|
|
|
157
251
|
.describe('Hint the portal to force a confirmation before this step runs.'),
|
|
158
252
|
});
|
|
159
253
|
|
|
254
|
+
/**
|
|
255
|
+
* {@link flowStepSchema} plus the whole-step size cap. A separate schema so
|
|
256
|
+
* `flowStepSchema` stays a plain object that `.partial()` can derive from.
|
|
257
|
+
*/
|
|
258
|
+
export const sizedFlowStepSchema = flowStepSchema.refine(
|
|
259
|
+
(step) => jsonByteLength(step) <= MAX_STEP_SPEC_BYTES,
|
|
260
|
+
`A step may be at most ${MAX_STEP_SPEC_BYTES} bytes of JSON.`,
|
|
261
|
+
);
|
|
262
|
+
|
|
160
263
|
/**
|
|
161
264
|
* A whole flow as the agent authors it. `ref` is the opaque flow handle; it is
|
|
162
265
|
* omitted on create and assigned by the plugin.
|
|
163
266
|
*/
|
|
164
|
-
export const flowSpecSchema = z
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
267
|
+
export const flowSpecSchema = z
|
|
268
|
+
.object({
|
|
269
|
+
ref: z
|
|
270
|
+
.string()
|
|
271
|
+
.optional()
|
|
272
|
+
.describe(
|
|
273
|
+
'Opaque flow handle. Omit when creating; the plugin assigns it.',
|
|
274
|
+
),
|
|
275
|
+
title: nameSchema.min(1),
|
|
276
|
+
goal: descriptionSchema.optional(),
|
|
277
|
+
steps: z
|
|
278
|
+
.array(sizedFlowStepSchema)
|
|
279
|
+
.max(MAX_FLOW_STEPS, `A flow may have at most ${MAX_FLOW_STEPS} steps.`),
|
|
280
|
+
})
|
|
281
|
+
.refine(
|
|
282
|
+
(flow) => jsonByteLength(flow) <= MAX_FLOW_SPEC_BYTES,
|
|
283
|
+
`A flow may be at most ${MAX_FLOW_SPEC_BYTES} bytes of JSON; build a larger flow step by step with add_step.`,
|
|
284
|
+
)
|
|
285
|
+
.superRefine((flow, issue) => {
|
|
286
|
+
// A secret reference must point at a step of this flow.
|
|
287
|
+
const stepIds = new Set(flow.steps.map((step) => step.id));
|
|
288
|
+
flow.steps.forEach((step, index) => {
|
|
289
|
+
const secrets = secretLiteralPaths(step.inputs ?? {}, stepIds);
|
|
290
|
+
if (secrets.length > 0)
|
|
291
|
+
issue.addIssue({
|
|
292
|
+
code: 'custom',
|
|
293
|
+
path: ['steps', index, 'inputs'],
|
|
294
|
+
message: secretLiteralMessage(secrets),
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
173
298
|
|
|
174
299
|
export type Condition = z.infer<typeof conditionSchema>;
|
|
175
300
|
export type FlowStep = z.infer<typeof flowStepSchema>;
|
package/src/plugins/index.ts
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
* Bundled Workers-runtime plugins — ports of the Node runtime's MCP / fetch
|
|
3
3
|
* plugins. Each is gated the same way as on Node: `autoDetect` on its env
|
|
4
4
|
* key where one exists (memory / sandbox / firecrawl / composio), always-on
|
|
5
|
-
* otherwise (domain-indexer; vfs self-gates on `ctx.ucan.hasSigningKey()
|
|
5
|
+
* otherwise (domain-indexer; vfs self-gates on `ctx.ucan.hasSigningKey()`;
|
|
6
|
+
* pod-creator is on-demand and stays hidden until `load_capability`).
|
|
6
7
|
*
|
|
7
8
|
* A host opts in by spreading `BUNDLED_WORKERS_PLUGINS` into
|
|
8
9
|
* `createOracleWorker({ plugins })` — plugins whose env keys are absent are
|
|
9
10
|
* skipped by their `autoDetect`.
|
|
11
|
+
*
|
|
12
|
+
* Shipped but opt-in (not in the bundle): `IxoTransactionPlugin`, which asks
|
|
13
|
+
* the user's Portal wallet to sign chain transactions; an oracle adds
|
|
14
|
+
* `new IxoTransactionPlugin()` itself.
|
|
10
15
|
*/
|
|
11
16
|
import type { OraclePlugin } from '../plugin-api/oracle-plugin';
|
|
12
17
|
import { ComposioPlugin } from './composio';
|
|
@@ -22,6 +27,7 @@ import { PortalPlugin } from './portal';
|
|
|
22
27
|
import { AGUIPlugin } from './agui';
|
|
23
28
|
import { AttachmentsPlugin } from './attachments';
|
|
24
29
|
import { MatrixGroupChatsPlugin } from './matrix-group-chats';
|
|
30
|
+
import { PodCreatorPlugin } from './pod-creator';
|
|
25
31
|
|
|
26
32
|
export * from './composio';
|
|
27
33
|
export * from './editor';
|
|
@@ -35,6 +41,8 @@ export * from './vfs';
|
|
|
35
41
|
export * from './user-preferences';
|
|
36
42
|
export * from './portal';
|
|
37
43
|
export * from './agui';
|
|
44
|
+
export * from './ixo-transaction';
|
|
45
|
+
export * from './pod-creator';
|
|
38
46
|
|
|
39
47
|
export const memoryPlugin = new MemoryPlugin();
|
|
40
48
|
export const sandboxPlugin = new SandboxPlugin();
|
|
@@ -48,6 +56,7 @@ export const portalPlugin = new PortalPlugin();
|
|
|
48
56
|
export const aguiPlugin = new AGUIPlugin();
|
|
49
57
|
export const attachmentsPlugin = new AttachmentsPlugin();
|
|
50
58
|
export const matrixGroupChatsPlugin = new MatrixGroupChatsPlugin();
|
|
59
|
+
export const podCreatorPlugin = new PodCreatorPlugin();
|
|
51
60
|
/** TasksPlugin is already an object (defineOraclePlugin), not a class. */
|
|
52
61
|
export const tasksPlugin = TasksPlugin;
|
|
53
62
|
|
|
@@ -70,4 +79,5 @@ export const BUNDLED_WORKERS_PLUGINS = [
|
|
|
70
79
|
aguiPlugin,
|
|
71
80
|
attachmentsPlugin,
|
|
72
81
|
matrixGroupChatsPlugin,
|
|
82
|
+
podCreatorPlugin,
|
|
73
83
|
] as const satisfies readonly OraclePlugin[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chain id each network's signing requests name, as plugin config. Every
|
|
3
|
+
* plugin that asks the Portal wallet to sign (`IxoTransactionPlugin`, the POD
|
|
4
|
+
* creator) declares this same schema object, so one set of variables
|
|
5
|
+
* configures them all and the env composition does not report the shared
|
|
6
|
+
* keys as a collision.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
ChainIdSchema,
|
|
10
|
+
DEFAULT_CHAIN_IDS,
|
|
11
|
+
type Network,
|
|
12
|
+
} from '@ixo/ixo-transaction';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The defaults are what each network's RPC reports (`DEFAULT_CHAIN_IDS`);
|
|
17
|
+
* override an entry after a chain upgrade changes the id. The Portal refuses
|
|
18
|
+
* a request for another chain than its wallet's.
|
|
19
|
+
*/
|
|
20
|
+
export const chainIdConfigSchema = z.object({
|
|
21
|
+
IXO_TRANSACTION_CHAIN_ID_DEVNET: ChainIdSchema.default(
|
|
22
|
+
DEFAULT_CHAIN_IDS.devnet,
|
|
23
|
+
),
|
|
24
|
+
IXO_TRANSACTION_CHAIN_ID_TESTNET: ChainIdSchema.default(
|
|
25
|
+
DEFAULT_CHAIN_IDS.testnet,
|
|
26
|
+
),
|
|
27
|
+
IXO_TRANSACTION_CHAIN_ID_MAINNET: ChainIdSchema.default(
|
|
28
|
+
DEFAULT_CHAIN_IDS.mainnet,
|
|
29
|
+
),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type ChainIdConfig = z.output<typeof chainIdConfigSchema>;
|
|
33
|
+
|
|
34
|
+
/** The chain id per network, from the parsed config. */
|
|
35
|
+
export function chainIdsFromConfig(
|
|
36
|
+
config: ChainIdConfig,
|
|
37
|
+
): Readonly<Record<Network, string>> {
|
|
38
|
+
return {
|
|
39
|
+
devnet: config.IXO_TRANSACTION_CHAIN_ID_DEVNET,
|
|
40
|
+
testnet: config.IXO_TRANSACTION_CHAIN_ID_TESTNET,
|
|
41
|
+
mainnet: config.IXO_TRANSACTION_CHAIN_ID_MAINNET,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IXO transaction plugin: turns a conversation into a validated IXO chain
|
|
3
|
+
* transaction and asks the user's Portal wallet to sign it. On demand, and
|
|
4
|
+
* opt-in — not part of `BUNDLED_WORKERS_PLUGINS`; an oracle adds
|
|
5
|
+
* `new IxoTransactionPlugin()` to its plugins.
|
|
6
|
+
*
|
|
7
|
+
* The catalog, intent routing and validation come from the runtime-neutral
|
|
8
|
+
* `@ixo/ixo-transaction` package, whose Portal half
|
|
9
|
+
* (`@ixo/ixo-transaction/react`) answers the `sign_transaction` action.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
13
|
+
import type {
|
|
14
|
+
PluginContext,
|
|
15
|
+
PluginManifest,
|
|
16
|
+
PluginTool,
|
|
17
|
+
} from '../../plugin-api/types';
|
|
18
|
+
import { chainIdConfigSchema, chainIdsFromConfig } from './chain-ids';
|
|
19
|
+
import { createIxoTransactionTools } from './tools';
|
|
20
|
+
|
|
21
|
+
/** Default time the user has to sign in their wallet. */
|
|
22
|
+
export const DEFAULT_SIGN_TIMEOUT_MS = 120_000;
|
|
23
|
+
/** Upper bound: well inside the turn's own deadline (`TURN_TIMEOUT_MS`, 10 min by default). */
|
|
24
|
+
export const MAX_SIGN_TIMEOUT_MS = 300_000;
|
|
25
|
+
|
|
26
|
+
const configSchema = z.object({
|
|
27
|
+
/**
|
|
28
|
+
* `'true'` lets mainnet drafts through validation, each still needing a
|
|
29
|
+
* successful testnet receipt or a recorded override. Off by default: the
|
|
30
|
+
* oracle prepares testnet (and devnet) transactions only.
|
|
31
|
+
*/
|
|
32
|
+
IXO_TRANSACTION_ALLOW_MAINNET: z.enum(['true', 'false']).default('false'),
|
|
33
|
+
/** The chain id each network's requests name (shared with the POD creator). */
|
|
34
|
+
...chainIdConfigSchema.shape,
|
|
35
|
+
/** How long `sign_ixo_transaction` waits for the wallet, in ms. */
|
|
36
|
+
IXO_TRANSACTION_SIGN_TIMEOUT_MS: z.coerce
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.positive()
|
|
40
|
+
.max(MAX_SIGN_TIMEOUT_MS)
|
|
41
|
+
.default(DEFAULT_SIGN_TIMEOUT_MS),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const manifest: PluginManifest = {
|
|
45
|
+
title: 'IXO Transaction',
|
|
46
|
+
summary:
|
|
47
|
+
'Prepares and validates IXO chain transactions; the user signs them in their Portal wallet.',
|
|
48
|
+
whenToUse: [
|
|
49
|
+
'The user wants to create, update, transfer, submit, evaluate, mint or retire on the IXO chain.',
|
|
50
|
+
'The user types a slash command such as /ixo entity create or /ixo token retire.',
|
|
51
|
+
'Classify, collect fields, validate, disclose risks, then sign only after explicit consent.',
|
|
52
|
+
],
|
|
53
|
+
whenNotToUse: [
|
|
54
|
+
'Reading chain state or balances: this plugin only prepares writes.',
|
|
55
|
+
'Asking the oracle to sign itself: only the user signs, in their wallet.',
|
|
56
|
+
'Bonds, liquid staking or names: not supported yet.',
|
|
57
|
+
],
|
|
58
|
+
examples: [
|
|
59
|
+
{
|
|
60
|
+
user: 'I want to create a new domain',
|
|
61
|
+
thought:
|
|
62
|
+
'Resolve the request to a Msg first, then collect its required fields.',
|
|
63
|
+
tool: 'classify_ixo_transaction_intent',
|
|
64
|
+
args: { input: 'I want to create a new domain' },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
user: '/ixo token retire',
|
|
68
|
+
thought:
|
|
69
|
+
'List the route to see its fields and risks before asking for values.',
|
|
70
|
+
tool: 'list_ixo_transaction_routes',
|
|
71
|
+
args: { messageType: 'token' },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
user: 'Yes, I accept that retiring is permanent. Sign it.',
|
|
75
|
+
thought:
|
|
76
|
+
"Risks disclosed and accepted in the user's words: send the testnet draft to the wallet.",
|
|
77
|
+
tool: 'sign_ixo_transaction',
|
|
78
|
+
args: {
|
|
79
|
+
command: '/ixo token retire',
|
|
80
|
+
network: 'testnet',
|
|
81
|
+
value: {
|
|
82
|
+
owner: 'ixo1qwertyuiopasdfghjklzxcvbnmqwerty12345',
|
|
83
|
+
tokens: [{ id: 'CARBON-1', amount: '10' }],
|
|
84
|
+
jurisdiction: 'ZA',
|
|
85
|
+
reason: 'Offsetting 2026 travel',
|
|
86
|
+
},
|
|
87
|
+
riskConfirmation: {
|
|
88
|
+
confirmed: true,
|
|
89
|
+
acceptedRisks: [
|
|
90
|
+
'Permanently retires (burns) impact credits. Irreversible.',
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
tags: ['ixo', 'transaction', 'wallet', 'portal', 'cosmos'],
|
|
97
|
+
category: 'integration',
|
|
98
|
+
visibility: 'on-demand',
|
|
99
|
+
stability: 'experimental',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export class IxoTransactionPlugin extends OraclePlugin {
|
|
103
|
+
readonly name = 'ixo-transaction';
|
|
104
|
+
|
|
105
|
+
readonly version = '1.0.0';
|
|
106
|
+
|
|
107
|
+
readonly manifest = manifest;
|
|
108
|
+
|
|
109
|
+
override readonly configSchema = configSchema;
|
|
110
|
+
|
|
111
|
+
override getTools(ctx: PluginContext): PluginTool[] {
|
|
112
|
+
const config = configSchema.parse(ctx.config);
|
|
113
|
+
return createIxoTransactionTools({
|
|
114
|
+
allowMainnet: config.IXO_TRANSACTION_ALLOW_MAINNET === 'true',
|
|
115
|
+
signTimeoutMs: config.IXO_TRANSACTION_SIGN_TIMEOUT_MS,
|
|
116
|
+
chainIds: chainIdsFromConfig(config),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testnet receipts: the oracle's own record that a user signed a transaction
|
|
3
|
+
* on testnet, which a mainnet draft of the same message must cite.
|
|
4
|
+
*
|
|
5
|
+
* Kept in the user's blob store (`ctx.blobStore`, Durable Object storage of
|
|
6
|
+
* the user's object, scoped to the user's DID). The store hands out its own
|
|
7
|
+
* random ids and keeps an entry at most 24 hours, so a receipt is cited by
|
|
8
|
+
* that id plus the transaction hash, and expires a day after the testnet
|
|
9
|
+
* signing. The model can only cite an id it was given: it cannot make one up,
|
|
10
|
+
* read another user's, or pass off another plugin's blob (the name is
|
|
11
|
+
* checked). Any store failure refuses the mainnet draft.
|
|
12
|
+
*/
|
|
13
|
+
import type { ITrxMsg, TestnetReceipt } from '@ixo/ixo-transaction';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { operationKey } from '../../core/middlewares/tool-execution';
|
|
16
|
+
import type { RuntimeContext } from '../../plugin-api/types';
|
|
17
|
+
|
|
18
|
+
export const RECEIPT_NAME_PREFIX = 'ixo-transaction/receipt:';
|
|
19
|
+
/** The blob store's maximum lifetime. */
|
|
20
|
+
export const RECEIPT_TTL_SECONDS = 24 * 60 * 60;
|
|
21
|
+
|
|
22
|
+
const ReceiptRecordSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
transactionHash: z.string(),
|
|
25
|
+
digest: z.string(),
|
|
26
|
+
chainId: z.string(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
type ReceiptStoreContext = Pick<RuntimeContext, 'blobStore' | 'user'>;
|
|
31
|
+
|
|
32
|
+
/** SHA-256 of the message's canonical form (typeUrl + canonical value). */
|
|
33
|
+
export function messageDigest(message: ITrxMsg): Promise<string> {
|
|
34
|
+
return operationKey(message.typeUrl, message.value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Record a signed testnet transaction; returns the receipt to cite. */
|
|
38
|
+
export async function recordTestnetReceipt(
|
|
39
|
+
ctx: ReceiptStoreContext,
|
|
40
|
+
signed: { transactionHash: string; message: ITrxMsg; chainId: string },
|
|
41
|
+
): Promise<TestnetReceipt> {
|
|
42
|
+
const receiptId = await ctx.blobStore.put({
|
|
43
|
+
userDid: ctx.user.did,
|
|
44
|
+
name: `${RECEIPT_NAME_PREFIX}${signed.transactionHash}`,
|
|
45
|
+
value: JSON.stringify({
|
|
46
|
+
transactionHash: signed.transactionHash,
|
|
47
|
+
digest: await messageDigest(signed.message),
|
|
48
|
+
chainId: signed.chainId,
|
|
49
|
+
}),
|
|
50
|
+
ttlSeconds: RECEIPT_TTL_SECONDS,
|
|
51
|
+
});
|
|
52
|
+
return { transactionHash: signed.transactionHash, receiptId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Why `receipt` does not prove a testnet signing of `message` by this user
|
|
57
|
+
* on `testnetChainId`, or null when it does.
|
|
58
|
+
*/
|
|
59
|
+
export async function testnetReceiptProblem(
|
|
60
|
+
ctx: ReceiptStoreContext,
|
|
61
|
+
receipt: TestnetReceipt,
|
|
62
|
+
message: ITrxMsg,
|
|
63
|
+
testnetChainId: string,
|
|
64
|
+
): Promise<string | null> {
|
|
65
|
+
const unknown = `Testnet receipt ${receipt.receiptId} is not one this oracle recorded for this user (or it is older than 24 hours): sign the same transaction on testnet and use the testnetReceipt that returns`;
|
|
66
|
+
if (!ctx.blobStore.isValidBlobId(receipt.receiptId)) return unknown;
|
|
67
|
+
let entry: { name: string; value: string } | null;
|
|
68
|
+
try {
|
|
69
|
+
entry = await ctx.blobStore.get({
|
|
70
|
+
userDid: ctx.user.did,
|
|
71
|
+
blobId: receipt.receiptId,
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return `Testnet receipts cannot be checked right now (${error instanceof Error ? error.message : String(error)}), so the mainnet transaction was refused`;
|
|
75
|
+
}
|
|
76
|
+
if (!entry) return unknown;
|
|
77
|
+
|
|
78
|
+
let parsed: unknown;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(entry.value);
|
|
81
|
+
} catch {
|
|
82
|
+
return unknown;
|
|
83
|
+
}
|
|
84
|
+
const record = ReceiptRecordSchema.safeParse(parsed);
|
|
85
|
+
if (!record.success) return unknown;
|
|
86
|
+
if (
|
|
87
|
+
entry.name !== `${RECEIPT_NAME_PREFIX}${receipt.transactionHash}` ||
|
|
88
|
+
record.data.transactionHash !== receipt.transactionHash
|
|
89
|
+
) {
|
|
90
|
+
return `Testnet receipt ${receipt.receiptId} is not for transaction ${receipt.transactionHash}`;
|
|
91
|
+
}
|
|
92
|
+
if (record.data.chainId !== testnetChainId) {
|
|
93
|
+
return `Testnet receipt ${receipt.receiptId} was signed on ${record.data.chainId}, not on the testnet chain ${testnetChainId}`;
|
|
94
|
+
}
|
|
95
|
+
if (record.data.digest !== (await messageDigest(message))) {
|
|
96
|
+
return `Testnet receipt ${receipt.receiptId} is for a different transaction: the mainnet draft must be the same message that was signed on testnet`;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|