@zktx.io/sui-mcp 0.5.0 → 0.6.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 (87) hide show
  1. package/README.md +6 -6
  2. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +3 -3
  3. package/dist/core/activity/localDataService.js +71 -52
  4. package/dist/core/activity/localDataValidation.js +1 -3
  5. package/dist/core/activity/sqliteActivityStore.js +24 -16
  6. package/dist/core/activity/sqliteActivityStoreSchema.js +83 -12
  7. package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
  8. package/dist/core/session/cardSession.js +1 -1
  9. package/dist/core/session/cardSessionStore.js +53 -3
  10. package/dist/core/session/sessionStore.js +0 -17
  11. package/dist/core/session/settingsFileIdentity.js +11 -0
  12. package/dist/core/session/settingsFileProtocol.js +25 -0
  13. package/dist/core/session/settingsFileStore.js +346 -0
  14. package/dist/core/session/settingsInputError.js +34 -0
  15. package/dist/core/session/settingsOperation.js +224 -0
  16. package/dist/core/session/settingsPreparationSql.js +15 -0
  17. package/dist/core/session/settingsWorkflow.js +1219 -0
  18. package/dist/core/session/sqliteSessionStore.js +0 -40
  19. package/dist/core/session/sqliteSettingsWorkflowStore.js +463 -0
  20. package/dist/core/session/sqliteWalletWorkflowStore.js +164 -41
  21. package/dist/core/session/status.js +2 -2
  22. package/dist/core/session/walletConnection.js +27 -8
  23. package/dist/core/session/walletWorkflow.js +73 -15
  24. package/dist/core/session/workflowState.js +16 -8
  25. package/dist/core/session/workflowView.js +18 -2
  26. package/dist/mcp/responseGuidance.js +10 -5
  27. package/dist/mcp/serverInfo.js +3 -4
  28. package/dist/mcp/toolNames.js +2 -2
  29. package/dist/mcp/tools/account/index.js +1 -19
  30. package/dist/mcp/tools/read/readToolHelpers.js +5 -4
  31. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  32. package/dist/mcp/tools/session/shared.js +1 -0
  33. package/dist/mcp/tools/session/statusTools.js +4 -3
  34. package/dist/mcp/tools/settings/index.js +8 -28
  35. package/dist/mcp-app/account.html +14 -14
  36. package/dist/mcp-app/account.notices.txt +1 -1
  37. package/dist/mcp-app/chart.html +9 -9
  38. package/dist/mcp-app/chart.notices.txt +1 -1
  39. package/dist/mcp-app/connect.html +14 -14
  40. package/dist/mcp-app/connect.notices.txt +1 -1
  41. package/dist/mcp-app/receipt.html +1298 -1463
  42. package/dist/mcp-app/receipt.notices.txt +226 -1204
  43. package/dist/mcp-app/review.html +1298 -1463
  44. package/dist/mcp-app/review.notices.txt +226 -1204
  45. package/dist/mcp-app/settings.html +3133 -0
  46. package/dist/mcp-app/settings.notices.txt +973 -0
  47. package/dist/mcp-ui/contracts.js +8 -2
  48. package/dist/mcp-ui/readCards.js +1 -1
  49. package/dist/mcp-ui/tools.js +33 -2
  50. package/dist/review-server/server.js +5 -51
  51. package/dist/runtime/application.js +41 -14
  52. package/dist/runtime/localSettingsService.js +26 -2
  53. package/dist/runtime/shared/authenticatedFetch.js +6 -2
  54. package/dist/runtime/shared/control.js +5 -6
  55. package/dist/runtime/shared/http.js +14 -0
  56. package/dist/runtime/shared/mcpHttp.js +9 -4
  57. package/dist/runtime/shared/server.js +1 -1
  58. package/dist/runtime/smokeMainnetRead.js +0 -1
  59. package/dist/runtime/walletConnectStorage.js +2 -4
  60. package/dist/runtime/walletSdkChildRuntime.js +2 -1
  61. package/dist/runtime/walletSdkIpc.js +2 -1
  62. package/dist/runtime/walletSdkProcess.js +3 -2
  63. package/docs/AGENT_BEHAVIOR.md +44 -10
  64. package/docs/AGENT_DEVELOPMENT_POLICY.md +16 -12
  65. package/docs/FRONTEND_POLICY.md +105 -28
  66. package/docs/LOCAL_DB_ARCHITECTURE.md +143 -20
  67. package/docs/MCP_SETUP.md +99 -39
  68. package/docs/MCP_TOOLS.md +273 -24
  69. package/docs/SDK_API.md +51 -10
  70. package/docs/UTILITY_INDEX.md +9 -6
  71. package/docs/WALLET_CONNECTION.md +30 -9
  72. package/package.json +13 -7
  73. package/dist/core/activity/schemaVersion.js +0 -2
  74. package/dist/core/session/keyedRecordStore.js +0 -18
  75. package/dist/core/session/settingsSession.js +0 -1
  76. package/dist/core/session/settingsSessions.js +0 -45
  77. package/dist/review-app/brand-dark.svg +0 -1
  78. package/dist/review-app/brand-light.svg +0 -1
  79. package/dist/review-app/favicon.svg +0 -1
  80. package/dist/review-app/settings.css +0 -1
  81. package/dist/review-app/settings.js +0 -1
  82. package/dist/review-app/ui.css +0 -809
  83. package/dist/review-server/assets.js +0 -48
  84. package/dist/review-server/html.js +0 -45
  85. package/dist/review-server/http.js +0 -47
  86. package/dist/review-server/middleware/reviewToken.js +0 -7
  87. package/dist/review-server/settingsApi.js +0 -158
