@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.
Files changed (205) hide show
  1. package/README.md +32 -10
  2. package/package.json +13 -8
  3. package/src/artifacts/config.ts +158 -0
  4. package/src/artifacts/crypto.ts +94 -0
  5. package/src/artifacts/routes.ts +73 -0
  6. package/src/artifacts/store.ts +238 -0
  7. package/src/artifacts/sweep.ts +200 -0
  8. package/src/artifacts/tool.ts +103 -0
  9. package/src/artifacts/viewer-page.ts +614 -0
  10. package/src/attachments/download.ts +168 -45
  11. package/src/attachments/index.ts +9 -0
  12. package/src/attachments/magic.ts +33 -0
  13. package/src/attachments/pipeline.ts +105 -49
  14. package/src/attachments/sandbox-archive.ts +9 -8
  15. package/src/attachments/view-cache.ts +142 -0
  16. package/src/attachments/view.ts +53 -12
  17. package/src/channels/auth.ts +267 -0
  18. package/src/channels/contract.ts +137 -0
  19. package/src/channels/turns.ts +270 -0
  20. package/src/core/budgeted-llm.ts +223 -0
  21. package/src/core/capability-router.ts +273 -0
  22. package/src/core/context-budget.ts +97 -10
  23. package/src/core/env.ts +111 -0
  24. package/src/core/execution-profile.ts +17 -0
  25. package/src/core/index.ts +267 -9
  26. package/src/core/llm.ts +30 -9
  27. package/src/core/main-agent-types.ts +41 -1
  28. package/src/core/main-agent.ts +367 -65
  29. package/src/core/manifest.ts +34 -0
  30. package/src/core/meta-tools.ts +128 -18
  31. package/src/core/middlewares/capability-gate.ts +130 -12
  32. package/src/core/middlewares/context-guard.ts +55 -24
  33. package/src/core/middlewares/index.ts +20 -2
  34. package/src/core/middlewares/page-context.ts +46 -2
  35. package/src/core/middlewares/return-direct-first.ts +47 -0
  36. package/src/core/middlewares/summarization.ts +152 -10
  37. package/src/core/middlewares/tool-execution.ts +323 -0
  38. package/src/core/middlewares/tool-repetition-guard.ts +202 -35
  39. package/src/core/middlewares/turn-boundary.ts +162 -0
  40. package/src/core/openrouter-pricing.ts +155 -22
  41. package/src/core/prompt-composer.ts +112 -7
  42. package/src/core/registries.ts +253 -48
  43. package/src/core/request-admission.ts +159 -0
  44. package/src/core/runtime-context.ts +107 -8
  45. package/src/core/state.ts +1 -1
  46. package/src/core/sub-agent-fallback.ts +21 -2
  47. package/src/core/subagent-as-tool.ts +78 -43
  48. package/src/core/tool-access.ts +189 -0
  49. package/src/core/tool-scheduler.ts +125 -0
  50. package/src/core/turn-budget.ts +205 -0
  51. package/src/core/turn-time-note.ts +43 -0
  52. package/src/core/user-kv.ts +446 -0
  53. package/src/core/wrap-plugin-tool.ts +20 -0
  54. package/src/delivery/plan.ts +278 -0
  55. package/src/delivery/profile.ts +161 -0
  56. package/src/delivery/prompt.ts +27 -0
  57. package/src/delivery/schema.ts +61 -0
  58. package/src/delivery/shaper.ts +389 -0
  59. package/src/delivery/types.ts +84 -0
  60. package/src/do/ambient.ts +51 -15
  61. package/src/do/contracts.ts +170 -6
  62. package/src/do/idle-eviction.ts +38 -0
  63. package/src/do/matrix-turn-ledger.ts +24 -5
  64. package/src/do/result-store-test-do.ts +137 -3
  65. package/src/do/result-store.ts +239 -58
  66. package/src/do/room-mirror.ts +29 -0
  67. package/src/do/run-buffer.ts +64 -9
  68. package/src/do/run-coordinator.ts +451 -76
  69. package/src/do/run-request.ts +2 -0
  70. package/src/do/run-store-test-do.ts +301 -1
  71. package/src/do/run-store.ts +385 -43
  72. package/src/do/secrets-adapter.ts +22 -3
  73. package/src/do/sse-stream.ts +60 -9
  74. package/src/do/transcript.ts +48 -5
  75. package/src/do/turn-body.ts +105 -5
  76. package/src/do/ucan-service.ts +204 -34
  77. package/src/do/user-oracle-do.ts +1883 -264
  78. package/src/feedback/config.ts +127 -0
  79. package/src/feedback/contract.ts +148 -0
  80. package/src/feedback/linear-sink.ts +285 -0
  81. package/src/feedback/privacy.ts +150 -0
  82. package/src/feedback/reservation.ts +298 -0
  83. package/src/feedback/submit.ts +247 -0
  84. package/src/index.ts +138 -6
  85. package/src/llm/byo-adapter.ts +27 -6
  86. package/src/llm/byo-catalog.ts +13 -0
  87. package/src/llm/byo-model-fallback.ts +248 -0
  88. package/src/llm/byo-service.ts +45 -8
  89. package/src/llm/provider-error.ts +139 -2
  90. package/src/matrix/gateway-do.ts +504 -108
  91. package/src/matrix/group-chat.ts +66 -23
  92. package/src/matrix/inbox-store.ts +16 -1
  93. package/src/matrix/ingest.ts +79 -14
  94. package/src/matrix/media-deadline.ts +130 -0
  95. package/src/matrix/replay-format.ts +5 -3
  96. package/src/matrix/reply-parts.ts +92 -0
  97. package/src/matrix/room-alias.ts +96 -0
  98. package/src/matrix/user-homeserver.ts +166 -7
  99. package/src/owner-store/ixo-vfs-store.ts +170 -43
  100. package/src/owner-store/migrating-store.ts +31 -15
  101. package/src/owner-store/types.ts +18 -3
  102. package/src/plugin-api/index.ts +43 -2
  103. package/src/plugin-api/oracle-plugin.ts +35 -0
  104. package/src/plugin-api/request-admission.ts +41 -0
  105. package/src/plugin-api/tool-helper.ts +25 -1
  106. package/src/plugin-api/tool-plane.ts +92 -0
  107. package/src/plugin-api/types.ts +194 -6
  108. package/src/plugins/agui/agui.plugin.ts +31 -16
  109. package/src/plugins/composio/composio-tools.ts +57 -6
  110. package/src/plugins/composio/composio-ucan.ts +3 -2
  111. package/src/plugins/composio/composio.plugin.ts +2 -0
  112. package/src/plugins/delegated-capabilities.ts +25 -0
  113. package/src/plugins/editor/content-session.ts +173 -83
  114. package/src/plugins/editor/content-tools.ts +76 -17
  115. package/src/plugins/editor/editor-mx.ts +28 -9
  116. package/src/plugins/editor/editor.plugin.ts +3 -1
  117. package/src/plugins/editor/failures.ts +28 -7
  118. package/src/plugins/editor/prompts.ts +2 -1
  119. package/src/plugins/editor/provider.ts +553 -40
  120. package/src/plugins/editor/standalone-editor-tool.ts +61 -18
  121. package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
  122. package/src/plugins/flows/action-metadata.ts +111 -11
  123. package/src/plugins/flows/actions.ts +18 -0
  124. package/src/plugins/flows/decision-context.ts +31 -0
  125. package/src/plugins/flows/edit.ts +223 -52
  126. package/src/plugins/flows/errors.ts +22 -1
  127. package/src/plugins/flows/flow-doc.ts +92 -9
  128. package/src/plugins/flows/flows.plugin.ts +11 -5
  129. package/src/plugins/flows/forms.ts +34 -0
  130. package/src/plugins/flows/index.ts +2 -0
  131. package/src/plugins/flows/input-policy.ts +140 -0
  132. package/src/plugins/flows/prompts.ts +14 -11
  133. package/src/plugins/flows/read.ts +49 -9
  134. package/src/plugins/flows/ref-syntax.ts +129 -0
  135. package/src/plugins/flows/test-homeserver.ts +245 -0
  136. package/src/plugins/flows/tools/authoring.ts +216 -112
  137. package/src/plugins/flows/tools/discovery.ts +4 -1
  138. package/src/plugins/flows/tools/forms.ts +21 -2
  139. package/src/plugins/flows/tools/settings.ts +34 -13
  140. package/src/plugins/flows/translator.ts +13 -10
  141. package/src/plugins/flows/types.ts +167 -42
  142. package/src/plugins/index.ts +11 -1
  143. package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
  144. package/src/plugins/ixo-transaction/index.ts +2 -0
  145. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
  146. package/src/plugins/ixo-transaction/receipts.ts +99 -0
  147. package/src/plugins/ixo-transaction/tools.ts +329 -0
  148. package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
  149. package/src/plugins/mcp-tool-adapter.ts +23 -2
  150. package/src/plugins/memory/memory-tools.ts +7 -2
  151. package/src/plugins/memory/memory-ucan.ts +3 -2
  152. package/src/plugins/memory/memory.plugin.ts +2 -0
  153. package/src/plugins/pod-creator/blueprint-store.ts +163 -0
  154. package/src/plugins/pod-creator/blueprint-types.ts +61 -0
  155. package/src/plugins/pod-creator/bounded-map.ts +87 -0
  156. package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
  157. package/src/plugins/pod-creator/chain-gateway.ts +110 -0
  158. package/src/plugins/pod-creator/config.ts +85 -0
  159. package/src/plugins/pod-creator/create-session-store.ts +180 -0
  160. package/src/plugins/pod-creator/create-tools.ts +538 -0
  161. package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
  162. package/src/plugins/pod-creator/index.ts +65 -0
  163. package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
  164. package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
  165. package/src/plugins/pod-creator/stage.ts +201 -0
  166. package/src/plugins/pod-creator/sub-agents.ts +189 -0
  167. package/src/plugins/portal/action-log.ts +21 -1
  168. package/src/plugins/portal/declared-tools.ts +90 -0
  169. package/src/plugins/portal/portal.plugin.ts +42 -16
  170. package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
  171. package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
  172. package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
  173. package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
  174. package/src/plugins/tasks/manifest.ts +1 -1
  175. package/src/plugins/tasks/middleware.ts +20 -7
  176. package/src/plugins/tasks/tasks-tools.ts +30 -3
  177. package/src/plugins/tasks/tasks.plugin.ts +64 -11
  178. package/src/plugins/user-preferences/schema.ts +12 -1
  179. package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
  180. package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
  181. package/src/plugins/vfs/vfs-client.ts +250 -71
  182. package/src/plugins/vfs/vfs-content.ts +30 -11
  183. package/src/plugins/vfs/vfs-errors.ts +22 -0
  184. package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
  185. package/src/plugins/vfs/vfs-tools.ts +60 -20
  186. package/src/realtime/frontend-call-registry.ts +228 -81
  187. package/src/realtime/realtime-endpoint.ts +215 -44
  188. package/src/realtime/session-socket-hub.ts +47 -2
  189. package/src/secrets/secrets-service.ts +9 -3
  190. package/src/shell/app.ts +529 -82
  191. package/src/shell/auth.ts +131 -37
  192. package/src/shell/turn-body-cap.ts +11 -13
  193. package/src/sqlite/blob-compactor.ts +61 -28
  194. package/src/sqlite/database.ts +156 -90
  195. package/src/sqlite/do-vfs.ts +416 -129
  196. package/src/sqlite/page-tier.ts +133 -40
  197. package/src/sqlite/serialization.ts +40 -0
  198. package/src/sqlite/sessions-store.ts +10 -2
  199. package/src/sqlite/sqlite-saver.ts +139 -19
  200. package/src/sqlite/user-kv-store.ts +317 -0
  201. package/src/tasks/schedule.ts +201 -12
  202. package/src/tasks/scheduler.ts +586 -98
  203. package/src/tasks/spec.ts +67 -3
  204. package/src/tasks/store.ts +292 -27
  205. package/src/tasks/topic-deliverables.ts +70 -0
