@zktx.io/sui-mcp 0.5.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 (218) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/@mysten-bcs-2.0.5-Apache-2.0.txt +205 -0
  3. package/LICENSES/@mysten-sui-2.17.0-Apache-2.0.txt +201 -0
  4. package/LICENSES/@mysten-utils-0.3.3-Apache-2.0.txt +205 -0
  5. package/LICENSES/fancy-canvas-2.1.0-MIT.txt +7 -0
  6. package/LICENSES/lightweight-charts-5.2.0-NOTICE.txt +2 -0
  7. package/README.md +288 -0
  8. package/dist/adapters/adapterLifecycleValidators.js +5 -0
  9. package/dist/adapters/adapterPromptSurfaces.js +60 -0
  10. package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +178 -0
  11. package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
  12. package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
  13. package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
  14. package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
  15. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +254 -0
  16. package/dist/adapters/intentPlanFactories.js +51 -0
  17. package/dist/adapters/reviewAdapters.js +47 -0
  18. package/dist/core/action/adapterLifecycleValidation.js +12 -0
  19. package/dist/core/action/contractNameRegistry.js +151 -0
  20. package/dist/core/action/forbiddenFields.js +43 -0
  21. package/dist/core/action/humanReadableReviewEvidence.js +193 -0
  22. package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
  23. package/dist/core/action/ptbVisualizationProducer.js +71 -0
  24. package/dist/core/action/receiptPtbGraph.js +29 -0
  25. package/dist/core/action/reviewCheckResults.js +6 -0
  26. package/dist/core/action/reviewStateValidation.js +11 -0
  27. package/dist/core/action/reviewTimeSimulationEvidence.js +470 -0
  28. package/dist/core/action/schemas.js +491 -0
  29. package/dist/core/action/signableAdapterContract.js +998 -0
  30. package/dist/core/action/suiChainReceiptEvidence.js +106 -0
  31. package/dist/core/action/suiChainReceiptMapping.js +107 -0
  32. package/dist/core/action/suiChainReceiptReader.js +211 -0
  33. package/dist/core/action/suiChainReceiptVerifier.js +178 -0
  34. package/dist/core/action/swapHumanReadableReviewProjection.js +130 -0
  35. package/dist/core/action/swapQuotePolicyEvidence.js +271 -0
  36. package/dist/core/action/transactionFunding.js +244 -0
  37. package/dist/core/action/transactionObjectOwnershipEvidence.js +256 -0
  38. package/dist/core/action/transactionObjectOwnershipProducer.js +323 -0
  39. package/dist/core/action/types.js +33 -0
  40. package/dist/core/action/walletReviewContractAssembler.js +286 -0
  41. package/dist/core/activity/accountAssetTimeline.js +157 -0
  42. package/dist/core/activity/accountAssetTimelineUsdcReferences.js +120 -0
  43. package/dist/core/activity/activityStore.js +16 -0
  44. package/dist/core/activity/externalActivityCoverage.js +76 -0
  45. package/dist/core/activity/externalActivityTransactionStream.js +82 -0
  46. package/dist/core/activity/localDataService.js +296 -0
  47. package/dist/core/activity/localDataTypes.js +11 -0
  48. package/dist/core/activity/localDataValidation.js +439 -0
  49. package/dist/core/activity/schemaVersion.js +2 -0
  50. package/dist/core/activity/sqliteActivityStore.js +1068 -0
  51. package/dist/core/activity/sqliteActivityStoreRows.js +389 -0
  52. package/dist/core/activity/sqliteActivityStoreSchema.js +367 -0
  53. package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
  54. package/dist/core/activity/suiFunctionTarget.js +43 -0
  55. package/dist/core/activity/transactionActivityAccountEffects.js +186 -0
  56. package/dist/core/activity/transactionActivityAnalysis.js +295 -0
  57. package/dist/core/activity/transactionActivityClassifier.js +306 -0
  58. package/dist/core/activity/transactionActivityDetails.js +229 -0
  59. package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
  60. package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
  61. package/dist/core/activity/transactionActivityService.js +379 -0
  62. package/dist/core/activity/transactionActivityTypes.js +18 -0
  63. package/dist/core/eventlog/sink.js +35 -0
  64. package/dist/core/evidence/settlementFamilies.js +85 -0
  65. package/dist/core/evidence/userAnswerUse.js +1 -0
  66. package/dist/core/numeric/rawU64.js +63 -0
  67. package/dist/core/preferences/preferencesStore.js +26 -0
  68. package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
  69. package/dist/core/proposal/externalProposalReview.js +347 -0
  70. package/dist/core/proposal/schemas.js +208 -0
  71. package/dist/core/proposal/types.js +35 -0
  72. package/dist/core/read/amounts.js +14 -0
  73. package/dist/core/read/coinMetadata.js +60 -0
  74. package/dist/core/read/deepbookOfficialIndexerSource.js +373 -0
  75. package/dist/core/read/deepbookRawQuoteClient.js +86 -0
  76. package/dist/core/read/deepbookReadHelpers.js +321 -0
  77. package/dist/core/read/deepbookRegistry.js +157 -0
  78. package/dist/core/read/deepbookSourceOwners.js +159 -0
  79. package/dist/core/read/deepbookUsdcChartService.js +342 -0
  80. package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
  81. package/dist/core/read/readCardInputs.js +19 -0
  82. package/dist/core/read/readCardResult.js +40 -0
  83. package/dist/core/read/readResponseGuidance.js +402 -0
  84. package/dist/core/read/readService.js +1639 -0
  85. package/dist/core/read/readServiceTypes.js +88 -0
  86. package/dist/core/read/settlementParityFormatting.js +82 -0
  87. package/dist/core/read/walletReadHelpers.js +99 -0
  88. package/dist/core/review/reviewChecks.js +45 -0
  89. package/dist/core/review/reviewComputation.js +38 -0
  90. package/dist/core/review/reviewComputationResult.js +87 -0
  91. package/dist/core/session/cardSession.js +31 -0
  92. package/dist/core/session/cardSessionStore.js +204 -0
  93. package/dist/core/session/keyedRecordStore.js +18 -0
  94. package/dist/core/session/liveReviewSessionContract.js +3 -0
  95. package/dist/core/session/localSession.js +31 -0
  96. package/dist/core/session/privateReviewArtifacts.js +75 -0
  97. package/dist/core/session/reviewValidity.js +49 -0
  98. package/dist/core/session/sessionErrors.js +9 -0
  99. package/dist/core/session/sessionRecordStore.js +40 -0
  100. package/dist/core/session/sessionStore.js +655 -0
  101. package/dist/core/session/settingsSession.js +1 -0
  102. package/dist/core/session/settingsSessions.js +45 -0
  103. package/dist/core/session/sqliteCardStore.js +88 -0
  104. package/dist/core/session/sqliteSessionStore.js +213 -0
  105. package/dist/core/session/sqliteTransactionMaterialStore.js +64 -0
  106. package/dist/core/session/sqliteWalletWorkflowStore.js +821 -0
  107. package/dist/core/session/status.js +60 -0
  108. package/dist/core/session/transactionMaterialStore.js +221 -0
  109. package/dist/core/session/transactionRequest.js +72 -0
  110. package/dist/core/session/wait.js +79 -0
  111. package/dist/core/session/walletConnection.js +92 -0
  112. package/dist/core/session/walletRuntime.js +38 -0
  113. package/dist/core/session/walletWorkflow.js +608 -0
  114. package/dist/core/session/workflowState.js +80 -0
  115. package/dist/core/session/workflowView.js +150 -0
  116. package/dist/core/suiAddress.js +18 -0
  117. package/dist/core/suiEndpoint.js +72 -0
  118. package/dist/mcp/activeAccountResponse.js +24 -0
  119. package/dist/mcp/prompts.js +146 -0
  120. package/dist/mcp/registerTool.js +19 -0
  121. package/dist/mcp/resources.js +72 -0
  122. package/dist/mcp/responseGuidance.js +326 -0
  123. package/dist/mcp/result.js +17 -0
  124. package/dist/mcp/schemas.js +11 -0
  125. package/dist/mcp/server.js +32 -0
  126. package/dist/mcp/serverInfo.js +131 -0
  127. package/dist/mcp/toolErrors.js +118 -0
  128. package/dist/mcp/toolNames.js +58 -0
  129. package/dist/mcp/tools/account/index.js +44 -0
  130. package/dist/mcp/tools/action/prepareSuiActionReview.js +126 -0
  131. package/dist/mcp/tools/read/commonSchemas.js +43 -0
  132. package/dist/mcp/tools/read/deepbookReadTools.js +686 -0
  133. package/dist/mcp/tools/read/index.js +14 -0
  134. package/dist/mcp/tools/read/readToolHelpers.js +97 -0
  135. package/dist/mcp/tools/read/reviewActivityTools.js +179 -0
  136. package/dist/mcp/tools/read/serverStatusTools.js +100 -0
  137. package/dist/mcp/tools/read/transactionActivityOutput.js +371 -0
  138. package/dist/mcp/tools/read/transactionActivityTools.js +858 -0
  139. package/dist/mcp/tools/read/walletReadTools.js +734 -0
  140. package/dist/mcp/tools/session/executionResultTools.js +44 -0
  141. package/dist/mcp/tools/session/index.js +8 -0
  142. package/dist/mcp/tools/session/shared.js +36 -0
  143. package/dist/mcp/tools/session/statusTools.js +69 -0
  144. package/dist/mcp/tools/session/walletConnectionTools.js +69 -0
  145. package/dist/mcp/tools/settings/index.js +66 -0
  146. package/dist/mcp-app/account.html +3133 -0
  147. package/dist/mcp-app/account.notices.txt +973 -0
  148. package/dist/mcp-app/chart.html +3354 -0
  149. package/dist/mcp-app/chart.notices.txt +1194 -0
  150. package/dist/mcp-app/connect.html +3179 -0
  151. package/dist/mcp-app/connect.notices.txt +1012 -0
  152. package/dist/mcp-app/receipt.html +8430 -0
  153. package/dist/mcp-app/receipt.notices.txt +3549 -0
  154. package/dist/mcp-app/review.html +8430 -0
  155. package/dist/mcp-app/review.notices.txt +3549 -0
  156. package/dist/mcp-ui/contracts.js +31 -0
  157. package/dist/mcp-ui/readCards.js +28 -0
  158. package/dist/mcp-ui/tools.js +134 -0
  159. package/dist/review-app/brand-dark.svg +1 -0
  160. package/dist/review-app/brand-light.svg +1 -0
  161. package/dist/review-app/favicon.svg +1 -0
  162. package/dist/review-app/settings.css +1 -0
  163. package/dist/review-app/settings.js +1 -0
  164. package/dist/review-app/ui.css +809 -0
  165. package/dist/review-server/assets.js +48 -0
  166. package/dist/review-server/html.js +45 -0
  167. package/dist/review-server/http.js +47 -0
  168. package/dist/review-server/middleware/hostOrigin.js +48 -0
  169. package/dist/review-server/middleware/reviewToken.js +7 -0
  170. package/dist/review-server/reviewServerPolicy.js +1 -0
  171. package/dist/review-server/server.js +72 -0
  172. package/dist/review-server/settingsApi.js +158 -0
  173. package/dist/runtime/application.js +203 -0
  174. package/dist/runtime/config.js +97 -0
  175. package/dist/runtime/localSettingsService.js +198 -0
  176. package/dist/runtime/logger.js +66 -0
  177. package/dist/runtime/reviewDependencies.js +58 -0
  178. package/dist/runtime/reviewServerAcquire.js +93 -0
  179. package/dist/runtime/runtimeTermination.js +60 -0
  180. package/dist/runtime/shared/authenticatedFetch.js +127 -0
  181. package/dist/runtime/shared/control.js +93 -0
  182. package/dist/runtime/shared/dataAccess.js +66 -0
  183. package/dist/runtime/shared/mcpHttp.js +84 -0
  184. package/dist/runtime/shared/ownerLease.js +50 -0
  185. package/dist/runtime/shared/server.js +98 -0
  186. package/dist/runtime/shared/stdio.js +113 -0
  187. package/dist/runtime/smokeMainnetRead.js +391 -0
  188. package/dist/runtime/smokeMainnetReadAssertions.js +306 -0
  189. package/dist/runtime/smokeReview.js +40 -0
  190. package/dist/runtime/start.js +90 -0
  191. package/dist/runtime/suiEndpoint.js +97 -0
  192. package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
  193. package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
  194. package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
  195. package/dist/runtime/walletConnectConfig.js +3 -0
  196. package/dist/runtime/walletConnectStorage.js +99 -0
  197. package/dist/runtime/walletConnectTransport.js +128 -0
  198. package/dist/runtime/walletSdkChild.js +3 -0
  199. package/dist/runtime/walletSdkChildRuntime.js +160 -0
  200. package/dist/runtime/walletSdkIpc.js +26 -0
  201. package/dist/runtime/walletSdkProcess.js +358 -0
  202. package/docs/AGENT_BEHAVIOR.md +524 -0
  203. package/docs/AGENT_DEVELOPMENT_POLICY.md +815 -0
  204. package/docs/FRONTEND_POLICY.md +534 -0
  205. package/docs/LOCAL_DB_ARCHITECTURE.md +381 -0
  206. package/docs/MCP_SETUP.md +742 -0
  207. package/docs/MCP_TOOLS.md +1318 -0
  208. package/docs/SDK_API.md +206 -0
  209. package/docs/SIGNABLE_ADAPTER_CONTRACT.md +396 -0
  210. package/docs/TRANSACTION_ACTIVITY_LOG.md +194 -0
  211. package/docs/UTILITY_INDEX.md +275 -0
  212. package/docs/WALLET_CONNECTION.md +238 -0
  213. package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +166 -0
  214. package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +161 -0
  215. package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +259 -0
  216. package/package.json +79 -0
  217. package/protocols/deepbook-margin.md +15 -0
  218. package/protocols/deepbook-v3.md +24 -0
