@zktx.io/sui-mcp 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -42
- package/dist/core/activity/localDataService.js +8 -21
- package/dist/core/activity/localDataValidation.js +22 -46
- package/dist/core/activity/sqliteAccountStore.js +46 -0
- package/dist/core/activity/sqliteActivityStore.js +65 -147
- package/dist/core/activity/sqliteActivityStoreSchema.js +42 -26
- package/dist/core/activity/transactionActivityService.js +2 -2
- package/dist/core/preferences/preferencesStore.js +2 -2
- package/dist/core/preferences/sqlitePreferencesRepository.js +10 -0
- package/dist/core/session/cardResult.js +41 -0
- package/dist/core/session/cardSession.js +1 -1
- package/dist/core/session/cardSessionStore.js +51 -19
- package/dist/core/session/chainAccounts.js +120 -0
- package/dist/core/session/sessionStore.js +4 -8
- package/dist/core/session/settingsFileStore.js +3 -30
- package/dist/core/session/settingsOperation.js +27 -19
- package/dist/core/session/settingsWorkflow.js +161 -87
- package/dist/core/session/sqliteCardStore.js +34 -14
- package/dist/core/session/sqliteSessionStore.js +5 -5
- package/dist/core/session/sqliteSettingsWorkflowStore.js +135 -41
- package/dist/core/session/sqliteTransactionRequest.js +18 -0
- package/dist/core/session/sqliteWalletConnectionOperation.js +13 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +569 -333
- package/dist/core/session/status.js +2 -2
- package/dist/core/session/wait.js +6 -11
- package/dist/core/session/walletConnection.js +71 -46
- package/dist/core/session/walletConnectionOperations.js +24 -0
- package/dist/core/session/walletRuntime.js +7 -13
- package/dist/core/session/walletWorkflow.js +248 -99
- package/dist/core/session/workflowState.js +104 -38
- package/dist/core/session/workflowView.js +92 -101
- package/dist/mcp/activeAccountResponse.js +3 -0
- package/dist/mcp/responseGuidance.js +7 -9
- package/dist/mcp/serverInfo.js +11 -10
- package/dist/mcp/toolErrors.js +5 -3
- package/dist/mcp/toolNames.js +1 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +9 -16
- package/dist/mcp/tools/read/readToolHelpers.js +9 -7
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +2 -2
- package/dist/mcp/tools/session/statusTools.js +12 -10
- package/dist/mcp/tools/session/walletConnectionTools.js +26 -14
- package/dist/mcp/tools/settings/index.js +7 -4
- package/dist/mcp-app/account.html +7 -7
- package/dist/mcp-app/chart.html +3 -3
- package/dist/mcp-app/connect.html +13 -13
- package/dist/mcp-app/disconnect.html +3637 -0
- package/dist/mcp-app/disconnect.notices.txt +1480 -0
- package/dist/mcp-app/receipt.html +393 -393
- package/dist/mcp-app/review.html +459 -459
- package/dist/mcp-app/settings.html +7 -7
- package/dist/mcp-ui/contracts.js +3 -1
- package/dist/mcp-ui/tools.js +4 -4
- package/dist/runtime/application.js +1 -1
- package/dist/runtime/localSettingsService.js +7 -4
- package/dist/runtime/smokeMainnetRead.js +5 -4
- package/dist/runtime/walletConnectTransport.js +16 -16
- package/dist/runtime/walletSdkChildRuntime.js +29 -12
- package/dist/runtime/walletSdkIpc.js +5 -4
- package/dist/runtime/walletSdkProcess.js +24 -8
- package/docs/AGENT_BEHAVIOR.md +82 -54
- package/docs/LOCAL_DB_ARCHITECTURE.md +100 -70
- package/docs/MCP_SETUP.md +319 -339
- package/docs/MCP_TOOLS.md +292 -177
- package/docs/SDK_API.md +15 -6
- package/docs/TRANSACTION_ACTIVITY_LOG.md +4 -0
- package/docs/UTILITY_INDEX.md +9 -7
- package/docs/WALLET_CONNECTION.md +162 -167
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { assertNoForbiddenMcpFields } from "../action/forbiddenFields.js";
|
|
2
|
+
import { closedWalletCardSchema, closedReviewCardSchema, REVIEW_BOUNDARY } from "./workflowView.js";
|
|
3
|
+
import { fixedReviewTarget } from "./workflowState.js";
|
|
4
|
+
export function walletCardResult(record, at, input) {
|
|
5
|
+
return closedWalletCardSchema.parse({ kind: record.state.kind, allowedActions: [], observe: false, actionRemainingMs: 0,
|
|
6
|
+
result: { message: input.message, recordedAt: at.toISOString(), ...(input.pendingWork ? { pendingWork: input.pendingWork } : {}) },
|
|
7
|
+
...(input.connection?.walletName ? { walletName: input.connection.walletName } : {}),
|
|
8
|
+
networks: (input.connection?.chainResults ?? []).map(({ chainId, status }) => ({ chainId, status })) });
|
|
9
|
+
}
|
|
10
|
+
export function reviewCardResult(record, at, input) {
|
|
11
|
+
const { session, request, state } = input;
|
|
12
|
+
if (request && request.reviewSessionId !== record.state.input.reviewSessionId)
|
|
13
|
+
throw new Error("The request does not belong to this card.");
|
|
14
|
+
const target = session && !session.plans[0]?.reviewModel ? fixedReviewTarget(session, record.state.input) : undefined;
|
|
15
|
+
const mode = record.scope === "review_manage" ? "review_manage" : "review";
|
|
16
|
+
const message = record.state.error ?? (request?.execution ? request.execution.status === "success" ? "Transaction succeeded on Sui." : "Transaction failed on Sui."
|
|
17
|
+
: mode === "review_manage" && record.state.reason === "expired" ? "This result lookup expired. Ask in chat for a new result card for the same transaction."
|
|
18
|
+
: input.observationStopped && request && request.requestStatus !== "stopped" ? "Result checking was stopped. This does not cancel the transaction. Ask in chat for a new result card for the same transaction."
|
|
19
|
+
: request ? request.reason ?? "No chain execution result has been confirmed."
|
|
20
|
+
: session?.plans[0]?.reviewModel ? "This proposal review has ended. It provided no wallet approval or execution."
|
|
21
|
+
: record.state.reason === "cancelled" ? "This review was cancelled. No approval request was sent."
|
|
22
|
+
: record.state.reason === "expired" ? "This review expired. Ask in chat for a new transaction review."
|
|
23
|
+
: record.state.reason === "server_restarted" ? "This review ended when the server restarted. Ask in chat for a new transaction review."
|
|
24
|
+
: "This review has ended.");
|
|
25
|
+
const gas = state?.simulation?.gasCostSummary;
|
|
26
|
+
const result = closedReviewCardSchema.parse({ kind: "review", mode, allowedActions: [], observe: false, actionRemainingMs: 0,
|
|
27
|
+
result: { message, recordedAt: at.toISOString() }, boundary: REVIEW_BOUNDARY,
|
|
28
|
+
...(target ? { reviewTarget: target } : {}),
|
|
29
|
+
...(session?.plans[0] ? { review: { reviewSessionId: session.id, plan: session.plans[0], reviewRevision: request?.reviewRevision ?? session.reviewRevision,
|
|
30
|
+
status: session.status === "expired" ? "expired" : state?.status ?? session.status, account: state?.account ?? session.account, preparing: false,
|
|
31
|
+
...(state ? { state } : {}), ...(session.preparationError ? { error: session.preparationError } : {}) } } : {}),
|
|
32
|
+
...(request ? { request } : {}), ...(input.observationStopped ? { observationStopped: true } : {}),
|
|
33
|
+
...(input.receipt === undefined ? {} : { receipt: input.receipt }),
|
|
34
|
+
...(gas ? { netGasMist: (BigInt(gas.computationCostRaw) + BigInt(gas.storageCostRaw) - BigInt(gas.storageRebateRaw)).toString() } : {}) });
|
|
35
|
+
assertNoForbiddenMcpFields(result);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
export function endedCardMessage(reason) {
|
|
39
|
+
return reason === "cancelled" ? "This card was cancelled." : reason === "expired" ? "The input period ended."
|
|
40
|
+
: reason === "server_restarted" ? "This card ended when the server restarted." : "This card has ended.";
|
|
41
|
+
}
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
export const walletDisplaySchema = z.object({
|
|
3
3
|
connectionId: z.string().min(1), pairingUri: z.string().startsWith("wc:"), expiresAt: z.string().datetime()
|
|
4
4
|
}).strict();
|
|
5
|
-
export const cardKindSchema = z.enum(["account", "receipt", "chart", "connect", "review", "settings"]);
|
|
5
|
+
export const cardKindSchema = z.enum(["account", "receipt", "chart", "connect", "disconnect", "review", "settings"]);
|
|
6
6
|
export const cardStateSchema = z.object({
|
|
7
7
|
cardId: z.string().min(1), kind: cardKindSchema,
|
|
8
8
|
state: z.enum(["ready", "running", "closed"]),
|
|
@@ -29,11 +29,13 @@ export class CardStore {
|
|
|
29
29
|
}
|
|
30
30
|
async create(kind, input, execute = false) {
|
|
31
31
|
this.assertCurrent();
|
|
32
|
-
const business = kind === "connect" || kind === "review";
|
|
33
|
-
if (kind === "settings" && (!this.options.settings || execute
|
|
32
|
+
const business = kind === "connect" || kind === "disconnect" || kind === "review";
|
|
33
|
+
if (kind === "settings" && (!this.options.settings || execute))
|
|
34
34
|
throw new CardError("Settings cards are unavailable for this input.");
|
|
35
35
|
if (business && (!this.options.workflow || execute))
|
|
36
36
|
throw new CardError("This type of card is unavailable.");
|
|
37
|
+
if (kind === "connect" || kind === "disconnect")
|
|
38
|
+
input = await this.options.workflow.connectionCardInput(kind, input);
|
|
37
39
|
const parsed = execute ? parseCardInput(kind, input) : undefined;
|
|
38
40
|
let preparation = { status: "ready" };
|
|
39
41
|
if (business) {
|
|
@@ -51,23 +53,30 @@ export class CardStore {
|
|
|
51
53
|
}
|
|
52
54
|
assertNoForbiddenMcpFields({ input, preparation });
|
|
53
55
|
const { base, token } = createLocalSessionBase(this.now(), DEFAULT_SESSION_TTL_MS);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
const settingsTarget = input.operationStatus ?? input.backupDelivery;
|
|
57
|
+
const settingsOperationId = settingsTarget && typeof settingsTarget === "object" && "operationId" in settingsTarget ? String(settingsTarget.operationId) : randomUUID();
|
|
58
|
+
let record = { ownerId: this.options.ownerId, tokenHash: base.tokenHash,
|
|
59
|
+
scope: kind === "settings" ? "settings" : kind === "connect" || kind === "disconnect" ? kind : kind === "review" ? input.mode === "manage" ? "review_manage" : "review" : "read",
|
|
60
|
+
...(kind === "settings" ? { operationId: settingsOperationId } : {}),
|
|
61
|
+
...((kind === "connect" || kind === "disconnect") ? { operationId: String(input.operationId) } : {}),
|
|
57
62
|
...(kind === "review" && input.mode === "manage" && typeof input.attemptId === "string" ? { operationId: input.attemptId } : {}),
|
|
58
|
-
state: { cardId: base.id, kind, state: preparation.status
|
|
63
|
+
state: { cardId: base.id, kind, state: preparation.status !== "ready" ? "closed" : "ready", revision: 0,
|
|
59
64
|
createdAt: base.createdAt, expiresAt: base.expiresAt, input,
|
|
60
|
-
...(preparation.status === "failed" ? { reason: "failed", error: preparation.error } :
|
|
65
|
+
...(preparation.status === "failed" ? { reason: "failed", error: preparation.error } : preparation.status === "completed" ? { reason: "completed" } :
|
|
61
66
|
preparation.data === undefined ? {} : { data: preparation.data }) } };
|
|
67
|
+
if (business && record.state.state === "closed")
|
|
68
|
+
record = this.options.workflow.terminalCard(record);
|
|
62
69
|
if (kind === "settings")
|
|
63
|
-
this.options.settings.create(record);
|
|
70
|
+
await this.options.settings.create(record);
|
|
64
71
|
else
|
|
65
72
|
this.options.records.create(record);
|
|
73
|
+
record = this.current(base.id);
|
|
66
74
|
if (preparation.status === "failed")
|
|
67
75
|
this.reportFailure(record, "input_preparation");
|
|
68
|
-
const result =
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
const result = kind === "connect" || kind === "disconnect" ? { snapshot: (await this.readWalletOperation(base.id)).card }
|
|
77
|
+
: parsed === undefined ? await this.describe(record, false) : await this.submit({
|
|
78
|
+
cardId: base.id, permission: token, revision: 0, input: parsed
|
|
79
|
+
});
|
|
71
80
|
return { ...result, permission: token };
|
|
72
81
|
}
|
|
73
82
|
async read(input) { return this.describe(this.require(input), true); }
|
|
@@ -80,16 +89,27 @@ export class CardStore {
|
|
|
80
89
|
return this.options.settings.readFile(record, input.fileId, input.offset, limit, input.attemptId);
|
|
81
90
|
}
|
|
82
91
|
async readSaved(cardId) { return (await this.describe(this.current(cardId), false)).snapshot; }
|
|
92
|
+
async readWalletOperation(cardId) {
|
|
93
|
+
if (!this.options.workflow)
|
|
94
|
+
throw new CardError("Wallet operations are unavailable.");
|
|
95
|
+
const result = this.options.workflow.readOperation(this.current(cardId));
|
|
96
|
+
return { card: this.snapshot(result.data ? { ...result.record, state: { ...result.record.state, data: result.data } } : result.record, result.evaluatedAt), operation: result.operation,
|
|
97
|
+
walletAvailability: result.walletAvailability, evaluatedAt: result.evaluatedAt };
|
|
98
|
+
}
|
|
83
99
|
async describe(record, uiObservation) {
|
|
100
|
+
if (record.state.state === "closed")
|
|
101
|
+
return this.response(record);
|
|
84
102
|
if (record.state.kind === "settings") {
|
|
85
103
|
if (!this.options.settings)
|
|
86
104
|
throw new CardError("Settings cards are unavailable.");
|
|
87
105
|
const described = await this.options.settings.describe(record);
|
|
106
|
+
if (described.record.state.state === "closed")
|
|
107
|
+
return this.response(described.record);
|
|
88
108
|
return { snapshot: cardSnapshotSchema.parse({ ...described.record.state, data: described.data,
|
|
89
109
|
inputRemainingMs: described.record.state.state === "ready" ? Math.max(0, Date.parse(described.record.state.expiresAt) - this.now().getTime()) : 0,
|
|
90
110
|
pollAfterMs: EXECUTION_POLLING_INTERVAL_SECONDS * 1000 }) };
|
|
91
111
|
}
|
|
92
|
-
if (record.state.kind !== "connect" && record.state.kind !== "review")
|
|
112
|
+
if (record.state.kind !== "connect" && record.state.kind !== "disconnect" && record.state.kind !== "review")
|
|
93
113
|
return this.response(record);
|
|
94
114
|
if (!this.options.workflow)
|
|
95
115
|
throw new CardError("Wallet actions are unavailable.");
|
|
@@ -97,6 +117,8 @@ export class CardStore {
|
|
|
97
117
|
const current = view.evaluated.record;
|
|
98
118
|
if (!current)
|
|
99
119
|
throw new CardError("Saved card data is unavailable.");
|
|
120
|
+
if (current.state.state === "closed")
|
|
121
|
+
return this.response(current);
|
|
100
122
|
const snapshot = cardSnapshotSchema.parse({ ...current.state, data: view.data,
|
|
101
123
|
inputRemainingMs: view.evaluated.inputRemainingMs,
|
|
102
124
|
pollAfterMs: (current.state.kind === "review" ? EXECUTION_POLLING_INTERVAL_SECONDS : WALLET_CONNECTION_POLL_SECONDS) * 1000 });
|
|
@@ -106,13 +128,20 @@ export class CardStore {
|
|
|
106
128
|
}
|
|
107
129
|
async act(input) {
|
|
108
130
|
const record = this.require(input);
|
|
131
|
+
if (record.state.state === "closed" && input.input.action !== "settings_report_download") {
|
|
132
|
+
const normalized = record.state.kind === "settings" ? settingsActionSchema.safeParse(input.input) : undefined;
|
|
133
|
+
if (isDeepStrictEqual(record.acceptedInput, normalized?.success ? normalized.data : input.input) || record.state.reason === "cancelled" &&
|
|
134
|
+
(input.input.action === "stop_waiting" || input.input.action === "cancel" || input.input.action === "stop_connection" && input.input.operationId === record.operationId))
|
|
135
|
+
return this.response(record);
|
|
136
|
+
return { ...this.response(record), error: { code: "card_conflict", message: "This card has ended. Ask in chat for a new request." } };
|
|
137
|
+
}
|
|
109
138
|
if (record.state.kind === "settings") {
|
|
110
139
|
if (!this.options.settings)
|
|
111
140
|
throw new CardError("Settings cards are unavailable.");
|
|
112
141
|
const parsed = settingsActionSchema.safeParse(input.input);
|
|
113
142
|
if (!parsed.success)
|
|
114
143
|
return { ...await this.describe(record, false), error: { code: "invalid_card_input", message: "This Settings action is not valid." } };
|
|
115
|
-
if ((parsed.data.action === "settings_confirm" || parsed.data.action === "settings_download_backup") && record.acceptedInput !== undefined && isDeepStrictEqual(record.acceptedInput, parsed.data))
|
|
144
|
+
if ((parsed.data.action === "settings_confirm" || parsed.data.action === "settings_download_backup" || parsed.data.action === "settings_save_wallet_networks" || parsed.data.action === "settings_clear_read_address") && record.acceptedInput !== undefined && isDeepStrictEqual(record.acceptedInput, parsed.data))
|
|
116
145
|
return this.describe(record, false);
|
|
117
146
|
if (input.revision !== record.state.revision && !isSettingsDownloadFileAction(parsed.data))
|
|
118
147
|
return { ...await this.describe(record, false), error: { code: "card_conflict", message: "This Settings selection changed. Review the current selection before continuing." } };
|
|
@@ -131,7 +160,7 @@ export class CardStore {
|
|
|
131
160
|
}
|
|
132
161
|
return this.describe(this.current(input.cardId), false);
|
|
133
162
|
}
|
|
134
|
-
if ((record.state.kind !== "connect" && record.state.kind !== "review") || !this.options.workflow) {
|
|
163
|
+
if ((record.state.kind !== "connect" && record.state.kind !== "disconnect" && record.state.kind !== "review") || !this.options.workflow) {
|
|
135
164
|
return this.conflict(record, "This card does not accept wallet workflow actions.");
|
|
136
165
|
}
|
|
137
166
|
let parsed;
|
|
@@ -156,11 +185,11 @@ export class CardStore {
|
|
|
156
185
|
code: error instanceof WalletUnavailableError ? "wallet_unavailable" : "card_conflict", message: error.message
|
|
157
186
|
} };
|
|
158
187
|
}
|
|
159
|
-
return this.describe(this.current(input.cardId),
|
|
188
|
+
return this.describe(this.current(input.cardId), true);
|
|
160
189
|
}
|
|
161
190
|
async submit(input) {
|
|
162
191
|
const record = this.require(input);
|
|
163
|
-
if (record.state.kind === "connect" || record.state.kind === "review" || record.state.kind === "settings")
|
|
192
|
+
if (record.state.kind === "connect" || record.state.kind === "disconnect" || record.state.kind === "review" || record.state.kind === "settings")
|
|
164
193
|
throw new CardError("This card does not accept data selections.");
|
|
165
194
|
let parsed;
|
|
166
195
|
try {
|
|
@@ -198,7 +227,7 @@ export class CardStore {
|
|
|
198
227
|
const complete = { ...admitted, ...("receiptDisplay" in projected ? { receiptDisplay: projected.receiptDisplay } : {}),
|
|
199
228
|
state: { ...admitted.state, data: projected.data, state: "closed", reason: "completed", revision: admitted.state.revision + 1 } };
|
|
200
229
|
try {
|
|
201
|
-
this.options.records.replace(admitted, complete);
|
|
230
|
+
this.options.records.replace(admitted, complete, this.now());
|
|
202
231
|
}
|
|
203
232
|
catch {
|
|
204
233
|
return this.fail(admitted, "result_storage", "The result could not be saved. This request will not be repeated automatically.");
|
|
@@ -210,7 +239,7 @@ export class CardStore {
|
|
|
210
239
|
this.assertCurrent();
|
|
211
240
|
this.reportFailure(record, stage);
|
|
212
241
|
this.options.records.replace(record, { ...record, state: { ...record.state,
|
|
213
|
-
state: "closed", reason: "failed", error: message, revision: record.state.revision + 1 } });
|
|
242
|
+
state: "closed", reason: "failed", error: message, revision: record.state.revision + 1 } }, this.now());
|
|
214
243
|
return this.response(this.current(record.state.cardId));
|
|
215
244
|
}
|
|
216
245
|
reportFailure(record, stage) {
|
|
@@ -244,8 +273,11 @@ export class CardStore {
|
|
|
244
273
|
return snapshot;
|
|
245
274
|
}
|
|
246
275
|
response(record) {
|
|
276
|
+
if (["connect", "disconnect", "review"].includes(record.state.kind) && record.state.state !== "closed")
|
|
277
|
+
record = this.options.workflow.expireCard(record);
|
|
247
278
|
const evaluated = this.options.records.evaluate(record, () => this.now());
|
|
248
279
|
record = evaluated.record;
|
|
249
|
-
|
|
280
|
+
const receipt = record.state.kind === "review" && record.state.state === "closed" ? this.options.workflow?.closedReceiptDisplay(record) : undefined;
|
|
281
|
+
return { ...(receipt ?? {}), snapshot: this.snapshot(record, evaluated.evaluatedAt), ...(record.receiptDisplay === undefined ? {} : { receiptDisplay: record.receiptDisplay }) };
|
|
250
282
|
}
|
|
251
283
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { fromBase58 } from "@mysten/sui/utils";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { parseSuiAddress } from "../suiAddress.js";
|
|
4
|
+
export const WALLET_CHAINS = ["sui:mainnet", "eip155:1", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"];
|
|
5
|
+
export const SUI_MAINNET_WALLET_CHAIN = WALLET_CHAINS[0];
|
|
6
|
+
export const walletChainSchema = z.enum(WALLET_CHAINS);
|
|
7
|
+
export const walletGrantRevisionsSchema = z.record(walletChainSchema, z.number().int().nonnegative());
|
|
8
|
+
export function initialGrantRevisions() {
|
|
9
|
+
return Object.fromEntries(WALLET_CHAINS.map((chain) => [chain, 0]));
|
|
10
|
+
}
|
|
11
|
+
export const walletChainsSchema = z.array(walletChainSchema).refine((chains) => new Set(chains).size === chains.length, "Networks must be distinct");
|
|
12
|
+
export function parseWalletChains(value) {
|
|
13
|
+
const selected = new Set(walletChainsSchema.parse(value));
|
|
14
|
+
return WALLET_CHAINS.filter((chain) => selected.has(chain));
|
|
15
|
+
}
|
|
16
|
+
export const DEFAULT_WALLET_CHAINS = [SUI_MAINNET_WALLET_CHAIN];
|
|
17
|
+
export const WALLET_CHAIN_LABELS = {
|
|
18
|
+
"sui:mainnet": "Sui", "eip155:1": "Ethereum", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": "Solana"
|
|
19
|
+
};
|
|
20
|
+
export function normalizeChainAddress(chainId, value) {
|
|
21
|
+
if (chainId === SUI_MAINNET_WALLET_CHAIN)
|
|
22
|
+
return parseSuiAddress(value);
|
|
23
|
+
if (chainId === "eip155:1")
|
|
24
|
+
return /^0x[0-9a-fA-F]{40}$/.test(value) ? value.toLowerCase() : undefined;
|
|
25
|
+
try {
|
|
26
|
+
return fromBase58(value).length === 32 ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export const chainAccountSchema = z.object({ chainId: walletChainSchema, address: z.string() }).strict()
|
|
33
|
+
.refine((value) => normalizeChainAddress(value.chainId, value.address) === value.address, "Expected a normalized chain address");
|
|
34
|
+
export const walletGrantAccountSchema = z.object({ address: z.string(), displayAddress: z.string().optional(),
|
|
35
|
+
methods: z.array(z.string()), events: z.array(z.string()) }).strict();
|
|
36
|
+
export const walletGrantSchema = z.object({ chainId: walletChainSchema, accounts: z.array(walletGrantAccountSchema),
|
|
37
|
+
invalidAccounts: z.literal(true).optional() }).strict().superRefine((grant, context) => {
|
|
38
|
+
if (new Set(grant.accounts.map((a) => a.address)).size !== grant.accounts.length ||
|
|
39
|
+
grant.invalidAccounts && grant.accounts.length || grant.accounts.some((account) => normalizeChainAddress(grant.chainId, account.address) !== account.address ||
|
|
40
|
+
account.displayAddress !== undefined && normalizeChainAddress(grant.chainId, account.displayAddress) !== account.address)) {
|
|
41
|
+
context.addIssue({ code: "custom", message: "Wallet grant accounts must match their chain" });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
export const walletGrantsSchema = z.array(walletGrantSchema).refine((grants) => new Set(grants.map((grant) => grant.chainId)).size === grants.length, "Wallet grants must have distinct chains");
|
|
45
|
+
export const walletChainResultSchema = z.discriminatedUnion("status", [
|
|
46
|
+
chainAccountSchema.safeExtend({ status: z.literal("bound") }).strict(),
|
|
47
|
+
z.object({ chainId: walletChainSchema, status: z.enum(["not_approved", "multiple_accounts", "invalid_response"]) }).strict()
|
|
48
|
+
]);
|
|
49
|
+
export const walletChainResultsSchema = z.array(walletChainResultSchema).refine((results) => new Set(results.map((result) => result.chainId)).size === results.length, "One result per requested chain is required");
|
|
50
|
+
export function fixedChainAccount(connection, chainId) {
|
|
51
|
+
const result = connection.chainResults.find((item) => item.chainId === chainId);
|
|
52
|
+
return result?.status === "bound" ? result.address : undefined;
|
|
53
|
+
}
|
|
54
|
+
export function approvedChainResults(chains, grants) {
|
|
55
|
+
return WALLET_CHAINS.filter((chain) => chains.includes(chain)).map((chainId) => {
|
|
56
|
+
const grant = grants.find((item) => item.chainId === chainId);
|
|
57
|
+
if (grant?.invalidAccounts)
|
|
58
|
+
return { chainId, status: "invalid_response" };
|
|
59
|
+
if (!grant?.accounts.length)
|
|
60
|
+
return { chainId, status: "not_approved" };
|
|
61
|
+
if (grant.accounts.length !== 1)
|
|
62
|
+
return { chainId, status: "multiple_accounts" };
|
|
63
|
+
return { chainId, status: "bound", address: grant.accounts[0].address };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const namespaceSchema = z.object({ accounts: z.array(z.string()), methods: z.array(z.string()), events: z.array(z.string()) });
|
|
67
|
+
export function normalizeWalletGrants(namespaces) {
|
|
68
|
+
const chains = new Map();
|
|
69
|
+
for (const [namespace, value] of Object.entries(namespaces)) {
|
|
70
|
+
const allowed = WALLET_CHAINS.filter((chain) => namespace === chain || namespace === chain.split(":")[0]);
|
|
71
|
+
if (!allowed.length)
|
|
72
|
+
continue;
|
|
73
|
+
const parsed = namespaceSchema.safeParse(value);
|
|
74
|
+
const entry = (chain) => {
|
|
75
|
+
let result = chains.get(chain);
|
|
76
|
+
if (!result) {
|
|
77
|
+
result = { invalid: false, accounts: new Map() };
|
|
78
|
+
chains.set(chain, result);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
if (!parsed.success) {
|
|
83
|
+
for (const chain of allowed)
|
|
84
|
+
entry(chain).invalid = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const caipAccount of parsed.data.accounts) {
|
|
88
|
+
const chain = allowed.find((id) => caipAccount.startsWith(`${id}:`));
|
|
89
|
+
if (!chain) {
|
|
90
|
+
if (!/^[^:]+:[^:]+:.+$/.test(caipAccount))
|
|
91
|
+
for (const id of allowed)
|
|
92
|
+
entry(id).invalid = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const rawAddress = caipAccount.slice(chain.length + 1), address = normalizeChainAddress(chain, rawAddress);
|
|
96
|
+
const grant = entry(chain);
|
|
97
|
+
if (!address) {
|
|
98
|
+
grant.invalid = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const prior = grant.accounts.get(address);
|
|
102
|
+
grant.accounts.set(address, { address,
|
|
103
|
+
...(rawAddress === address ? prior?.displayAddress ? { displayAddress: prior.displayAddress } : {} : { displayAddress: rawAddress }),
|
|
104
|
+
methods: [...new Set([...(prior?.methods ?? []), ...parsed.data.methods])].sort(),
|
|
105
|
+
events: [...new Set([...(prior?.events ?? []), ...parsed.data.events])].sort() });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return WALLET_CHAINS.flatMap((chainId) => {
|
|
109
|
+
const grant = chains.get(chainId);
|
|
110
|
+
return grant ? [{ chainId, accounts: grant.invalid ? [] : [...grant.accounts.values()].sort((a, b) => a.address.localeCompare(b.address)),
|
|
111
|
+
...(grant.invalid ? { invalidAccounts: true } : {}) }] : [];
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function walletGrantAccount(grants, chainId, address) {
|
|
115
|
+
return grants.find((grant) => grant.chainId === chainId)?.accounts.find((account) => account.address === address);
|
|
116
|
+
}
|
|
117
|
+
export function comparableGrantScope(grant) {
|
|
118
|
+
return grant && { chainId: grant.chainId, invalidAccounts: grant.invalidAccounts,
|
|
119
|
+
accounts: grant.accounts.map(({ address, methods, events }) => ({ address, methods, events })) };
|
|
120
|
+
}
|
|
@@ -188,17 +188,13 @@ export class LocalSessionStore {
|
|
|
188
188
|
if (!normalizedAccount) {
|
|
189
189
|
throw new SessionStoreError("input_invalid", "Invalid wallet account address");
|
|
190
190
|
}
|
|
191
|
-
const activeAccount = await this.activityStore.getActiveAccount();
|
|
192
|
-
if (!activeAccount) {
|
|
193
|
-
throw new SessionStoreError("active_account_not_set", "Review account binding requires an active read account");
|
|
194
|
-
}
|
|
195
|
-
if (activeAccount.address !== normalizedAccount) {
|
|
196
|
-
throw new SessionStoreError("invalid_session_transition", `Review account does not match active read account: ${id}`);
|
|
197
|
-
}
|
|
198
191
|
const nextSession = cloneLocalSession(session);
|
|
199
192
|
if (session.account && session.account !== normalizedAccount) {
|
|
200
193
|
throw new SessionStoreError("invalid_session_transition", `Review session already bound to a different account: ${id}`);
|
|
201
194
|
}
|
|
195
|
+
const knownAccount = await this.activityStore.getKnownAccount(normalizedAccount);
|
|
196
|
+
if (!knownAccount)
|
|
197
|
+
throw new SessionStoreError("active_account_not_set", "Review account binding requires a known Sui account");
|
|
202
198
|
if (nextSession.status === "proposed") {
|
|
203
199
|
transition(nextSession, "awaiting_wallet");
|
|
204
200
|
transition(nextSession, "wallet_connected");
|
|
@@ -239,7 +235,7 @@ export class LocalSessionStore {
|
|
|
239
235
|
async recordReviewStateInternal(id, state, privateArtifacts, now, preparation) {
|
|
240
236
|
const session = await this.requireMutableSession(id, now);
|
|
241
237
|
if (preparation && (session.preparationId !== preparation.id || session.walletConnectionId !== preparation.connectionId ||
|
|
242
|
-
session.
|
|
238
|
+
session.walletGrantRevision !== preparation.grantRevision))
|
|
243
239
|
this.throwStaleReviewSession(id);
|
|
244
240
|
assertSameSessionId(id, state.reviewSessionId, "Review state");
|
|
245
241
|
assertPlanInSession(session, state.planId);
|
|
@@ -42,16 +42,11 @@ export class SettingsFileStore {
|
|
|
42
42
|
}
|
|
43
43
|
stagingPath(file) {
|
|
44
44
|
this.assertId(file.fileId);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
finalPath(file) {
|
|
48
|
-
this.assertId(file.operationId);
|
|
49
|
-
if (file.role !== "export" || file.relativePath !== `backups/${file.operationId}.json`) {
|
|
50
|
-
throw new SettingsConflict("The backup location does not match this request.");
|
|
45
|
+
if (file.role === "export" && !isSettingsDownloadFile(file)) {
|
|
46
|
+
throw new SettingsConflict("The private backup path does not match this request.");
|
|
51
47
|
}
|
|
52
|
-
return join(this.directory("
|
|
48
|
+
return join(this.directory("staging"), `${file.fileId}.${file.role === "upload" ? "upload" : "download"}`);
|
|
53
49
|
}
|
|
54
|
-
exportPath(file) { return this.finalPath(file); }
|
|
55
50
|
localBackupPath(file) {
|
|
56
51
|
this.assertId(file.operationId);
|
|
57
52
|
if (!isSettingsDownloadFile(file))
|
|
@@ -277,28 +272,6 @@ export class SettingsFileStore {
|
|
|
277
272
|
throw new SettingsConflict("This file is not the private download snapshot.");
|
|
278
273
|
return this.readVerifiedRange(file, this.directory("staging"), this.stagingPath(file), offset, length);
|
|
279
274
|
}
|
|
280
|
-
async verifyExport(file) {
|
|
281
|
-
const directory = this.directory("backups");
|
|
282
|
-
const info = await lstat(directory);
|
|
283
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
284
|
-
throw new SettingsConflict("The local backup directory changed.");
|
|
285
|
-
const handle = await this.handle(this.finalPath(file), constants.O_RDONLY);
|
|
286
|
-
try {
|
|
287
|
-
if ((await handle.stat()).size !== file.totalSize)
|
|
288
|
-
throw new SettingsConflict("The local backup length changed.");
|
|
289
|
-
const hash = createHash("sha256");
|
|
290
|
-
for await (const chunk of handle.createReadStream({ autoClose: false }))
|
|
291
|
-
hash.update(chunk);
|
|
292
|
-
if (hash.digest("hex") !== file.sha256)
|
|
293
|
-
throw new SettingsConflict("The local backup content changed.");
|
|
294
|
-
}
|
|
295
|
-
finally {
|
|
296
|
-
await handle.close();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
async readExportRange(file, offset, length) {
|
|
300
|
-
return this.readVerifiedRange(file, this.directory("backups"), this.finalPath(file), offset, length);
|
|
301
|
-
}
|
|
302
275
|
async readVerifiedRange(file, directory, path, offset, length) {
|
|
303
276
|
if (!file.sealed || file.invalidated || !Number.isSafeInteger(offset) || !Number.isSafeInteger(length) ||
|
|
304
277
|
offset < 0 || length < 1 || offset + length > file.totalSize)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { assetReadAccountSchema, walletAvailabilitySchema } from "./walletConnection.js";
|
|
3
3
|
import { LOCAL_DATA_EXPORT_FORMAT } from "../activity/localDataTypes.js";
|
|
4
|
-
import {
|
|
4
|
+
import { walletChainsSchema, parseWalletChains } from "./chainAccounts.js";
|
|
5
5
|
export const SETTINGS_IMPORT_MAX_SIZE = 16 * 1024 * 1024;
|
|
6
6
|
const identity = z.string().uuid();
|
|
7
7
|
const size = z.number().int().nonnegative().safe();
|
|
@@ -18,12 +18,22 @@ export const restoreCheckSchema = z.discriminatedUnion("status", [
|
|
|
18
18
|
]);
|
|
19
19
|
export const storedRestoreCheckSchema = z.object({ fileId: identity, sha256, totalSize: size, dataEpoch: size,
|
|
20
20
|
userDataRevision: size, check: restoreCheckSchema }).strict();
|
|
21
|
-
export const
|
|
21
|
+
export const settingsSelectionSchema = z.discriminatedUnion("kind", [
|
|
22
22
|
z.object({ kind: z.literal("set_endpoint"), endpoint: z.enum(["grpc", "graphql"]), value: z.string().min(1) }).strict(),
|
|
23
23
|
z.object({ kind: z.literal("restore_endpoint"), endpoint: z.enum(["grpc", "graphql"]) }).strict(),
|
|
24
|
-
z.object({ kind: z.enum(["
|
|
24
|
+
z.object({ kind: z.enum(["import_local_data", "reset_local_data", "restart_wallet_service"]) }).strict()
|
|
25
|
+
]);
|
|
26
|
+
export const settingsDraftSchema = z.discriminatedUnion("kind", [
|
|
27
|
+
z.object({ kind: z.literal("clear_read_address") }).strict(),
|
|
28
|
+
...settingsSelectionSchema.options,
|
|
29
|
+
z.object({ kind: z.literal("set_wallet_networks"), chains: walletChainsSchema }).strict(),
|
|
30
|
+
z.object({ kind: z.literal("download_backup") }).strict()
|
|
25
31
|
]);
|
|
26
|
-
export const
|
|
32
|
+
export const settingsCardInputSchema = z.object({
|
|
33
|
+
operationStatus: z.object({ operationId: z.string().uuid() }).strict().optional(),
|
|
34
|
+
backupDelivery: z.object({ operationId: z.string().uuid(), fileId: z.string().uuid() }).strict().optional()
|
|
35
|
+
}).strict().refine((input) => !(input.operationStatus && input.backupDelivery), "Choose exactly one card purpose.");
|
|
36
|
+
export const settingsDownloadReportSchema = z.enum(["acknowledged", "not_completed", "unconfirmed", "unsupported", "not_sent"]);
|
|
27
37
|
export const settingsDownloadFailureSchema = z.object({ method: z.literal("ui/download-file"), code: z.literal(-32601) }).strict();
|
|
28
38
|
export const settingsDownloadEndSchema = z.enum(["acknowledged", "cancelled", "expired", "server_restarted", "data_replaced", "source_unavailable", "stored_locally", "local_save_failed"]);
|
|
29
39
|
export const settingsFileIdentitySchema = z.object({ device: z.string().regex(/^\d+$/), inode: z.string().regex(/^\d+$/) }).strict();
|
|
@@ -31,7 +41,7 @@ const localSaveFailure = z.enum(["not_published", "target_conflict", "source_una
|
|
|
31
41
|
const storedLocalSaveSchema = z.object({ status: z.enum(["needs_consent", "saving", "saved", "failed"]),
|
|
32
42
|
attemptId: identity, fileId: identity, sha256, totalSize: size, destination: z.string(), ownerId: z.string().min(1),
|
|
33
43
|
currentSaveAttemptId: identity.optional(),
|
|
34
|
-
attempts: z.array(z.object({ saveAttemptId: identity, previousSaveAttemptId: identity.optional(),
|
|
44
|
+
attempts: z.array(z.object({ saveAttemptId: identity, cardId: z.string().min(1), previousSaveAttemptId: identity.optional(),
|
|
35
45
|
admittedAt: z.string().datetime(), allowPossibleCopy: z.boolean() }).strict()),
|
|
36
46
|
sourceIdentity: settingsFileIdentitySchema.optional(), savedAt: z.string().datetime().optional(), failure: localSaveFailure.optional()
|
|
37
47
|
}).strict().superRefine((value, context) => {
|
|
@@ -47,7 +57,7 @@ const storedLocalSaveSchema = z.object({ status: z.enum(["needs_consent", "savin
|
|
|
47
57
|
});
|
|
48
58
|
export const storedSettingsDownloadSchema = z.object({
|
|
49
59
|
currentAttemptId: identity,
|
|
50
|
-
attempts: z.array(z.object({ attemptId: identity, admittedAt: z.string().datetime(), previousAttemptId: identity.optional(),
|
|
60
|
+
attempts: z.array(z.object({ attemptId: identity, cardId: z.string().min(1), admittedAt: z.string().datetime(), previousAttemptId: identity.optional(),
|
|
51
61
|
hostReport: settingsDownloadReportSchema.optional(), failure: settingsDownloadFailureSchema.optional() }).strict()).min(1),
|
|
52
62
|
readyAt: z.string().datetime().optional(), expiresAt: z.string().datetime().optional(), endReason: settingsDownloadEndSchema.optional(),
|
|
53
63
|
localSave: storedLocalSaveSchema.optional()
|
|
@@ -69,6 +79,9 @@ export const storedSettingsDownloadSchema = z.object({
|
|
|
69
79
|
});
|
|
70
80
|
export const settingsDownloadFileActionSchema = z.enum(["settings_retry_download", "settings_report_download", "settings_cancel_download", "settings_store_backup"]);
|
|
71
81
|
export const settingsActionSchema = z.discriminatedUnion("action", [
|
|
82
|
+
z.object({ action: z.literal("settings_clear_read_address"), operationId: identity, connectionId: z.string().min(1), account: z.string().min(1) }).strict(),
|
|
83
|
+
z.object({ action: z.literal("settings_save_wallet_networks"), operationId: identity,
|
|
84
|
+
chains: walletChainsSchema.transform(parseWalletChains), expectedChains: walletChainsSchema.transform(parseWalletChains) }).strict(),
|
|
72
85
|
z.object({ action: z.literal("settings_download_backup"), operationId: identity, attemptId: identity }).strict(),
|
|
73
86
|
z.object({ action: z.literal("settings_retry_download"), operationId: identity, fileId: identity,
|
|
74
87
|
previousAttemptId: identity, attemptId: identity }).strict(),
|
|
@@ -77,7 +90,7 @@ export const settingsActionSchema = z.discriminatedUnion("action", [
|
|
|
77
90
|
z.object({ action: z.literal("settings_store_backup"), operationId: identity, fileId: identity, attemptId: identity,
|
|
78
91
|
saveAttemptId: identity, previousSaveAttemptId: identity.optional(), allowPossibleCopy: z.boolean() }).strict(),
|
|
79
92
|
z.object({ action: z.literal("settings_cancel_download"), operationId: identity, fileId: identity, attemptId: identity }).strict(),
|
|
80
|
-
z.object({ action: z.literal("settings_select"), draft:
|
|
93
|
+
z.object({ action: z.literal("settings_select"), draft: settingsSelectionSchema }).strict(),
|
|
81
94
|
z.object({ action: z.literal("settings_prepare"), operationId: identity }).strict(),
|
|
82
95
|
z.object({ action: z.literal("settings_begin_upload"), operationId: identity,
|
|
83
96
|
name: z.string().min(1), totalSize: size.max(SETTINGS_IMPORT_MAX_SIZE), sha256 }).strict(),
|
|
@@ -107,7 +120,7 @@ export class SettingsConflict extends Error {
|
|
|
107
120
|
}
|
|
108
121
|
export const settingsResultSchema = z.object({
|
|
109
122
|
outcome: z.enum(["completed", "failed", "interrupted", "superseded"]), recordedAt: z.string().datetime(),
|
|
110
|
-
message: z.string().min(1), details: z.record(z.string(), z.unknown()).optional(), completionScope: z.literal("
|
|
123
|
+
message: z.string().min(1), details: z.record(z.string(), z.unknown()).optional(), completionScope: z.literal("backup_delivery").optional()
|
|
111
124
|
}).strict();
|
|
112
125
|
const endpointView = z.object({ storedValue: z.string(), effectiveValue: z.string(),
|
|
113
126
|
source: z.enum(["environment", "local_db", "builtin_default"]), pendingStoredValue: z.string().optional(),
|
|
@@ -121,8 +134,7 @@ const localDataCounts = z.object({ accounts: count, reviewSessions: count, revie
|
|
|
121
134
|
externalActivityTransactions: count, localSettings: count }).strict();
|
|
122
135
|
const importPreview = z.object({ status: z.literal("valid"), format: z.literal(LOCAL_DATA_EXPORT_FORMAT), network: z.literal("mainnet"),
|
|
123
136
|
exportedAt: z.string().datetime(), currentCounts: localDataCounts, incomingCounts: localDataCounts, willReplace: z.literal(true),
|
|
124
|
-
|
|
125
|
-
incomingReadAddress: suiAddressStringSchema.nullable(), restartRequiredAfterImport: z.boolean() }).strict();
|
|
137
|
+
restartRequiredAfterImport: z.boolean() }).strict();
|
|
126
138
|
export const settingsViewSchema = z.object({
|
|
127
139
|
kind: z.literal("settings"), operationId: identity,
|
|
128
140
|
phase: z.enum(["draft", "uploading", "sealed", "prepared", "executing", "stopping", "starting", "terminal"]),
|
|
@@ -142,13 +154,13 @@ export const settingsViewSchema = z.object({
|
|
|
142
154
|
priorRunId: identity.optional(), nextRunId: identity.optional(), supersedesOperationId: identity.optional(), supersededByOperationId: identity.optional(),
|
|
143
155
|
allowedActions: z.array(z.enum(settingsActionSchema.options.map((option) => option.shape.action.value))), observe: z.boolean(),
|
|
144
156
|
facts: z.object({ server: z.object({ name: z.string(), version: z.string(), network: z.literal("mainnet") }).strict(),
|
|
145
|
-
localSettings: z.object({ suiGrpcUrl: endpointView, suiGraphqlUrl: endpointView }).strict(), readAddress: z.string().optional(),
|
|
157
|
+
localSettings: z.object({ suiGrpcUrl: endpointView, suiGraphqlUrl: endpointView, walletConnectionChains: walletChainsSchema }).strict(), readAddress: z.string().optional(), readConnectionId: z.string().optional(),
|
|
146
158
|
assetReadAccount: assetReadAccountSchema, walletAvailability: walletAvailabilitySchema,
|
|
147
159
|
connectionReconciliationRequired: z.literal(true).optional(), observedAt: z.string().datetime().optional(),
|
|
148
160
|
counts: localDataCounts }).strict().optional()
|
|
149
161
|
}).strict().superRefine((value, context) => {
|
|
150
162
|
if (value.download && (value.draft?.kind !== "download_backup" || !value.file ||
|
|
151
|
-
value.download.availability === "available" && (!value.file.sealed ||
|
|
163
|
+
value.download.availability === "available" && (!value.file.sealed || value.download.endReason || !value.download.expiresAt) ||
|
|
152
164
|
value.download.availability === "ended" && !value.download.endReason)) {
|
|
153
165
|
context.addIssue({ code: "custom", message: "Download delivery must describe its own snapshot and lifetime." });
|
|
154
166
|
}
|
|
@@ -174,20 +186,16 @@ export const settingsViewSchema = z.object({
|
|
|
174
186
|
(value.phase === "prepared" || value.allowedActions.includes("settings_prepare"))) {
|
|
175
187
|
context.addIssue({ code: "custom", message: "A rejected backup requires an explicit new selection." });
|
|
176
188
|
}
|
|
177
|
-
if (value.backupLocation && !(value.result?.outcome === "completed" &&
|
|
178
|
-
value.draft?.kind === "download_backup" && value.download?.localSave?.status === "saved" && value.download.endReason === "stored_locally"))
|
|
189
|
+
if (value.backupLocation && !(value.result?.outcome === "completed" &&
|
|
190
|
+
value.draft?.kind === "download_backup" && value.download?.localSave?.status === "saved" && value.download.endReason === "stored_locally")) {
|
|
179
191
|
context.addIssue({ code: "custom", message: "A backup location requires its recorded successful export." });
|
|
180
192
|
}
|
|
181
193
|
if (value.download?.localSave?.status === "saved" && !value.backupLocation) {
|
|
182
194
|
context.addIssue({ code: "custom", message: "A saved backup must identify its verified location." });
|
|
183
195
|
}
|
|
184
|
-
if (value.draft?.kind === "export_local_data" && value.result?.outcome === "completed" &&
|
|
185
|
-
typeof value.result.details?.path === "string" && !value.backupLocation) {
|
|
186
|
-
context.addIssue({ code: "custom", message: "A recorded backup path must provide its folder and file name." });
|
|
187
|
-
}
|
|
188
196
|
if (value.temporaryFileCleanup && !value.result)
|
|
189
197
|
context.addIssue({ code: "custom", message: "Cleanup of an applied or ended operation requires its recorded result." });
|
|
190
|
-
if ((value.phase === "terminal") !== (value.result !== undefined) || value.phase === "terminal" && (value.observe || value.allowedActions.length)) {
|
|
198
|
+
if ((value.phase === "terminal") !== (value.result !== undefined) || value.phase === "terminal" && (value.observe || value.allowedActions.length || value.download?.allowedFileActions.length)) {
|
|
191
199
|
context.addIssue({ code: "custom", message: "A terminal Settings result cannot grant input or observation." });
|
|
192
200
|
}
|
|
193
201
|
if (value.phase === "prepared" && (!value.preparationId || !value.draft || !value.preparation))
|