@@ -0,0 +1,110 @@
1
+ import type { BatchMessageRoute, ITrxMsg, Network } from '@ixo/ixo-transaction';
2
+ import type { RuntimeContext } from '../../plugin-api/types';
3
+ import type { ServicePodBlueprint } from './blueprint-types';
4
+
5
+ /**
6
+ * The POD creation batch the user's wallet will sign as ONE transaction:
7
+ * proto-JSON messages the Portal can show and re-validate against the
8
+ * `@ixo/ixo-transaction` catalog — never encoded bytes.
9
+ */
10
+ export interface PreparedPodBatch {
11
+ /**
12
+ * The messages, in execution order: `MsgCreateEntity` first, then the
13
+ * claim collection(s), entity accounts and grants (`POD_BATCH_TYPE_URLS`).
14
+ * Each is `{ typeUrl, value }` in proto-JSON (camelCase fields, integer
15
+ * amounts as strings, bytes as base64) with exactly its catalog fields.
16
+ */
17
+ messages: ITrxMsg[];
18
+ /** Human-readable summary of what the batch creates (at most 1000 characters). */
19
+ summary: string;
20
+ /** Estimated cost, when the gateway can compute it. */
21
+ estimatedCost?: string;
22
+ }
23
+
24
+ /**
25
+ * The message types a POD creation batch may carry. The chain runs the
26
+ * messages of one transaction in order, all or nothing, so a later message
27
+ * can name what an earlier one creates — the entity DID, its admin account,
28
+ * the collection id — as long as the gateway predicts those values right
29
+ * (otherwise the whole transaction fails and nothing is created).
30
+ */
31
+ export const POD_BATCH_TYPE_URLS = [
32
+ '/ixo.entity.v1beta1.MsgCreateEntity',
33
+ // A collection's payment accounts must be accounts of the entity.
34
+ '/ixo.entity.v1beta1.MsgCreateEntityAccount',
35
+ '/ixo.claims.v1beta1.MsgCreateCollection',
36
+ // Submit / evaluate authorizations from the collection admin account.
37
+ '/ixo.claims.v1beta1.MsgCreateClaimAuthorization',
38
+ // Generic or bank-send grants from an entity account.
39
+ '/ixo.entity.v1beta1.MsgGrantEntityAccountAuthz',
40
+ ] as const;
41
+
42
+ const CREATE_ENTITY = POD_BATCH_TYPE_URLS[0];
43
+
44
+ /**
45
+ * Why a validated batch is not a POD creation batch, or null when it is:
46
+ * exactly one `MsgCreateEntity`, first, and only `POD_BATCH_TYPE_URLS`.
47
+ */
48
+ export function podBatchProblem(
49
+ routes: readonly BatchMessageRoute[],
50
+ ): string | null {
51
+ const allowed: readonly string[] = POD_BATCH_TYPE_URLS;
52
+ const foreign = routes.filter((route) => !allowed.includes(route.typeUrl));
53
+ if (foreign.length > 0) {
54
+ return `A POD creation batch carries only ${POD_BATCH_TYPE_URLS.join(', ')}; it had ${foreign
55
+ .map((route) => route.typeUrl)
56
+ .join(', ')}`;
57
+ }
58
+ const entities = routes.filter((route) => route.typeUrl === CREATE_ENTITY);
59
+ if (entities.length !== 1 || routes[0]?.typeUrl !== CREATE_ENTITY) {
60
+ return `A POD creation batch creates exactly one entity, with ${CREATE_ENTITY} as its first message`;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /** The created POD, resolved from the broadcast transaction. */
66
+ export interface CreatedPod {
67
+ podDid: string;
68
+ summary: string;
69
+ }
70
+
71
+ /**
72
+ * The chain-facing seam for the create path. The planned concrete implementation
73
+ * calls the IXO MCP server (hosted on Cloudflare) to compose the POD-creation
74
+ * batch (`MsgCreateEntity` + the claim collection + authz grants) and to read a
75
+ * broadcast transaction back. The oracle never signs creation — the user's
76
+ * wallet does — so the gateway only ever returns proto-JSON messages, which
77
+ * the plugin validates against the `@ixo/ixo-transaction` catalog before
78
+ * storing them, and resolves results.
79
+ *
80
+ * Both methods receive the request `RuntimeContext` because reaching the IXO MCP
81
+ * server follows the runtime's remote-MCP pattern (see the sandbox plugin):
82
+ * resolve the server DID via did:web and mint a per-user `ixo:*` UCAN invocation
83
+ * through `ctx.ucan` for the `Authorization` header, with the MCP URL read from
84
+ * `ctx.config`. The seam is injected so the create path stays unit-testable
85
+ * without a live server or chain.
86
+ */
87
+ export interface ChainGateway {
88
+ preparePodBatch(
89
+ input: { blueprint: ServicePodBlueprint; network: Network },
90
+ ctx: RuntimeContext,
91
+ ): Promise<PreparedPodBatch>;
92
+ confirmPodCreation(
93
+ input: { txHash: string; network: Network },
94
+ ctx: RuntimeContext,
95
+ ): Promise<CreatedPod>;
96
+ }
97
+
98
+ const notConfigured = (): never => {
99
+ throw new Error(
100
+ 'ChainGateway not configured: the POD create path needs a chain gateway ' +
101
+ 'wired to the IXO MCP server before it can prepare or confirm ' +
102
+ 'transactions.',
103
+ );
104
+ };
105
+
106
+ /** Default gateway that errors until a real one is wired. */
107
+ export const notConfiguredChainGateway: ChainGateway = {
108
+ preparePodBatch: async () => notConfigured(),
109
+ confirmPodCreation: async () => notConfigured(),
110
+ };
@@ -0,0 +1,85 @@
1
+ import {
2
+ DEFAULT_CHAIN_IDS,
3
+ NetworkSchema,
4
+ type Network,
5
+ } from '@ixo/ixo-transaction';
6
+ import { z } from 'zod';
7
+ import type { RuntimeContext } from '../../plugin-api/types';
8
+ import {
9
+ chainIdConfigSchema,
10
+ chainIdsFromConfig,
11
+ } from '../ixo-transaction/chain-ids';
12
+
13
+ /**
14
+ * The network every pod-creator surface falls back to when the merged config
15
+ * carries no readable `NETWORK` — the safe one. On a Workers oracle the base
16
+ * env schema always fills `NETWORK` (default `mainnet`), so in practice the
17
+ * mainnet opt-in below is what keeps an unconfigured oracle off mainnet.
18
+ */
19
+ export const DEFAULT_NETWORK: Network = 'testnet';
20
+
21
+ /**
22
+ * Plugin-owned env vars. The capsules registry URL and `NETWORK` are read as
23
+ * siblings from the merged config (owned by the skills plugin / base schema),
24
+ * so they are intentionally NOT redeclared here — two plugins declaring one
25
+ * key with different schemas would collide in the config-schema registry.
26
+ * The chain ids are the ixo-transaction plugin's own schema objects, which
27
+ * both plugins declare (one set of variables for every wallet-signing
28
+ * plugin; the composition treats the shared objects as one key).
29
+ */
30
+ export const podCreatorConfigSchema = z.object({
31
+ /**
32
+ * Must be explicitly enabled before a mainnet creation batch can be
33
+ * prepared. Testnet / devnet require no opt-in. Accepts a real boolean
34
+ * (tests / programmatic config) or the string `'true'` / `'false'` (a
35
+ * Worker var).
36
+ */
37
+ POD_CREATOR_ALLOW_MAINNET: z
38
+ .union([
39
+ z.boolean(),
40
+ z.enum(['true', 'false']).transform((value) => value === 'true'),
41
+ ])
42
+ .default(false),
43
+ /** The chain id each network's signing request names. */
44
+ ...chainIdConfigSchema.shape,
45
+ });
46
+
47
+ /** Sibling env + plugin flags the tools read at request time. */
48
+ const configReadSchema = z.object({
49
+ NETWORK: NetworkSchema.optional(),
50
+ POD_CREATOR_ALLOW_MAINNET:
51
+ podCreatorConfigSchema.shape.POD_CREATOR_ALLOW_MAINNET.optional(),
52
+ ...chainIdConfigSchema.shape,
53
+ });
54
+
55
+ export interface PodCreatorConfig {
56
+ network: Network;
57
+ mainnetAllowed: boolean;
58
+ /** The chain id each network's signing request names. */
59
+ chainIds: Readonly<Record<Network, string>>;
60
+ }
61
+
62
+ /**
63
+ * Network routing + the mainnet opt-in, read from the merged config. Falls
64
+ * back to testnet with mainnet disabled when the config is unreadable — and
65
+ * says so, since a silent network fallback on the create path would mask
66
+ * misconfiguration.
67
+ */
68
+ export function readPodCreatorConfig(ctx: RuntimeContext): PodCreatorConfig {
69
+ const parsed = configReadSchema.safeParse(ctx.config);
70
+ if (!parsed.success) {
71
+ ctx.logger.warn(
72
+ '[pod-creator] config unreadable; defaulting to testnet with mainnet disabled',
73
+ );
74
+ return {
75
+ network: DEFAULT_NETWORK,
76
+ mainnetAllowed: false,
77
+ chainIds: DEFAULT_CHAIN_IDS,
78
+ };
79
+ }
80
+ return {
81
+ network: parsed.data.NETWORK ?? DEFAULT_NETWORK,
82
+ mainnetAllowed: parsed.data.POD_CREATOR_ALLOW_MAINNET ?? false,
83
+ chainIds: chainIdsFromConfig(parsed.data),
84
+ };
85
+ }
@@ -0,0 +1,180 @@
1
+ import { z } from 'zod';
2
+ import type { RuntimeContext, UserKvSurface } from '../../plugin-api/types';
3
+
4
+ /**
5
+ * Per-(user, thread) state machine for the create path's propose → approve →
6
+ * commit handoff. Exactly one batch can be pending per key: `prepared`
7
+ * supersedes anything before it, `approve` binds the user's go-ahead to that
8
+ * exact blobId, and `consume` spends the approval — so every wallet sign
9
+ * dispatch requires a fresh, explicit approval and a sign request can never be
10
+ * replayed from a stale one. Keyed by user DID as well as thread so a batch
11
+ * prepared in another thread (or by another user in a shared thread) can never
12
+ * be approved here.
13
+ *
14
+ * The session also records the request (turn) that prepared the batch, and
15
+ * approval is refused within that same request: the user has to send a new
16
+ * message after seeing the batch before it can be approved, so a single model
17
+ * turn — steered, say, by injected text in the brief or a section — cannot run
18
+ * prepare → approve → sign on its own.
19
+ */
20
+ export interface CreateSessionStore {
21
+ /** Record a freshly prepared batch; any prior approval is superseded. */
22
+ prepared(
23
+ userDid: string,
24
+ threadId: string,
25
+ blobId: string,
26
+ requestId: string,
27
+ ): Promise<void>;
28
+ /**
29
+ * Approve the pending batch from request `requestId`. `not-prepared` when
30
+ * `blobId` is not the batch prepared for this user+thread; `same-request`
31
+ * when the approval comes from the request that prepared it.
32
+ */
33
+ approve(
34
+ userDid: string,
35
+ threadId: string,
36
+ blobId: string,
37
+ requestId: string,
38
+ ): Promise<ApproveOutcome>;
39
+ /**
40
+ * Spend the approval for a sign dispatch. Returns true only when `blobId`
41
+ * is the approved pending batch; the approval is cleared when it matches, so
42
+ * a second dispatch needs a fresh approve.
43
+ */
44
+ consume(userDid: string, threadId: string, blobId: string): Promise<boolean>;
45
+ /** Drop the session (after on-chain confirmation). */
46
+ clear(userDid: string, threadId: string): Promise<void>;
47
+ }
48
+
49
+ /** Resolves the create-session store for one request (it lives in the user's database). */
50
+ export type CreateSessionStoreFor = (ctx: RuntimeContext) => CreateSessionStore;
51
+
52
+ export type ApproveOutcome = 'approved' | 'not-prepared' | 'same-request';
53
+
54
+ /** `ctx.kv` namespace holding one create session per (user, thread). */
55
+ export const CREATE_SESSION_NAMESPACE = 'pod-creator/create-sessions';
56
+
57
+ const createSessionSchema = z.object({
58
+ preparedBlobId: z.string(),
59
+ /** The request (turn) that prepared the batch. */
60
+ preparedRequestId: z.string(),
61
+ approved: z.boolean(),
62
+ });
63
+
64
+ type CreateSession = z.infer<typeof createSessionSchema>;
65
+
66
+ /** Mirrors the blob store's default TTL — the approval is useless without the blob. */
67
+ const DEFAULT_TTL_MS = 60 * 60 * 1000;
68
+ const DEFAULT_MAX_ENTRIES = 1000;
69
+
70
+ export interface KvCreateSessionStoreOptions {
71
+ maxEntries?: number;
72
+ ttlMs?: number;
73
+ }
74
+
75
+ /** A JSON pair cannot collide the way a joined string could. */
76
+ const key = (userDid: string, threadId: string): string =>
77
+ JSON.stringify([userDid, threadId]);
78
+
79
+ /**
80
+ * A stored session that no longer parses is treated as no session: the only
81
+ * consequence is that the user has to prepare the batch again, which is the
82
+ * safe direction for an approval gate.
83
+ */
84
+ function parseSession(value: unknown): CreateSession | undefined {
85
+ const parsed = createSessionSchema.safeParse(value);
86
+ return parsed.success ? parsed.data : undefined;
87
+ }
88
+
89
+ /**
90
+ * {@link CreateSessionStore} over the host's `ctx.kv` rows in the user's own
91
+ * database, so a prepared or approved batch survives the user object being
92
+ * evicted between the user's "yes" and the sign request. Each transition is
93
+ * one atomic read-modify-write.
94
+ */
95
+ export class KvCreateSessionStore implements CreateSessionStore {
96
+ private readonly writeOptions: { idleTtlMs: number; maxEntries: number };
97
+
98
+ constructor(
99
+ private readonly kv: UserKvSurface,
100
+ options: KvCreateSessionStoreOptions = {},
101
+ ) {
102
+ this.writeOptions = {
103
+ idleTtlMs: options.ttlMs ?? DEFAULT_TTL_MS,
104
+ maxEntries: options.maxEntries ?? DEFAULT_MAX_ENTRIES,
105
+ };
106
+ }
107
+
108
+ async prepared(
109
+ userDid: string,
110
+ threadId: string,
111
+ blobId: string,
112
+ requestId: string,
113
+ ): Promise<void> {
114
+ const session: CreateSession = {
115
+ preparedBlobId: blobId,
116
+ preparedRequestId: requestId,
117
+ approved: false,
118
+ };
119
+ await this.kv.set(
120
+ CREATE_SESSION_NAMESPACE,
121
+ key(userDid, threadId),
122
+ session,
123
+ this.writeOptions,
124
+ );
125
+ }
126
+
127
+ async approve(
128
+ userDid: string,
129
+ threadId: string,
130
+ blobId: string,
131
+ requestId: string,
132
+ ): Promise<ApproveOutcome> {
133
+ let outcome: ApproveOutcome = 'not-prepared';
134
+ await this.kv.update(
135
+ CREATE_SESSION_NAMESPACE,
136
+ key(userDid, threadId),
137
+ (current) => {
138
+ const session = parseSession(current);
139
+ if (!session || session.preparedBlobId !== blobId) {
140
+ outcome = 'not-prepared';
141
+ return session;
142
+ }
143
+ if (session.preparedRequestId === requestId) {
144
+ outcome = 'same-request';
145
+ return session;
146
+ }
147
+ outcome = 'approved';
148
+ return { ...session, approved: true };
149
+ },
150
+ this.writeOptions,
151
+ );
152
+ return outcome;
153
+ }
154
+
155
+ async consume(
156
+ userDid: string,
157
+ threadId: string,
158
+ blobId: string,
159
+ ): Promise<boolean> {
160
+ let spent = false;
161
+ await this.kv.update(
162
+ CREATE_SESSION_NAMESPACE,
163
+ key(userDid, threadId),
164
+ (current) => {
165
+ const session = parseSession(current);
166
+ spent =
167
+ session !== undefined &&
168
+ session.preparedBlobId === blobId &&
169
+ session.approved;
170
+ return session && spent ? { ...session, approved: false } : session;
171
+ },
172
+ this.writeOptions,
173
+ );
174
+ return spent;
175
+ }
176
+
177
+ async clear(userDid: string, threadId: string): Promise<void> {
178
+ await this.kv.delete(CREATE_SESSION_NAMESPACE, key(userDid, threadId));
179
+ }
180
+ }