@@ -0,0 +1,655 @@
1
+ import { ZodError } from "zod";
2
+ import { decideReviewEvaluation, needsReviewMaterial } from "./reviewValidity.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { parseLifecycleValidatedReviewState } from "../action/reviewStateValidation.js";
6
+ import { hashEventValue, NullEventLogSink } from "../eventlog/sink.js";
7
+ import { LocalTransactionMaterialStoreError, verifyLocalTransactionMaterialArtifacts } from "./transactionMaterialStore.js";
8
+ import { verifyTransactionObjectOwnershipEvidence } from "../action/transactionObjectOwnershipEvidence.js";
9
+ import { verifySwapQuotePolicyEvidence } from "../action/swapQuotePolicyEvidence.js";
10
+ import { publicHumanReadableReviewFromEvidence } from "../action/humanReadableReviewEvidence.js";
11
+ import { publicTransactionSimulationSummaryFromEvidence, verifyReviewTimeSimulationEvidence } from "../action/reviewTimeSimulationEvidence.js";
12
+ import { verifySupportedHumanReadableReviewEvidence } from "../action/humanReadableReviewProjectionVerifier.js";
13
+ import { clonePrivateReviewArtifacts, InMemoryPrivateReviewArtifactStore } from "./privateReviewArtifacts.js";
14
+ import { InMemorySessionRecordStore } from "./sessionRecordStore.js";
15
+ import { isFinalSessionStatus } from "./status.js";
16
+ import { parseSuiAddress } from "../suiAddress.js";
17
+ import { cloneLocalSession, createLocalSessionBase, DEFAULT_SESSION_TTL_MS, isLocalSessionExpired, tokenMatchesHash } from "./localSession.js";
18
+ import { SessionStoreError } from "./sessionErrors.js";
19
+ import { SettingsSessionManager } from "./settingsSessions.js";
20
+ const PRIVATE_DERIVED_REVIEW_FIELD_BINDINGS = [
21
+ {
22
+ field: "humanReadableReview",
23
+ getPublicState: (state) => state.humanReadableReview,
24
+ getPrivateEvidence: (artifacts) => artifacts.humanReadableReview,
25
+ projectPrivateEvidence: (evidence) => publicHumanReadableReviewFromEvidence(evidence)
26
+ },
27
+ {
28
+ field: "simulation",
29
+ getPublicState: (state) => state.simulation,
30
+ getPrivateEvidence: (artifacts) => artifacts.reviewTimeSimulation,
31
+ projectPrivateEvidence: (evidence) => publicTransactionSimulationSummaryFromEvidence(evidence)
32
+ }
33
+ ];
34
+ export { SessionStoreError } from "./sessionErrors.js";
35
+ const REVIEW_STATE_RECOMPUTE_STATUSES = new Set([
36
+ "wallet_connected",
37
+ "ready_for_wallet_review",
38
+ "refresh_required",
39
+ "blocked"
40
+ ]);
41
+ const ALLOWED_TRANSITIONS = {
42
+ proposed: ["awaiting_wallet", "expired"],
43
+ awaiting_wallet: ["wallet_connected", "expired"],
44
+ wallet_connected: ["ready_for_wallet_review", "refresh_required", "blocked", "expired"],
45
+ ready_for_wallet_review: ["refresh_required", "blocked", "expired"],
46
+ refresh_required: ["ready_for_wallet_review", "blocked", "expired"],
47
+ blocked: ["ready_for_wallet_review", "refresh_required", "expired"],
48
+ expired: []
49
+ };
50
+ export class LocalSessionStore {
51
+ sessions;
52
+ privateReviewArtifacts;
53
+ ownerId;
54
+ settings;
55
+ ttlMs;
56
+ clock;
57
+ eventLog;
58
+ activityStore;
59
+ transactionMaterialStore;
60
+ logger;
61
+ validateAdapterLifecycle;
62
+ constructor(options) {
63
+ this.sessions = options.sessions;
64
+ this.clock = options.now ?? (() => new Date());
65
+ this.privateReviewArtifacts = options.artifacts;
66
+ this.ttlMs = options.ttlMs ?? DEFAULT_SESSION_TTL_MS;
67
+ this.ownerId = options.ownerId ?? randomUUID();
68
+ this.settings = new SettingsSessionManager({
69
+ ttlMs: this.ttlMs,
70
+ appendEventLog: (record) => this.appendEventLog(record),
71
+ ...(options.settingsStore ? { recordStore: options.settingsStore } : {})
72
+ });
73
+ this.eventLog = options.eventLog ?? new NullEventLogSink();
74
+ this.activityStore = options.activityStore;
75
+ this.transactionMaterialStore = options.transactionMaterialStore;
76
+ this.logger = options.logger;
77
+ this.validateAdapterLifecycle = options.validateAdapterLifecycle;
78
+ }
79
+ async createReviewSession(plans, now = new Date()) {
80
+ if (plans.length !== 1) {
81
+ throw new SessionStoreError("input_invalid", "Exactly one action plan is required per review session");
82
+ }
83
+ const { base, token } = createLocalSessionBase(now, this.ttlMs);
84
+ const session = {
85
+ ...base,
86
+ ownerId: this.ownerId,
87
+ reviewRevision: 0,
88
+ status: "proposed",
89
+ plans
90
+ };
91
+ const reviewSessionInput = {
92
+ reviewSessionId: session.id,
93
+ plan: plans[0],
94
+ currentStatus: session.status,
95
+ createdAt: session.createdAt
96
+ };
97
+ await this.commitLiveReviewSessionProcess({
98
+ sessionId: session.id,
99
+ nextSession: session,
100
+ commitWithLiveSession: this.activityStore.recordReviewSessionWithLiveSession
101
+ ? (live) => this.activityStore.recordReviewSessionWithLiveSession(reviewSessionInput, live)
102
+ : undefined,
103
+ commitActivityOnly: () => this.activityStore.recordReviewSession(reviewSessionInput),
104
+ commitLiveSessionOnly: () => this.sessions.create(session.id, session),
105
+ failureMessage: `Review session already exists: ${session.id}`
106
+ });
107
+ await this.appendEventLog({
108
+ type: "session.created",
109
+ sessionId: session.id,
110
+ at: now.toISOString()
111
+ });
112
+ return { session: cloneLocalSession(session), token };
113
+ }
114
+ // Snapshot access has no expiry, artifact reconciliation or external work.
115
+ // Public consumers project this private record before exposing any fields.
116
+ readReviewSession(id) {
117
+ const session = this.sessions.get(id);
118
+ return session ? cloneLocalSession(session) : undefined;
119
+ }
120
+ reviewSessionIds() { return this.sessions.ids(); }
121
+ async inspectReview(id, now = this.clock()) {
122
+ const session = this.sessions.get(id), rowRevision = this.sessions.revision(id);
123
+ if (!session || rowRevision === undefined)
124
+ return undefined;
125
+ const artifacts = this.privateReviewArtifacts.get(id);
126
+ const candidate = { session, rowRevision, artifacts };
127
+ if (!needsReviewMaterial(session, this.sessions.hasAdmittedRevision(id, session.reviewRevision)))
128
+ return candidate;
129
+ if (!artifacts || !session.reviewState)
130
+ return candidate;
131
+ try {
132
+ const checked = await this.parseReviewSessionPrivateArtifacts(id, session.reviewState, artifacts, now);
133
+ const handle = checked.artifacts.transactionMaterial, digest = checked.artifacts.transactionMaterialDigest;
134
+ candidate.material = { reviewSessionId: id, reviewRevision: session.reviewRevision, rowRevision, review: session, artifacts,
135
+ planId: handle.planId, account: handle.account, reviewedTransactionDigest: digest.transactionDigest,
136
+ transactionMaterial: handle, transactionMaterialDigest: digest,
137
+ transactionBytesBase64: Buffer.from(checked.transactionBytes).toString("base64") };
138
+ }
139
+ catch (error) {
140
+ // Invalid evidence is a candidate for refresh, never permission to mutate
141
+ // a potentially newer revision. Storage failures must still propagate.
142
+ if (!(error instanceof LocalTransactionMaterialStoreError) && !(error instanceof ReviewEvidenceMismatch) && !(error instanceof ZodError))
143
+ throw error;
144
+ }
145
+ return candidate;
146
+ }
147
+ recordEvaluationEvents(events) {
148
+ for (const event of events)
149
+ void this.appendEventLog(event).catch(() => { });
150
+ }
151
+ async getReviewSession(id, clock = this.clock) {
152
+ const candidate = await this.inspectReview(id, clock());
153
+ if (!candidate)
154
+ return undefined;
155
+ if (this.activityStore.finalizeReviewEvaluation) {
156
+ const result = this.activityStore.finalizeReviewEvaluation(candidate, clock);
157
+ this.recordEvaluationEvents(result.events);
158
+ return cloneLocalSession(result.session);
159
+ }
160
+ // Preparation-only in-memory fixtures share the decision function. They do
161
+ // not establish SQLite atomicity or wallet admission guarantees.
162
+ const current = this.sessions.get(id);
163
+ if (!current)
164
+ return undefined;
165
+ const at = clock(), artifacts = this.privateReviewArtifacts.get(id);
166
+ const admitted = this.sessions.hasAdmittedRevision(id, current.reviewRevision);
167
+ const material = !admitted && !current.preparationId && artifacts?.transactionMaterial
168
+ ? this.transactionMaterialStore?.getTransactionMaterial(artifacts.transactionMaterial, at) : undefined;
169
+ if (!admitted && !current.preparationId && artifacts && !material && !needsReviewMaterial(current, admitted)) {
170
+ this.deleteReviewSessionTransactionMaterials(id);
171
+ }
172
+ const next = decideReviewEvaluation(candidate, current, this.sessions.revision(id), artifacts, material, admitted, at);
173
+ if (next === current)
174
+ return cloneLocalSession(current);
175
+ if (next.status === "expired")
176
+ return this.expireReviewSession(id, current, at);
177
+ await this.recordReviewStateSnapshotWithLiveSession({ reviewSessionId: id, fromStatus: current.status,
178
+ state: next.reviewState, reviewRevision: next.reviewRevision, recordedAt: at.toISOString() }, current, next, { deleteTransactionMaterials: true });
179
+ return cloneLocalSession(next);
180
+ }
181
+ async listReviewSessions(clock = this.clock) {
182
+ const sessions = [];
183
+ for (const id of this.sessions.ids()) {
184
+ const session = await this.getReviewSession(id, clock);
185
+ if (session) {
186
+ sessions.push(session);
187
+ }
188
+ }
189
+ return sessions;
190
+ }
191
+ async validateReviewToken(id, token, _now = new Date()) {
192
+ const session = this.sessions.get(id);
193
+ if (!session) {
194
+ return false;
195
+ }
196
+ // Review sessions expose expired/final lifecycle states through read APIs after token validation.
197
+ // Mutable methods own expiry transitions and return lifecycle-specific errors.
198
+ return tokenMatchesHash(session.tokenHash, token);
199
+ }
200
+ async recordWalletConnected(id, account, now = new Date()) {
201
+ const session = await this.requireMutableSession(id, now);
202
+ const normalizedAccount = parseSuiAddress(account);
203
+ if (!normalizedAccount) {
204
+ throw new SessionStoreError("input_invalid", "Invalid wallet account address");
205
+ }
206
+ const activeAccount = await this.activityStore.getActiveAccount();
207
+ if (!activeAccount) {
208
+ throw new SessionStoreError("active_account_not_set", "Review account binding requires an active read account");
209
+ }
210
+ if (activeAccount.address !== normalizedAccount) {
211
+ throw new SessionStoreError("invalid_session_transition", `Review account does not match active read account: ${id}`);
212
+ }
213
+ const nextSession = cloneLocalSession(session);
214
+ if (session.account && session.account !== normalizedAccount) {
215
+ throw new SessionStoreError("invalid_session_transition", `Review session already bound to a different account: ${id}`);
216
+ }
217
+ if (nextSession.status === "proposed") {
218
+ // Explicit backend account binding follows the preparation lifecycle.
219
+ // Active read context alone is not wallet approval; the workflow checks
220
+ // the live wallet connection before invoking this operation.
221
+ transition(nextSession, "awaiting_wallet");
222
+ transition(nextSession, "wallet_connected");
223
+ }
224
+ else if (nextSession.status === "awaiting_wallet") {
225
+ transition(nextSession, "wallet_connected");
226
+ }
227
+ else if (!REVIEW_STATE_RECOMPUTE_STATUSES.has(nextSession.status)) {
228
+ throw new SessionStoreError("invalid_session_transition", `Invalid session transition: ${nextSession.status} -> wallet_connected`);
229
+ }
230
+ nextSession.account = normalizedAccount;
231
+ nextSession.lastActivityAt = now.toISOString();
232
+ await this.recordReviewTransitionWithLiveSession({
233
+ reviewSessionId: id,
234
+ event: "wallet_connected",
235
+ fromStatus: session.status,
236
+ toStatus: nextSession.status,
237
+ account: normalizedAccount,
238
+ transitionedAt: now.toISOString()
239
+ }, session, nextSession);
240
+ await this.appendEventLog({
241
+ type: "wallet.connected",
242
+ sessionId: id,
243
+ walletAddressHash: hashEventValue(normalizedAccount),
244
+ at: now.toISOString()
245
+ });
246
+ return cloneLocalSession(nextSession);
247
+ }
248
+ async recordReviewState(id, state, now = new Date()) {
249
+ return this.recordReviewStateInternal(id, state, undefined, now);
250
+ }
251
+ async recordReviewStateWithArtifacts(id, state, privateArtifacts, now = this.clock(), preparation) {
252
+ if (this.sessions.hasUnsettledRequest(id, now)) {
253
+ throw new SessionStoreError("invalid_session_transition", "The previous wallet request is still being settled.");
254
+ }
255
+ return this.recordReviewStateInternal(id, state, privateArtifacts, now, preparation);
256
+ }
257
+ async recordReviewStateInternal(id, state, privateArtifacts, now, preparation) {
258
+ const session = await this.requireMutableSession(id, now);
259
+ if (preparation && (session.preparationId !== preparation.id || session.walletConnectionId !== preparation.connectionId ||
260
+ session.walletConnectionRevision !== preparation.connectionRevision))
261
+ this.throwStaleReviewSession(id);
262
+ assertSameSessionId(id, state.reviewSessionId, "Review state");
263
+ assertPlanInSession(session, state.planId);
264
+ const parsedState = parseReviewState(state, this.validateAdapterLifecycle);
265
+ if (!session.account) {
266
+ throw new SessionStoreError("invalid_session_transition", `Review state requires a wallet-connected account: ${id}`);
267
+ }
268
+ if (session.account !== parsedState.account) {
269
+ throw new SessionStoreError("invalid_session_transition", `Review state account does not match the review session account: ${id}`);
270
+ }
271
+ let verified;
272
+ try {
273
+ verified = await this.assertReviewSessionPrivateArtifacts(id, parsedState, privateArtifacts, now);
274
+ }
275
+ catch (error) {
276
+ if (isDeepStrictEqual(this.sessions.get(id), session))
277
+ this.deleteReviewSessionTransactionMaterials(id);
278
+ throw error;
279
+ }
280
+ const nextSession = cloneLocalSession(session);
281
+ transition(nextSession, parsedState.status);
282
+ nextSession.account = session.account;
283
+ nextSession.reviewState = parsedState;
284
+ nextSession.reviewRevision = session.reviewRevision + 1;
285
+ delete nextSession.preparationId;
286
+ delete nextSession.preparationError;
287
+ nextSession.lastActivityAt = now.toISOString();
288
+ try {
289
+ await this.recordReviewStateSnapshotWithLiveSession({
290
+ reviewSessionId: id,
291
+ fromStatus: session.status,
292
+ state: parsedState,
293
+ reviewRevision: nextSession.reviewRevision,
294
+ recordedAt: now.toISOString()
295
+ }, session, nextSession, {
296
+ privateArtifactsJson: this.privateArtifactsJsonForLiveMutation(privateArtifacts),
297
+ ...(preparation && verified ? { publication: { material: verified, clock: this.clock } } : {})
298
+ }, () => this.replaceReviewSessionPrivateArtifacts(id, privateArtifacts));
299
+ await this.appendEventLog({
300
+ type: "state.computed",
301
+ sessionId: id,
302
+ planId: parsedState.planId,
303
+ walletAddressHash: hashEventValue(parsedState.account),
304
+ status: parsedState.status,
305
+ at: now.toISOString()
306
+ });
307
+ return cloneLocalSession(nextSession);
308
+ }
309
+ catch (error) {
310
+ if (privateArtifacts && !this.isStaleReviewSessionCommitError(error) && isDeepStrictEqual(this.sessions.get(id), session)) {
311
+ this.deleteReviewSessionTransactionMaterials(id);
312
+ }
313
+ throw error;
314
+ }
315
+ }
316
+ async requireMutableSession(id, now) {
317
+ const session = this.sessions.get(id);
318
+ if (!session) {
319
+ throw new SessionStoreError("session_not_found", `Review session not found: ${id}`);
320
+ }
321
+ if (isLocalSessionExpired(session, now) && !isFinalSessionStatus(session.status)) {
322
+ await this.expireReviewSession(id, session, now);
323
+ throw new SessionStoreError("session_expired", `Review session expired: ${id}`);
324
+ }
325
+ if (session.status === "expired") {
326
+ throw new SessionStoreError("session_expired", `Review session expired: ${id}`);
327
+ }
328
+ return session;
329
+ }
330
+ async createSettingsSession(now = new Date()) {
331
+ return this.settings.create(now);
332
+ }
333
+ async getSettingsSession(id, now = new Date()) {
334
+ return this.settings.get(id, now);
335
+ }
336
+ async validateSettingsToken(id, token, now = new Date()) {
337
+ return this.settings.validateToken(id, token, now);
338
+ }
339
+ async prepareReviewedTransaction(id, planId, account, now = this.clock()) {
340
+ const candidate = await this.inspectReview(id, now);
341
+ if (!candidate)
342
+ throw new SessionStoreError("session_not_found", "Review session is unavailable.");
343
+ assertPlanInSession(candidate.session, planId);
344
+ const state = candidate.session.reviewState;
345
+ if (candidate.session.status !== "ready_for_wallet_review" || state?.planId !== planId || !state.transactionReviewData) {
346
+ throw new SessionStoreError("invalid_session_transition", "Reviewed transaction requires current verified review evidence.");
347
+ }
348
+ if (state.account !== parseSuiAddress(account))
349
+ throw new SessionStoreError("input_invalid", "Reviewed transaction account does not match the reviewed account");
350
+ const material = candidate.material;
351
+ if (!material)
352
+ throw new SessionStoreError("handoff_unavailable", "Reviewed transaction material is unavailable.");
353
+ if (material.reviewedTransactionDigest !== state.transactionReviewData.reviewedTransactionDigest) {
354
+ throw new SessionStoreError("handoff_commitment_mismatch", "Reviewed transaction digest differs from the reviewed commitment.");
355
+ }
356
+ return material;
357
+ }
358
+ async invalidateAllLocalSessions(reason, now = new Date()) {
359
+ for (const id of this.sessions.ids()) {
360
+ this.deleteReviewSessionTransactionMaterials(id);
361
+ }
362
+ this.sessions.clear();
363
+ this.settings.clear();
364
+ await this.appendEventLog({
365
+ type: "local_sessions.invalidated",
366
+ sessionId: "all",
367
+ reason,
368
+ at: now.toISOString()
369
+ });
370
+ }
371
+ async appendEventLog(record) {
372
+ try {
373
+ await this.eventLog.append(record);
374
+ }
375
+ catch (error) {
376
+ // Event logs are optional audit/debug sinks. SQLite and in-memory session state remain authoritative.
377
+ this.logger.error("event log append failed", {
378
+ eventType: record.type,
379
+ error: error instanceof Error ? error.message : String(error)
380
+ });
381
+ }
382
+ }
383
+ deleteReviewSessionTransactionMaterials(reviewSessionId) {
384
+ try {
385
+ this.transactionMaterialStore?.deleteReviewSessionTransactionMaterials(reviewSessionId);
386
+ }
387
+ catch (error) {
388
+ this.logger.error("transaction material cleanup failed", {
389
+ reviewSessionId,
390
+ error: error instanceof Error ? error.message : String(error)
391
+ });
392
+ }
393
+ this.privateReviewArtifacts.delete(reviewSessionId);
394
+ }
395
+ async commitLiveReviewSessionProcess(input) {
396
+ const live = input.expectedSession
397
+ ? {
398
+ expected: input.expectedSession,
399
+ next: input.nextSession,
400
+ ...input.liveSideEffects
401
+ }
402
+ : {
403
+ next: input.nextSession,
404
+ ...input.liveSideEffects
405
+ };
406
+ if (this.canUseActivityStoreLiveSessionMutations() && input.commitWithLiveSession) {
407
+ const committed = await input.commitWithLiveSession(live);
408
+ if (!committed) {
409
+ this.throwStaleReviewSession(input.sessionId, input.failureMessage);
410
+ }
411
+ return;
412
+ }
413
+ try {
414
+ await input.commitActivityOnly();
415
+ input.commitLiveSessionOnly();
416
+ input.applyActivityOnlySideEffects?.();
417
+ }
418
+ catch (error) {
419
+ if (input.applyActivityOnlySideEffectsOnFailure) {
420
+ input.applyActivityOnlySideEffects?.();
421
+ }
422
+ throw error;
423
+ }
424
+ }
425
+ async recordReviewTransitionWithLiveSession(input, expectedSession, nextSession, liveSideEffects = {}, options = {}) {
426
+ await this.commitLiveReviewSessionProcess({
427
+ sessionId: expectedSession.id,
428
+ expectedSession,
429
+ nextSession,
430
+ liveSideEffects,
431
+ commitWithLiveSession: this.activityStore.recordReviewTransitionWithLiveSession
432
+ ? (live) => this.activityStore.recordReviewTransitionWithLiveSession(input, live)
433
+ : undefined,
434
+ commitActivityOnly: () => this.activityStore.recordReviewTransition(input),
435
+ commitLiveSessionOnly: () => this.commitReviewSessionUpdate(expectedSession.id, expectedSession, nextSession),
436
+ applyActivityOnlySideEffects: () => this.applyActivityOnlyLiveSessionSideEffects(expectedSession.id, liveSideEffects),
437
+ applyActivityOnlySideEffectsOnFailure: options.applyActivityOnlySideEffectsOnFailure
438
+ });
439
+ }
440
+ async recordReviewStateSnapshotWithLiveSession(input, expectedSession, nextSession, liveSideEffects = {}, applyActivityOnlySideEffects) {
441
+ await this.commitLiveReviewSessionProcess({
442
+ sessionId: expectedSession.id,
443
+ expectedSession,
444
+ nextSession,
445
+ liveSideEffects,
446
+ commitWithLiveSession: this.activityStore.recordReviewStateSnapshotWithLiveSession
447
+ ? (live) => this.activityStore.recordReviewStateSnapshotWithLiveSession(input, live)
448
+ : undefined,
449
+ commitActivityOnly: () => this.activityStore.recordReviewStateSnapshot(input),
450
+ commitLiveSessionOnly: () => this.commitReviewSessionUpdate(expectedSession.id, expectedSession, nextSession),
451
+ applyActivityOnlySideEffects: applyActivityOnlySideEffects
452
+ ?? (() => this.applyActivityOnlyLiveSessionSideEffects(expectedSession.id, liveSideEffects))
453
+ });
454
+ }
455
+ applyActivityOnlyLiveSessionSideEffects(reviewSessionId, liveSideEffects) {
456
+ if (liveSideEffects.deleteTransactionMaterials) {
457
+ this.deleteReviewSessionTransactionMaterials(reviewSessionId);
458
+ }
459
+ }
460
+ privateArtifactsJsonForLiveMutation(privateArtifacts) {
461
+ if (!privateArtifacts?.transactionMaterial || !privateArtifacts.transactionMaterialDigest) {
462
+ return null;
463
+ }
464
+ return JSON.stringify(clonePrivateReviewArtifacts(privateArtifacts));
465
+ }
466
+ canUseActivityStoreLiveSessionMutations() {
467
+ return this.sessions.usesActivityStoreLiveSessionMutations === true;
468
+ }
469
+ commitReviewSessionUpdate(id, expectedSession, nextSession) {
470
+ if (this.sessions.commitReviewSessionTransition(id, expectedSession, nextSession)) {
471
+ return;
472
+ }
473
+ this.throwStaleReviewSession(id);
474
+ }
475
+ throwStaleReviewSession(id, message) {
476
+ throw new SessionStoreError("invalid_session_transition", message ?? `Review session changed before transition committed: ${id}`);
477
+ }
478
+ isStaleReviewSessionCommitError(error) {
479
+ return (error instanceof SessionStoreError &&
480
+ error.code === "invalid_session_transition" &&
481
+ error.message.includes("changed before transition committed"));
482
+ }
483
+ async expireReviewSession(id, session, now) {
484
+ const nextSession = cloneLocalSession(session);
485
+ transition(nextSession, "expired");
486
+ await this.recordReviewTransitionWithLiveSession({
487
+ reviewSessionId: id,
488
+ event: "expired",
489
+ fromStatus: session.status,
490
+ toStatus: nextSession.status,
491
+ transitionedAt: now.toISOString()
492
+ }, session, nextSession, { deleteTransactionMaterials: true }, {
493
+ applyActivityOnlySideEffectsOnFailure: true
494
+ });
495
+ return nextSession;
496
+ }
497
+ replaceReviewSessionPrivateArtifacts(reviewSessionId, privateArtifacts) {
498
+ if (!privateArtifacts?.transactionMaterial || !privateArtifacts.transactionMaterialDigest) {
499
+ this.deleteReviewSessionTransactionMaterials(reviewSessionId);
500
+ return;
501
+ }
502
+ this.privateReviewArtifacts.set(reviewSessionId, privateArtifacts);
503
+ }
504
+ async assertReviewSessionPrivateArtifacts(reviewSessionId, state, privateArtifacts, now) {
505
+ if (!privateArtifacts) {
506
+ if (state.humanReadableReview || state.simulation) {
507
+ throw new SessionStoreError("session_mismatch", `Review private-derived state requires matching private evidence: ${reviewSessionId}`);
508
+ }
509
+ return;
510
+ }
511
+ try {
512
+ return await this.parseReviewSessionPrivateArtifacts(reviewSessionId, state, privateArtifacts, now);
513
+ }
514
+ catch (error) {
515
+ this.logger.error("private review artifact rejected", {
516
+ reviewSessionId,
517
+ error: error instanceof Error ? error.message : String(error)
518
+ });
519
+ throw new SessionStoreError("session_mismatch", `Review private artifacts do not match the stored review state: ${reviewSessionId}`);
520
+ }
521
+ }
522
+ async parseReviewSessionPrivateArtifacts(reviewSessionId, state, privateArtifacts, now) {
523
+ const { transactionMaterial, transactionMaterialDigest } = privateArtifacts;
524
+ if (!transactionMaterial ||
525
+ !transactionMaterialDigest ||
526
+ !this.transactionMaterialStore) {
527
+ throw new ReviewEvidenceMismatch("missing private artifact material, digest, or material store");
528
+ }
529
+ const parsed = await verifyLocalTransactionMaterialArtifacts({
530
+ materialStore: this.transactionMaterialStore,
531
+ transactionMaterial,
532
+ transactionMaterialDigest,
533
+ now
534
+ });
535
+ if (parsed.transactionMaterial.reviewSessionId !== reviewSessionId ||
536
+ parsed.transactionMaterial.planId !== state.planId ||
537
+ parsed.transactionMaterial.account !== state.account) {
538
+ throw new ReviewEvidenceMismatch("private artifacts do not match review state identity");
539
+ }
540
+ try {
541
+ const transactionObjectOwnership = privateArtifacts.transactionObjectOwnership
542
+ ? verifyTransactionObjectOwnershipEvidence({
543
+ transactionMaterial: parsed.transactionMaterial,
544
+ transactionMaterialDigest: parsed.transactionMaterialDigest,
545
+ evidence: privateArtifacts.transactionObjectOwnership,
546
+ transactionBytes: parsed.transactionBytes,
547
+ now
548
+ })
549
+ : undefined;
550
+ const swapQuotePolicy = privateArtifacts.swapQuotePolicy
551
+ ? verifySwapQuotePolicyEvidence({
552
+ transactionMaterial: parsed.transactionMaterial,
553
+ evidence: privateArtifacts.swapQuotePolicy,
554
+ now
555
+ })
556
+ : undefined;
557
+ const humanReadableReview = privateArtifacts.humanReadableReview
558
+ ? verifySupportedHumanReadableReviewEvidence({
559
+ transactionMaterial: parsed.transactionMaterial,
560
+ transactionMaterialDigest: parsed.transactionMaterialDigest,
561
+ swapQuotePolicy,
562
+ transactionObjectOwnership,
563
+ evidence: privateArtifacts.humanReadableReview,
564
+ now
565
+ })
566
+ : undefined;
567
+ const reviewTimeSimulation = privateArtifacts.reviewTimeSimulation
568
+ ? verifyReviewTimeSimulationEvidence({
569
+ transactionMaterial: parsed.transactionMaterial,
570
+ transactionMaterialDigest: parsed.transactionMaterialDigest,
571
+ evidence: privateArtifacts.reviewTimeSimulation,
572
+ transactionBytes: parsed.transactionBytes,
573
+ now
574
+ })
575
+ : undefined;
576
+ const verifiedArtifacts = {
577
+ transactionMaterial: parsed.transactionMaterial,
578
+ transactionMaterialDigest: parsed.transactionMaterialDigest,
579
+ ...(swapQuotePolicy ? { swapQuotePolicy } : {}),
580
+ ...(transactionObjectOwnership ? { transactionObjectOwnership } : {}),
581
+ ...(humanReadableReview ? { humanReadableReview } : {}),
582
+ ...(reviewTimeSimulation ? { reviewTimeSimulation } : {})
583
+ };
584
+ assertPrivateDerivedReviewStateProjections(state, verifiedArtifacts);
585
+ return { artifacts: verifiedArtifacts, transactionBytes: parsed.transactionBytes };
586
+ }
587
+ catch (error) {
588
+ throw new ReviewEvidenceMismatch(error instanceof Error ? error.message : "Private review evidence does not match.");
589
+ }
590
+ }
591
+ }
592
+ // In-memory session store: the orchestration above with Map-backed record and
593
+ // artifact stores. Public name + constructor are unchanged so existing callers and
594
+ // the session-store contract test keep working as the regression wall.
595
+ export class InMemorySessionStore extends LocalSessionStore {
596
+ constructor(options) {
597
+ super({
598
+ ...options,
599
+ sessions: new InMemorySessionRecordStore(),
600
+ artifacts: new InMemoryPrivateReviewArtifactStore()
601
+ });
602
+ }
603
+ }
604
+ function assertPrivateDerivedReviewStateProjections(state, privateArtifacts) {
605
+ for (const binding of PRIVATE_DERIVED_REVIEW_FIELD_BINDINGS) {
606
+ const publicValue = binding.getPublicState(state);
607
+ const privateEvidence = binding.getPrivateEvidence(privateArtifacts);
608
+ if (publicValue === undefined && privateEvidence === undefined) {
609
+ continue;
610
+ }
611
+ if (publicValue === undefined || privateEvidence === undefined) {
612
+ throw new ReviewEvidenceMismatch(`review state ${binding.field} must match private ${binding.field} evidence`);
613
+ }
614
+ const projected = binding.projectPrivateEvidence(privateEvidence);
615
+ if (!isDeepStrictEqual(publicValue, projected)) {
616
+ throw new ReviewEvidenceMismatch(`review state ${binding.field} must be projected from private ${binding.field} evidence`);
617
+ }
618
+ }
619
+ }
620
+ export function transition(session, next) {
621
+ if (session.status === next) {
622
+ return;
623
+ }
624
+ const allowed = ALLOWED_TRANSITIONS[session.status] ?? [];
625
+ if (!allowed.includes(next)) {
626
+ throw new SessionStoreError("invalid_session_transition", `Invalid session transition: ${session.status} -> ${next}`);
627
+ }
628
+ session.status = next;
629
+ }
630
+ function assertPlanInSession(session, planId) {
631
+ if (!session.plans.some((plan) => plan.id === planId)) {
632
+ throw new SessionStoreError("plan_not_in_session", `Action plan not found in review session: ${planId}`);
633
+ }
634
+ }
635
+ function assertSameSessionId(expected, actual, label) {
636
+ if (actual !== expected) {
637
+ throw new SessionStoreError("session_mismatch", `${label} session mismatch: expected ${expected}, got ${actual}`);
638
+ }
639
+ }
640
+ function parseReviewState(state, validateAdapterLifecycle) {
641
+ let parsed;
642
+ try {
643
+ parsed = parseLifecycleValidatedReviewState(state, validateAdapterLifecycle);
644
+ }
645
+ catch {
646
+ throw new SessionStoreError("input_invalid", "Invalid review state shape or adapter lifecycle");
647
+ }
648
+ const normalizedAccount = parseSuiAddress(parsed.account);
649
+ if (!normalizedAccount) {
650
+ throw new SessionStoreError("input_invalid", "Invalid review state account");
651
+ }
652
+ return { ...parsed, account: normalizedAccount };
653
+ }
654
+ class ReviewEvidenceMismatch extends Error {
655
+ }
@@ -0,0 +1 @@
1
+ export {};