Skip to content

Commit 810144e

Browse files
earayu梅西
andauthored
fix(desktop): stabilize workflow trust hashes (#25)
Co-authored-by: 梅西 <messi-agent@users.noreply.github.com>
1 parent b8edf7a commit 810144e

4 files changed

Lines changed: 55 additions & 10 deletions

File tree

ui/desktop/src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
} from './utils/autoUpdater';
4848
import { UPDATES_ENABLED } from './updates';
4949
import './utils/recipeHash';
50+
import { calculateStableRecipeHash } from './utils/stableRecipeHash';
5051
import { Client } from './api/client';
5152
import { GooseApp } from './api';
5253
import * as mesh from './mesh';
@@ -210,7 +211,7 @@ async function seedDefaultRecipes(): Promise<void> {
210211
) {
211212
bundledTitles.push({ title: parsed.title, description: parsed.description });
212213
}
213-
const hash = crypto.createHash('sha256').update(JSON.stringify(parsed)).digest('hex');
214+
const hash = calculateStableRecipeHash(parsed);
214215
const hashFile = path.join(hashesDir, `${hash}.hash`);
215216
if (!fsSync.existsSync(hashFile)) {
216217
await fs.writeFile(hashFile, new Date().toISOString());

ui/desktop/src/utils/recipeHash.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@ import { ipcMain, app, BrowserWindow } from 'electron';
22
import fs from 'node:fs/promises';
33
import fsSync from 'node:fs';
44
import path from 'node:path';
5-
import crypto from 'crypto';
6-
7-
function calculateRecipeHash(recipe: unknown): string {
8-
const hash = crypto.createHash('sha256');
9-
hash.update(JSON.stringify(recipe));
10-
return hash.digest('hex');
11-
}
5+
import { calculateStableRecipeHash } from './stableRecipeHash';
126

137
async function getRecipeHashesDir(): Promise<string> {
148
const userDataPath = app.getPath('userData');
@@ -41,7 +35,7 @@ function isBundledRecipeByTitleAndDescription(recipe: unknown): boolean {
4135
}
4236

4337
ipcMain.handle('has-accepted-recipe-before', async (_event, recipe) => {
44-
const hash = calculateRecipeHash(recipe);
38+
const hash = calculateStableRecipeHash(recipe);
4539
const hashFile = path.join(await getRecipeHashesDir(), `${hash}.hash`);
4640
try {
4741
await fs.access(hashFile);
@@ -55,7 +49,7 @@ ipcMain.handle('has-accepted-recipe-before', async (_event, recipe) => {
5549
});
5650

5751
ipcMain.handle('record-recipe-hash', async (_event, recipe) => {
58-
const hash = calculateRecipeHash(recipe);
52+
const hash = calculateStableRecipeHash(recipe);
5953
const filePath = path.join(await getRecipeHashesDir(), `${hash}.hash`);
6054
const timestamp = new Date().toISOString();
6155
await fs.writeFile(filePath, timestamp);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { calculateStableRecipeHash } from './stableRecipeHash';
3+
4+
describe('calculateStableRecipeHash', () => {
5+
it('is stable when object keys are serialized in a different order', () => {
6+
const fromSave = {
7+
version: '1.0.0',
8+
title: '本地验收工作流',
9+
description: '用于验证保存后信任',
10+
instructions: '请按要求回答。',
11+
parameters: [{ key: 'question', input_type: 'string', requirement: 'required' }],
12+
};
13+
const fromList = {
14+
parameters: [{ requirement: 'required', input_type: 'string', key: 'question' }],
15+
instructions: '请按要求回答。',
16+
description: '用于验证保存后信任',
17+
title: '本地验收工作流',
18+
version: '1.0.0',
19+
};
20+
21+
expect(calculateStableRecipeHash(fromSave)).toBe(calculateStableRecipeHash(fromList));
22+
});
23+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import crypto from 'crypto';
2+
3+
function normalizeForHash(value: unknown): unknown {
4+
if (Array.isArray(value)) {
5+
return value.map(normalizeForHash);
6+
}
7+
8+
if (value && typeof value === 'object') {
9+
return Object.keys(value as Record<string, unknown>)
10+
.sort()
11+
.reduce<Record<string, unknown>>((acc, key) => {
12+
const item = (value as Record<string, unknown>)[key];
13+
if (item !== undefined) {
14+
acc[key] = normalizeForHash(item);
15+
}
16+
return acc;
17+
}, {});
18+
}
19+
20+
return value;
21+
}
22+
23+
export function calculateStableRecipeHash(recipe: unknown): string {
24+
const hash = crypto.createHash('sha256');
25+
hash.update(JSON.stringify(normalizeForHash(recipe)));
26+
return hash.digest('hex');
27+
}

0 commit comments

Comments
 (0)