@@ -0,0 +1,224 @@
1
+ import { z } from "zod";
2
+ import { assetReadAccountSchema, walletAvailabilitySchema } from "./walletConnection.js";
3
+ import { LOCAL_DATA_EXPORT_FORMAT } from "../activity/localDataTypes.js";
4
+ import { suiAddressStringSchema } from "../suiAddress.js";
5
+ // This is the existing backup input limit, measured before Base64/JSON wrapping.
6
+ export const SETTINGS_IMPORT_MAX_SIZE = 16 * 1024 * 1024;
7
+ const identity = z.string().uuid();
8
+ const size = z.number().int().nonnegative().safe();
9
+ const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
10
+ export const restoreCheckSchema = z.discriminatedUnion("status", [
11
+ z.object({ status: z.literal("no_file") }).strict(),
12
+ z.object({ status: z.literal("receiving") }).strict(),
13
+ z.object({ status: z.literal("checking") }).strict(),
14
+ z.object({ status: z.literal("check_required"), message: z.string().min(1) }).strict(),
15
+ z.object({ status: z.literal("can_restore") }).strict(),
16
+ z.object({ status: z.literal("cannot_restore"), reason: z.enum([
17
+ "invalid_backup", "file_unavailable", "wallet_request_unsettled", "operation_busy", "input_unavailable", "check_failed"
18
+ ]), message: z.string().min(1) }).strict()
19
+ ]);
20
+ export const storedRestoreCheckSchema = z.object({ fileId: identity, sha256, totalSize: size, dataEpoch: size,
21
+ userDataRevision: size, check: restoreCheckSchema }).strict();
22
+ export const settingsDraftSchema = z.discriminatedUnion("kind", [
23
+ z.object({ kind: z.literal("set_endpoint"), endpoint: z.enum(["grpc", "graphql"]), value: z.string().min(1) }).strict(),
24
+ z.object({ kind: z.literal("restore_endpoint"), endpoint: z.enum(["grpc", "graphql"]) }).strict(),
25
+ z.object({ kind: z.enum(["clear_read_address", "export_local_data", "download_backup", "import_local_data", "reset_local_data", "restart_wallet_service"]) }).strict()
26
+ ]);
27
+ export const settingsDownloadReportSchema = z.enum(["acknowledged", "not_completed", "unconfirmed", "unsupported"]);
28
+ export const settingsDownloadFailureSchema = z.object({ method: z.literal("ui/download-file"), code: z.literal(-32601) }).strict();
29
+ export const settingsDownloadEndSchema = z.enum(["acknowledged", "cancelled", "expired", "server_restarted", "data_replaced", "source_unavailable", "stored_locally", "local_save_failed"]);
30
+ export const settingsFileIdentitySchema = z.object({ device: z.string().regex(/^\d+$/), inode: z.string().regex(/^\d+$/) }).strict();
31
+ const localSaveFailure = z.enum(["not_published", "target_conflict", "source_unavailable", "interrupted"]);
32
+ const storedLocalSaveSchema = z.object({ status: z.enum(["needs_consent", "saving", "saved", "failed"]),
33
+ attemptId: identity, fileId: identity, sha256, totalSize: size, destination: z.string(), ownerId: z.string().min(1),
34
+ currentSaveAttemptId: identity.optional(),
35
+ attempts: z.array(z.object({ saveAttemptId: identity, previousSaveAttemptId: identity.optional(),
36
+ admittedAt: z.string().datetime(), allowPossibleCopy: z.boolean() }).strict()),
37
+ sourceIdentity: settingsFileIdentitySchema.optional(), savedAt: z.string().datetime().optional(), failure: localSaveFailure.optional()
38
+ }).strict().superRefine((value, context) => {
39
+ if (value.status === "needs_consent" ? value.attempts.length !== 0 || value.currentSaveAttemptId !== undefined :
40
+ !value.currentSaveAttemptId || !value.attempts.some((a) => a.saveAttemptId === value.currentSaveAttemptId)) {
41
+ context.addIssue({ code: "custom", message: "A local save must identify its admitted attempt." });
42
+ }
43
+ if (new Set(value.attempts.map((a) => a.saveAttemptId)).size !== value.attempts.length ||
44
+ (value.status === "saved") !== (value.savedAt !== undefined) || value.status === "saved" && !value.sourceIdentity ||
45
+ (value.status === "failed") !== (value.failure !== undefined)) {
46
+ context.addIssue({ code: "custom", message: "Local save evidence must match its state." });
47
+ }
48
+ });
49
+ export const storedSettingsDownloadSchema = z.object({
50
+ currentAttemptId: identity,
51
+ attempts: z.array(z.object({ attemptId: identity, admittedAt: z.string().datetime(), previousAttemptId: identity.optional(),
52
+ hostReport: settingsDownloadReportSchema.optional(), failure: settingsDownloadFailureSchema.optional() }).strict()).min(1),
53
+ readyAt: z.string().datetime().optional(), expiresAt: z.string().datetime().optional(), endReason: settingsDownloadEndSchema.optional(),
54
+ localSave: storedLocalSaveSchema.optional()
55
+ }).strict().superRefine((value, context) => {
56
+ if (new Set(value.attempts.map((attempt) => attempt.attemptId)).size !== value.attempts.length ||
57
+ !value.attempts.some((attempt) => attempt.attemptId === value.currentAttemptId) ||
58
+ (value.readyAt === undefined) !== (value.expiresAt === undefined)) {
59
+ context.addIssue({ code: "custom", message: "Download attempts and lifetime must identify one stored snapshot." });
60
+ }
61
+ if (value.endReason === "stored_locally" && value.localSave?.status !== "saved" ||
62
+ value.endReason === "local_save_failed" && value.localSave?.status !== "failed" ||
63
+ value.attempts.some((a) => (a.hostReport === "unsupported") !== (a.failure !== undefined)) ||
64
+ value.localSave && (value.localSave.attemptId !== value.currentAttemptId ||
65
+ !value.attempts.some((a) => a.attemptId === value.currentAttemptId && a.hostReport === "unsupported") ||
66
+ value.localSave.status === "saving" && value.endReason !== undefined ||
67
+ (value.localSave.status === "saved") !== (value.endReason === "stored_locally"))) {
68
+ context.addIssue({ code: "custom", message: "Local saving requires the current download's unsupported report." });
69
+ }
70
+ });
71
+ export const settingsDownloadFileActionSchema = z.enum(["settings_retry_download", "settings_report_download", "settings_cancel_download", "settings_store_backup"]);
72
+ export const settingsActionSchema = z.discriminatedUnion("action", [
73
+ z.object({ action: z.literal("settings_download_backup"), operationId: identity, attemptId: identity }).strict(),
74
+ z.object({ action: z.literal("settings_retry_download"), operationId: identity, fileId: identity,
75
+ previousAttemptId: identity, attemptId: identity }).strict(),
76
+ z.object({ action: z.literal("settings_report_download"), operationId: identity, fileId: identity,
77
+ attemptId: identity, status: settingsDownloadReportSchema, failure: settingsDownloadFailureSchema.optional() }).strict().refine((value) => (value.status === "unsupported") === (value.failure !== undefined), "Only unsupported downloads require the exact method failure."),
78
+ z.object({ action: z.literal("settings_store_backup"), operationId: identity, fileId: identity, attemptId: identity,
79
+ saveAttemptId: identity, previousSaveAttemptId: identity.optional(), allowPossibleCopy: z.boolean() }).strict(),
80
+ z.object({ action: z.literal("settings_cancel_download"), operationId: identity, fileId: identity, attemptId: identity }).strict(),
81
+ z.object({ action: z.literal("settings_select"), draft: settingsDraftSchema }).strict(),
82
+ z.object({ action: z.literal("settings_prepare"), operationId: identity }).strict(),
83
+ z.object({ action: z.literal("settings_begin_upload"), operationId: identity,
84
+ name: z.string().min(1), totalSize: size.max(SETTINGS_IMPORT_MAX_SIZE), sha256 }).strict(),
85
+ z.object({ action: z.literal("settings_upload_chunk"), operationId: identity, fileId: identity,
86
+ offset: size.max(SETTINGS_IMPORT_MAX_SIZE), chunkBase64: z.string() }).strict(),
87
+ z.object({ action: z.literal("settings_seal_upload"), operationId: identity, fileId: identity }).strict(),
88
+ z.object({ action: z.literal("settings_confirm"), operationId: identity, preparationId: identity }).strict(),
89
+ z.object({ action: z.literal("settings_back"), operationId: identity }).strict(),
90
+ z.object({ action: z.literal("settings_cancel") }).strict()
91
+ ]);
92
+ // A known rejection belongs to the selected complete file, not to the current
93
+ // local-data revision or short-lived confirmation. Selection/owner/epoch rules
94
+ // remain enforced by the operation's store and action handlers.
95
+ export function retainedBackupRejection(operation, file) {
96
+ const saved = storedRestoreCheckSchema.safeParse(operation.preparation?.restoreCheck);
97
+ if (operation.draft?.kind !== "import_local_data" || !file || operation.fileId !== file.fileId ||
98
+ file.operationId !== operation.operationId || file.role !== "upload" || file.invalidated || !file.sealed ||
99
+ file.receivedSize !== file.totalSize || file.reservation || !saved.success ||
100
+ saved.data.fileId !== file.fileId || saved.data.sha256 !== file.sha256 || saved.data.totalSize !== file.totalSize ||
101
+ saved.data.dataEpoch !== operation.dataEpoch || saved.data.check.status !== "cannot_restore" ||
102
+ saved.data.check.reason !== "invalid_backup")
103
+ return undefined;
104
+ return saved.data;
105
+ }
106
+ export function permitsBackupRecheck(check) {
107
+ return check.status === "check_required" || check.status === "cannot_restore" &&
108
+ ["wallet_request_unsettled", "operation_busy", "check_failed"].includes(check.reason);
109
+ }
110
+ export class SettingsConflict extends Error {
111
+ }
112
+ export const settingsResultSchema = z.object({
113
+ outcome: z.enum(["completed", "failed", "interrupted", "superseded"]), recordedAt: z.string().datetime(),
114
+ message: z.string().min(1), details: z.record(z.string(), z.unknown()).optional(), completionScope: z.literal("backup_snapshot").optional()
115
+ }).strict();
116
+ const endpointView = z.object({ storedValue: z.string(), effectiveValue: z.string(),
117
+ source: z.enum(["environment", "local_db", "builtin_default"]), pendingStoredValue: z.string().optional(),
118
+ appliesAfter: z.literal("mcp_server_restart").optional() }).strict();
119
+ export const endpointPreparationSchema = z.object({ endpoint: z.enum(["grpc", "graphql"]),
120
+ key: z.enum(["suiGrpcUrl", "suiGraphqlUrl"]), value: z.string().min(1), status: z.enum(["saved", "reset"]),
121
+ storedValue: z.string(), effectiveValue: z.string(), source: endpointView.shape.source }).strict().refine((target) => target.key === (target.endpoint === "grpc" ? "suiGrpcUrl" : "suiGraphqlUrl"), "The endpoint kind and preference key must agree.");
122
+ const count = z.number().int().nonnegative().safe();
123
+ const localDataCounts = z.object({ accounts: count, reviewSessions: count, reviewStateSnapshots: count, reviewStatusTransitions: count,
124
+ reviewExecutions: count, reviewRequests: count, outcomeUnknownRequests: count, externalActivityScans: count,
125
+ externalActivityTransactions: count, localSettings: count }).strict();
126
+ const importPreview = z.object({ status: z.literal("valid"), format: z.literal(LOCAL_DATA_EXPORT_FORMAT), network: z.literal("mainnet"),
127
+ exportedAt: z.string().datetime(), currentCounts: localDataCounts, incomingCounts: localDataCounts, willReplace: z.literal(true),
128
+ activeAccountChange: z.enum(["unchanged", "set", "cleared"]), currentReadAddress: suiAddressStringSchema.nullable(),
129
+ incomingReadAddress: suiAddressStringSchema.nullable(), restartRequiredAfterImport: z.boolean() }).strict();
130
+ export const settingsViewSchema = z.object({
131
+ kind: z.literal("settings"), operationId: identity,
132
+ phase: z.enum(["draft", "uploading", "sealed", "prepared", "executing", "stopping", "starting", "terminal"]),
133
+ draft: settingsDraftSchema.optional(), preparationId: identity.optional(), preparation: z.record(z.string(), z.unknown()).optional(),
134
+ file: z.object({ fileId: identity, displayName: z.string(), totalSize: size, receivedSize: size, sha256,
135
+ sealed: z.boolean(), cleanup: z.enum(["pending", "complete", "failed"]) }).strict().optional(),
136
+ download: z.object({ availability: z.enum(["preparing", "available", "ended"]), currentAttemptId: identity,
137
+ hostReport: settingsDownloadReportSchema.optional(), previousUnconfirmed: z.boolean(),
138
+ endReason: settingsDownloadEndSchema.optional(), expiresAt: z.string().datetime().optional(), remainingMs: size.optional(),
139
+ localSave: z.object({ status: z.enum(["needs_consent", "saving", "saved", "failed"]), currentSaveAttemptId: identity.optional(),
140
+ failure: localSaveFailure.optional(), savedAt: z.string().datetime().optional() }).strict().optional(),
141
+ allowedFileActions: z.array(settingsDownloadFileActionSchema) }).strict().optional(),
142
+ restoreCheck: restoreCheckSchema.optional(),
143
+ backupLocation: z.object({ directory: z.string().min(1), filename: z.string().min(1) }).strict().optional(),
144
+ result: settingsResultSchema.optional(),
145
+ temporaryFileCleanup: z.enum(["pending", "complete", "failed"]).optional(),
146
+ priorRunId: identity.optional(), nextRunId: identity.optional(), supersedesOperationId: identity.optional(), supersededByOperationId: identity.optional(),
147
+ allowedActions: z.array(z.enum(settingsActionSchema.options.map((option) => option.shape.action.value))), observe: z.boolean(),
148
+ facts: z.object({ server: z.object({ name: z.string(), version: z.string(), network: z.literal("mainnet") }).strict(),
149
+ localSettings: z.object({ suiGrpcUrl: endpointView, suiGraphqlUrl: endpointView }).strict(), readAddress: z.string().optional(),
150
+ assetReadAccount: assetReadAccountSchema, walletAvailability: walletAvailabilitySchema,
151
+ connectionReconciliationRequired: z.literal(true).optional(), observedAt: z.string().datetime().optional(),
152
+ counts: localDataCounts }).strict().optional()
153
+ }).strict().superRefine((value, context) => {
154
+ if (value.download && (value.draft?.kind !== "download_backup" || !value.file ||
155
+ value.download.availability === "available" && (!value.file.sealed || !value.result || value.download.endReason || !value.download.expiresAt) ||
156
+ value.download.availability === "ended" && !value.download.endReason)) {
157
+ context.addIssue({ code: "custom", message: "Download delivery must describe its own snapshot and lifetime." });
158
+ }
159
+ if (value.draft?.kind === "download_backup" && value.preparation !== undefined) {
160
+ context.addIssue({ code: "custom", message: "Private download preparation is not public preparation data." });
161
+ }
162
+ if (value.restoreCheck && (value.draft?.kind !== "import_local_data" || value.result)) {
163
+ context.addIssue({ code: "custom", message: "Restore checks belong to an uncompleted import preparation." });
164
+ }
165
+ if (value.restoreCheck?.status === "can_restore" && (value.phase !== "prepared" || !value.file?.sealed || !value.allowedActions.includes("settings_confirm"))) {
166
+ context.addIssue({ code: "custom", message: "Restore availability requires a sealed file and current confirmation." });
167
+ }
168
+ if (value.draft?.kind === "import_local_data" && value.restoreCheck?.status !== "can_restore" && value.allowedActions.includes("settings_confirm")) {
169
+ context.addIssue({ code: "custom", message: "An unchecked or blocked import cannot offer confirmation." });
170
+ }
171
+ if (value.restoreCheck?.status === "receiving" && (!value.file || value.file.sealed || !["draft", "uploading"].includes(value.phase))) {
172
+ context.addIssue({ code: "custom", message: "Receiving requires the selected unsealed upload." });
173
+ }
174
+ if (value.restoreCheck?.status === "check_required" && (!value.file?.sealed || value.phase !== "sealed")) {
175
+ context.addIssue({ code: "custom", message: "A new check requires the same complete sealed upload." });
176
+ }
177
+ if (value.restoreCheck?.status === "cannot_restore" && value.restoreCheck.reason === "invalid_backup" &&
178
+ (value.phase === "prepared" || value.allowedActions.includes("settings_prepare"))) {
179
+ context.addIssue({ code: "custom", message: "A rejected backup requires an explicit new selection." });
180
+ }
181
+ if (value.backupLocation && !(value.result?.outcome === "completed" && (value.draft?.kind === "export_local_data" ||
182
+ value.draft?.kind === "download_backup" && value.download?.localSave?.status === "saved" && value.download.endReason === "stored_locally"))) {
183
+ context.addIssue({ code: "custom", message: "A backup location requires its recorded successful export." });
184
+ }
185
+ if (value.download?.localSave?.status === "saved" && !value.backupLocation) {
186
+ context.addIssue({ code: "custom", message: "A saved backup must identify its verified location." });
187
+ }
188
+ if (value.draft?.kind === "export_local_data" && value.result?.outcome === "completed" &&
189
+ typeof value.result.details?.path === "string" && !value.backupLocation) {
190
+ context.addIssue({ code: "custom", message: "A recorded backup path must provide its folder and file name." });
191
+ }
192
+ if (value.temporaryFileCleanup && !value.result)
193
+ context.addIssue({ code: "custom", message: "Cleanup of an applied or ended operation requires its recorded result." });
194
+ if ((value.phase === "terminal") !== (value.result !== undefined) || value.phase === "terminal" && (value.observe || value.allowedActions.length)) {
195
+ context.addIssue({ code: "custom", message: "A terminal Settings result cannot grant input or observation." });
196
+ }
197
+ if (value.phase === "prepared" && (!value.preparationId || !value.draft || !value.preparation))
198
+ context.addIssue({ code: "custom", message: "Settings confirmation requires a prepared identity and facts." });
199
+ if (value.phase === "prepared" && value.draft?.kind === "import_local_data" && !importPreview.safeParse(value.preparation?.preview).success) {
200
+ context.addIssue({ code: "custom", path: ["preparation", "preview"], message: "Import confirmation requires the validated current and incoming read context and record counts." });
201
+ }
202
+ if (value.phase === "prepared" && (value.draft?.kind === "set_endpoint" || value.draft?.kind === "restore_endpoint")) {
203
+ const target = endpointPreparationSchema.safeParse(value.preparation?.endpoint);
204
+ if (!target.success || target.data.endpoint !== value.draft.endpoint ||
205
+ target.data.status !== (value.draft.kind === "set_endpoint" ? "saved" : "reset") ||
206
+ value.draft.kind === "set_endpoint" && target.data.value !== value.draft.value) {
207
+ context.addIssue({ code: "custom", path: ["preparation", "endpoint"], message: "Endpoint confirmation must name its exact prepared target." });
208
+ }
209
+ }
210
+ if (value.file && value.file.receivedSize > value.file.totalSize)
211
+ context.addIssue({ code: "custom", message: "Received content exceeds the selected file." });
212
+ });
213
+ export function settingsDownload(operation) {
214
+ if (operation.draft?.kind !== "download_backup")
215
+ return undefined;
216
+ const download = storedSettingsDownloadSchema.parse(operation.preparation?.download), save = download.localSave;
217
+ if (save && (save.fileId !== operation.fileId || save.destination !== `backups/${operation.operationId}.json`)) {
218
+ throw new SettingsConflict("The local save does not identify this request's backup.");
219
+ }
220
+ return download;
221
+ }
222
+ export function isSettingsDownloadFileAction(action) {
223
+ return settingsDownloadFileActionSchema.safeParse(action.action).success;
224
+ }
@@ -0,0 +1,15 @@
1
+ // The trigger and explicit Back use the same stored preparation transition.
2
+ // A sealed upload is separate from the short-lived confirmation of its impact.
3
+ export const SETTINGS_REUSABLE_UPLOAD_SQL = `
4
+ kind='import_local_data' AND EXISTS (
5
+ SELECT 1 FROM settings_files f
6
+ WHERE f.file_id=settings_operations.file_id
7
+ AND f.operation_id=settings_operations.operation_id AND f.role='upload'
8
+ AND f.sealed=1 AND f.invalidated=0 AND f.received_size=f.total_size
9
+ AND f.reservation_json IS NULL
10
+ )`;
11
+ export const SETTINGS_RESET_PREPARATION_SQL = `
12
+ UPDATE settings_operations SET
13
+ phase=CASE WHEN ${SETTINGS_REUSABLE_UPLOAD_SQL} THEN 'sealed' ELSE 'draft' END,
14
+ preparation_id=NULL,fingerprint=NULL,preparation_json=NULL,
15
+ prepared_user_data_revision=NULL`;