moving
This commit is contained in:
14
app/package-lock.json
generated
14
app/package-lock.json
generated
@@ -2958,9 +2958,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.17",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
|
||||
"integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3007,9 +3007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"version": "8.5.25",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
|
||||
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3027,7 +3027,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
"typecheck": "tsc -b",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"tauri:build": "node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/bundle-node.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build",
|
||||
"tauri:build:local": "node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/bundle-node.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --debug",
|
||||
"tauri:build:win": "node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/bundle-node.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target x86_64-pc-windows-msvc",
|
||||
"tauri:build": "node ../scripts/bundle-node.mjs && node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build",
|
||||
"tauri:build:local": "node ../scripts/bundle-node.mjs && node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --debug",
|
||||
"tauri:build:win": "node ../scripts/bundle-node.mjs && node ../scripts/build-sidecar.mjs && node ../scripts/bundle-native-deps.mjs && node ../scripts/stage-sidecar-deps.mjs && node ../scripts/check-sidecar-resources.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target x86_64-pc-windows-msvc",
|
||||
"tauri:build:win:pilot-signed": "npm run tauri:sign:pilot:win:apply && npm run tauri:build:win -- --config src-tauri/tauri.build-override.conf.json",
|
||||
"tauri:build:mac": "npm run tauri:build:mac:arm64 && npm run tauri:build:mac:x64",
|
||||
"tauri:build:mac:arm64": "node ../scripts/build-sidecar.mjs && TARGET_ARCH=arm64 node ../scripts/bundle-native-deps.mjs && TARGET_ARCH=arm64 node ../scripts/bundle-node.mjs && TARGET_ARCH=arm64 node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target aarch64-apple-darwin",
|
||||
"tauri:build:mac:x64": "node ../scripts/build-sidecar.mjs && TARGET_ARCH=x64 node ../scripts/bundle-native-deps.mjs && TARGET_ARCH=x64 node ../scripts/bundle-node.mjs && TARGET_ARCH=x64 node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target x86_64-apple-darwin",
|
||||
"tauri:build:mac:arm64": "TARGET_ARCH=arm64 node ../scripts/bundle-node.mjs && node ../scripts/build-sidecar.mjs && TARGET_ARCH=arm64 node ../scripts/bundle-native-deps.mjs && TARGET_ARCH=arm64 node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target aarch64-apple-darwin",
|
||||
"tauri:build:mac:x64": "TARGET_ARCH=x64 node ../scripts/bundle-node.mjs && node ../scripts/build-sidecar.mjs && TARGET_ARCH=x64 node ../scripts/bundle-native-deps.mjs && TARGET_ARCH=x64 node ../scripts/stage-sidecar-deps.mjs && cd ../apps/web && npx vite build && cd ../../app && npx tauri build --target x86_64-apple-darwin",
|
||||
"tauri:dev": "npx tauri dev",
|
||||
"tauri:sign:pilot:win:setup": "powershell -ExecutionPolicy Bypass -File scripts/sign-windows-pilot.ps1 -Mode Setup",
|
||||
"tauri:sign:pilot:win:apply": "node scripts/apply-signing-config.mjs",
|
||||
|
||||
@@ -16,12 +16,10 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// The pure helpers below mirror app/scripts/signing-config.ts so this CLI has
|
||||
// zero TS-loader dependency at runtime. The .ts version is the canonical
|
||||
// implementation tested by signing-config.test.ts (19 cases covering parse,
|
||||
// merge, idempotency, immutability). Keep the two implementations in lockstep:
|
||||
// any change to parseThumbprintString or addWindowsSigningToOverride below
|
||||
// MUST be mirrored in signing-config.ts and vice versa.
|
||||
// The pure helpers below mirror the certificate-store helpers in
|
||||
// app/scripts/signing-config.ts so this pilot CLI has zero TS-loader dependency
|
||||
// at runtime. Keep parseThumbprintString and addWindowsSigningToOverride in
|
||||
// lockstep with the canonical TypeScript implementation.
|
||||
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const APP_DIR = resolve(SCRIPT_DIR, '..');
|
||||
@@ -33,11 +31,12 @@ const OVERRIDE_PATH = resolve(
|
||||
'tauri.build-override.conf.json',
|
||||
);
|
||||
const THUMBPRINT_PATH = resolve(APP_DIR, 'src-tauri', '.thumbprint.txt');
|
||||
|
||||
const DEFAULT_DIGEST_ALGORITHM = 'sha256';
|
||||
const DEFAULT_TIMESTAMP_URL = 'http://timestamp.digicert.com';
|
||||
const THUMBPRINT_LENGTH = 40;
|
||||
const HEX_PATTERN = /^[0-9A-F]+$/;
|
||||
const WINDOWS_SIGNING_MODE =
|
||||
process.env.WAGGLE_WINDOWS_SIGNING_MODE ?? 'certificate-store';
|
||||
|
||||
function parseThumbprintString(raw) {
|
||||
if (!raw || raw.trim().length === 0) {
|
||||
@@ -59,13 +58,15 @@ function addWindowsSigningToOverride(config, thumbprint, options = {}) {
|
||||
|
||||
const existingBundle = config.bundle ?? {};
|
||||
const existingWindows = existingBundle.windows ?? {};
|
||||
const nonCustomCommandWindows = { ...existingWindows };
|
||||
delete nonCustomCommandWindows.signCommand;
|
||||
|
||||
return {
|
||||
...config,
|
||||
bundle: {
|
||||
...existingBundle,
|
||||
windows: {
|
||||
...existingWindows,
|
||||
...nonCustomCommandWindows,
|
||||
certificateThumbprint: normalisedThumbprint,
|
||||
digestAlgorithm,
|
||||
timestampUrl,
|
||||
@@ -77,6 +78,20 @@ function addWindowsSigningToOverride(config, thumbprint, options = {}) {
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function main() {
|
||||
if (!['certificate-store', 'artifact-signing'].includes(WINDOWS_SIGNING_MODE)) {
|
||||
console.error(
|
||||
`[apply-signing-config] unsupported WAGGLE_WINDOWS_SIGNING_MODE: ${WINDOWS_SIGNING_MODE}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (WINDOWS_SIGNING_MODE === 'artifact-signing') {
|
||||
console.error(
|
||||
'[apply-signing-config] Azure Artifact Signing is hosted-only. '
|
||||
+ 'Run the protected GitHub-hosted release workflow; this local helper cannot issue '
|
||||
+ 'the immutable build receipt, protected OIDC identity, session manifest, or callback ledger.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!existsSync(THUMBPRINT_PATH)) {
|
||||
console.error(
|
||||
`[apply-signing-config] thumbprint file missing: ${THUMBPRINT_PATH}`,
|
||||
@@ -93,7 +108,6 @@ function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rawThumbprint = readFileSync(THUMBPRINT_PATH, 'utf8');
|
||||
const overrideRaw = readFileSync(OVERRIDE_PATH, 'utf8');
|
||||
|
||||
let override;
|
||||
@@ -108,6 +122,7 @@ function main() {
|
||||
|
||||
let updated;
|
||||
try {
|
||||
const rawThumbprint = readFileSync(THUMBPRINT_PATH, 'utf8');
|
||||
updated = addWindowsSigningToOverride(override, rawThumbprint);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
@@ -122,9 +137,9 @@ function main() {
|
||||
writeFileSync(OVERRIDE_PATH, serialised, 'utf8');
|
||||
|
||||
const relativePath = OVERRIDE_PATH.replace(REPO_ROOT, '').replace(/^\\/, '');
|
||||
console.log(
|
||||
`[apply-signing-config] wrote thumbprint ${updated.bundle.windows.certificateThumbprint.slice(0, 8)}... to ${relativePath}`,
|
||||
);
|
||||
const signingDescription =
|
||||
`thumbprint ${updated.bundle.windows.certificateThumbprint.slice(0, 8)}...`;
|
||||
console.log(`[apply-signing-config] wrote ${signingDescription} to ${relativePath}`);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
1059
app/scripts/new-windows-signing-handoff.ps1
Normal file
1059
app/scripts/new-windows-signing-handoff.ps1
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,10 @@
|
||||
# Usage:
|
||||
# ./sign-macos-adhoc.sh <path-to-Waggle.app>
|
||||
#
|
||||
# Tauri's bundle config (tauri.build-override.conf.json) already passes
|
||||
# `signingIdentity: "-"` to codesign at build time, so the produced .app is
|
||||
# already ad-hoc-signed. This script:
|
||||
# Ordinary `npm run tauri:build:mac` does not load the optional build override.
|
||||
# Treat the input as unsigned until this script signs and verifies it. This script:
|
||||
#
|
||||
# 1. Re-signs the bundle with --force --deep to catch any nested helpers
|
||||
# 1. Signs or re-signs the bundle with --force --deep to catch nested helpers
|
||||
# (sidecar binary, native deps) that Tauri's pass missed.
|
||||
# 2. Verifies the signature with --verify --deep --strict.
|
||||
#
|
||||
|
||||
4704
app/scripts/sign-windows-artifact.ps1
Normal file
4704
app/scripts/sign-windows-artifact.ps1
Normal file
File diff suppressed because it is too large
Load Diff
2772
app/scripts/sign-windows-artifact.test.ps1
Normal file
2772
app/scripts/sign-windows-artifact.test.ps1
Normal file
File diff suppressed because it is too large
Load Diff
@@ -155,9 +155,8 @@ if ($Mode -eq 'Setup') {
|
||||
Write-Host "[setup] thumbprint -> $ThumbprintFile" -ForegroundColor Green
|
||||
Write-Host ''
|
||||
Write-Host 'Next:' -ForegroundColor Cyan
|
||||
Write-Host ' 1. cd app && npm run tauri:sign:pilot:win:apply'
|
||||
Write-Host ' 2. npm run tauri:build:win'
|
||||
Write-Host ' 3. .\scripts\sign-windows-pilot.ps1 -Mode Sign -ArtifactPath <path-to-msi>'
|
||||
Write-Host ' 1. npm run tauri:build:win:pilot-signed'
|
||||
Write-Host ' 2. .\scripts\sign-windows-pilot.ps1 -Mode Sign -ArtifactPath <path-to-msi> # optional'
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
parseThumbprintString,
|
||||
addWindowsArtifactSigningToOverride,
|
||||
addWindowsSigningToOverride,
|
||||
addMacosAdhocToOverride,
|
||||
type TauriOverrideConfig,
|
||||
@@ -86,6 +90,23 @@ describe('addWindowsSigningToOverride', () => {
|
||||
expect(out.bundle?.windows?.certificateThumbprint).toBe(VALID_THUMBPRINT);
|
||||
});
|
||||
|
||||
it('removes an Azure signCommand when returning to certificate-store signing', () => {
|
||||
const azure: TauriOverrideConfig = addWindowsArtifactSigningToOverride(
|
||||
{
|
||||
bundle: {
|
||||
windows: { nsis: { installMode: 'currentUser' } },
|
||||
},
|
||||
},
|
||||
String.raw`D:\a\waggle-os\app\scripts\sign-windows-artifact.ps1`,
|
||||
);
|
||||
|
||||
const out = addWindowsSigningToOverride(azure, VALID_THUMBPRINT);
|
||||
|
||||
expect(out.bundle?.windows?.signCommand).toBeUndefined();
|
||||
expect(out.bundle?.windows?.certificateThumbprint).toBe(VALID_THUMBPRINT);
|
||||
expect(out.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
||||
});
|
||||
|
||||
it('overrides custom digestAlgorithm and timestampUrl when options provided', () => {
|
||||
const out = addWindowsSigningToOverride({}, VALID_THUMBPRINT, {
|
||||
digestAlgorithm: 'sha384',
|
||||
@@ -127,6 +148,154 @@ describe('addWindowsSigningToOverride', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addWindowsArtifactSigningToOverride ───────────────────────────────────
|
||||
|
||||
describe('addWindowsArtifactSigningToOverride', () => {
|
||||
const WRAPPER_PATH = String.raw`D:\a\waggle-os\app\scripts\sign-windows-artifact.ps1`;
|
||||
const SYSTEM_POWERSHELL_PATH =
|
||||
String.raw`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`;
|
||||
|
||||
it('configures an object-form Tauri signCommand with one artifact placeholder', () => {
|
||||
const out = addWindowsArtifactSigningToOverride(
|
||||
{},
|
||||
WRAPPER_PATH,
|
||||
);
|
||||
|
||||
expect(out.bundle?.windows?.signCommand).toEqual({
|
||||
cmd: SYSTEM_POWERSHELL_PATH,
|
||||
args: [
|
||||
'-NoLogo',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-File',
|
||||
WRAPPER_PATH,
|
||||
'-ArtifactPath',
|
||||
'%1',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes mutually exclusive certificate-store signing fields', () => {
|
||||
const input: TauriOverrideConfig = {
|
||||
bundle: {
|
||||
windows: {
|
||||
certificateThumbprint: 'AB'.repeat(20),
|
||||
digestAlgorithm: 'sha256',
|
||||
timestampUrl: 'http://timestamp.digicert.com',
|
||||
tsp: true,
|
||||
nsis: { installMode: 'currentUser' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const out = addWindowsArtifactSigningToOverride(
|
||||
input,
|
||||
WRAPPER_PATH,
|
||||
);
|
||||
expect(out.bundle?.windows?.certificateThumbprint).toBeUndefined();
|
||||
expect(out.bundle?.windows?.digestAlgorithm).toBeUndefined();
|
||||
expect(out.bundle?.windows?.timestampUrl).toBeUndefined();
|
||||
expect(out.bundle?.windows?.tsp).toBeUndefined();
|
||||
expect(out.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
||||
});
|
||||
|
||||
it('is immutable and idempotent', () => {
|
||||
const input: TauriOverrideConfig = {
|
||||
build: {
|
||||
beforeBuildCommand: 'npm run build',
|
||||
beforeBundleCommand: 'node mutate-bundle.mjs',
|
||||
},
|
||||
bundle: {
|
||||
active: false,
|
||||
targets: ['msi'],
|
||||
windows: { nsis: { installMode: 'currentUser' } },
|
||||
},
|
||||
};
|
||||
const snapshot = JSON.parse(JSON.stringify(input));
|
||||
const once = addWindowsArtifactSigningToOverride(
|
||||
input,
|
||||
WRAPPER_PATH,
|
||||
);
|
||||
const twice = addWindowsArtifactSigningToOverride(
|
||||
once,
|
||||
WRAPPER_PATH,
|
||||
);
|
||||
|
||||
expect(input).toEqual(snapshot);
|
||||
expect(twice).toEqual(once);
|
||||
expect(once.build).toEqual({
|
||||
beforeBuildCommand: '',
|
||||
beforeBundleCommand: '',
|
||||
});
|
||||
expect(once.bundle?.active).toBe(true);
|
||||
expect(once.bundle?.targets).toEqual(['nsis']);
|
||||
expect(once.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
||||
});
|
||||
|
||||
it('rejects non-absolute, placeholder-bearing, or control-character wrapper paths', () => {
|
||||
expect(() =>
|
||||
addWindowsArtifactSigningToOverride(
|
||||
{},
|
||||
'scripts/sign.ps1',
|
||||
),
|
||||
).toThrow(/absolute Windows path/i);
|
||||
expect(() =>
|
||||
addWindowsArtifactSigningToOverride(
|
||||
{},
|
||||
String.raw`D:\a\%1\sign-windows-artifact.ps1`,
|
||||
),
|
||||
).toThrow(/placeholder/i);
|
||||
expect(() =>
|
||||
addWindowsArtifactSigningToOverride(
|
||||
{},
|
||||
'D:\\safe\nmalicious.ps1',
|
||||
),
|
||||
).toThrow(/control characters/i);
|
||||
expect(() =>
|
||||
addWindowsArtifactSigningToOverride(
|
||||
{},
|
||||
String.raw`D:\safe\..\malicious.ps1`,
|
||||
),
|
||||
).toThrow(/canonical local Windows/i);
|
||||
expect(() =>
|
||||
addWindowsArtifactSigningToOverride(
|
||||
{},
|
||||
String.raw`D:\safe\sign.ps1:payload`,
|
||||
),
|
||||
).toThrow(/canonical local Windows/i);
|
||||
});
|
||||
|
||||
it('contains exactly one artifact placeholder across the complete command', () => {
|
||||
const out = addWindowsArtifactSigningToOverride({}, WRAPPER_PATH);
|
||||
const command = out.bundle?.windows?.signCommand;
|
||||
const placeholderCount = [command?.cmd, ...(command?.args ?? [])]
|
||||
.flatMap((part) => part?.match(/%1/g) ?? [])
|
||||
.length;
|
||||
expect(placeholderCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('apply-signing-config Artifact Signing boundary', () => {
|
||||
it('fails closed toward the protected hosted release workflow, never local Build mode', () => {
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[resolve(scriptDir, 'apply-signing-config.mjs')],
|
||||
{
|
||||
cwd: resolve(scriptDir, '..'),
|
||||
env: { ...process.env, WAGGLE_WINDOWS_SIGNING_MODE: 'artifact-signing' },
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(output).toMatch(/protected GitHub-hosted release workflow/i);
|
||||
expect(output).not.toMatch(/-Mode Build/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addMacosAdhocToOverride ────────────────────────────────────────────────
|
||||
|
||||
describe('addMacosAdhocToOverride', () => {
|
||||
|
||||
@@ -18,10 +18,17 @@
|
||||
export interface TauriBundleWindows {
|
||||
certificateThumbprint?: string;
|
||||
digestAlgorithm?: string;
|
||||
signCommand?: TauriSignCommand;
|
||||
timestampUrl?: string;
|
||||
tsp?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface TauriSignCommand {
|
||||
cmd: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export interface TauriBundleMacOS {
|
||||
signingIdentity?: string;
|
||||
[key: string]: unknown;
|
||||
@@ -34,6 +41,7 @@ export interface TauriBundle {
|
||||
}
|
||||
|
||||
export interface TauriOverrideConfig {
|
||||
build?: Record<string, unknown>;
|
||||
bundle?: TauriBundle;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -50,6 +58,39 @@ const DEFAULT_TIMESTAMP_URL = 'http://timestamp.digicert.com';
|
||||
const MACOS_ADHOC_IDENTITY = '-';
|
||||
const THUMBPRINT_LENGTH = 40;
|
||||
const HEX_PATTERN = /^[0-9A-F]+$/;
|
||||
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
|
||||
const WINDOWS_POWERSHELL_PATH =
|
||||
String.raw`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`;
|
||||
|
||||
function containsControlCharacter(value: string): boolean {
|
||||
return [...value].some((character) => {
|
||||
const codePoint = character.codePointAt(0) ?? 0;
|
||||
return codePoint <= 31 || codePoint === 127;
|
||||
});
|
||||
}
|
||||
|
||||
function assertCanonicalWindowsFilePath(value: string): void {
|
||||
if (containsControlCharacter(value)) {
|
||||
throw new Error('Artifact Signing wrapper path contains control characters.');
|
||||
}
|
||||
if (!WINDOWS_ABSOLUTE_PATH_PATTERN.test(value)) {
|
||||
throw new Error('Artifact Signing wrapper must use an absolute Windows path.');
|
||||
}
|
||||
const pathTail = value.slice(3);
|
||||
const segments = pathTail.split(/[\\/]/);
|
||||
if (
|
||||
pathTail.length === 0
|
||||
|| value.slice(2).includes(':')
|
||||
|| segments.some(
|
||||
(segment) => segment.length === 0
|
||||
|| segment === '.'
|
||||
|| segment === '..'
|
||||
|| /[. ]$/.test(segment),
|
||||
)
|
||||
) {
|
||||
throw new Error('Artifact Signing wrapper must use a canonical local Windows file path.');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── parseThumbprintString ──────────────────────────────────────────────────
|
||||
|
||||
@@ -82,7 +123,7 @@ export function parseThumbprintString(raw: string): string {
|
||||
* Return a new override config with Windows code-signing fields applied.
|
||||
*
|
||||
* Preserves all existing top-level and bundle fields; replaces only the
|
||||
* three signing-specific keys under `bundle.windows`. Idempotent — calling
|
||||
* signing-specific keys under `bundle.windows`. Idempotent — calling
|
||||
* twice with the same thumbprint yields an equal result.
|
||||
*/
|
||||
export function addWindowsSigningToOverride<T extends TauriOverrideConfig>(
|
||||
@@ -96,9 +137,11 @@ export function addWindowsSigningToOverride<T extends TauriOverrideConfig>(
|
||||
|
||||
const existingBundle: TauriBundle = config.bundle ?? {};
|
||||
const existingWindows: TauriBundleWindows = existingBundle.windows ?? {};
|
||||
const nonCustomCommandWindows: TauriBundleWindows = { ...existingWindows };
|
||||
delete nonCustomCommandWindows.signCommand;
|
||||
|
||||
const nextWindows: TauriBundleWindows = {
|
||||
...existingWindows,
|
||||
...nonCustomCommandWindows,
|
||||
certificateThumbprint: normalisedThumbprint,
|
||||
digestAlgorithm,
|
||||
timestampUrl,
|
||||
@@ -115,6 +158,73 @@ export function addWindowsSigningToOverride<T extends TauriOverrideConfig>(
|
||||
};
|
||||
}
|
||||
|
||||
// ─── addWindowsArtifactSigningToOverride ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return a new override config that delegates every Tauri Windows signing
|
||||
* target to the fail-closed Azure Artifact Signing wrapper.
|
||||
*
|
||||
* Tauri replaces `%1` with each binary path. Object form keeps the absolute
|
||||
* wrapper path intact when the checkout contains spaces. Certificate-store
|
||||
* fields are removed because Tauri must not combine them with `signCommand`.
|
||||
*/
|
||||
export function addWindowsArtifactSigningToOverride<
|
||||
T extends TauriOverrideConfig,
|
||||
>(config: Readonly<T>, wrapperPath: string): T {
|
||||
assertCanonicalWindowsFilePath(wrapperPath);
|
||||
if (wrapperPath.includes('%1')) {
|
||||
throw new Error('Artifact Signing wrapper path cannot contain the %1 placeholder.');
|
||||
}
|
||||
const existingBuild = config.build ?? {};
|
||||
const existingBundle: TauriBundle = config.bundle ?? {};
|
||||
const existingWindows: TauriBundleWindows = existingBundle.windows ?? {};
|
||||
const nonSigningWindows: TauriBundleWindows = { ...existingWindows };
|
||||
delete nonSigningWindows.certificateThumbprint;
|
||||
delete nonSigningWindows.digestAlgorithm;
|
||||
delete nonSigningWindows.timestampUrl;
|
||||
delete nonSigningWindows.tsp;
|
||||
|
||||
const nextWindows: TauriBundleWindows = {
|
||||
...nonSigningWindows,
|
||||
signCommand: {
|
||||
cmd: WINDOWS_POWERSHELL_PATH,
|
||||
args: [
|
||||
'-NoLogo',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-File',
|
||||
wrapperPath,
|
||||
'-ArtifactPath',
|
||||
'%1',
|
||||
],
|
||||
},
|
||||
};
|
||||
const placeholderCount = [
|
||||
nextWindows.signCommand?.cmd,
|
||||
...(nextWindows.signCommand?.args ?? []),
|
||||
].flatMap((part) => part?.match(/%1/g) ?? []).length;
|
||||
if (placeholderCount !== 1) {
|
||||
throw new Error('Artifact Signing command must contain exactly one %1 placeholder.');
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
build: {
|
||||
...existingBuild,
|
||||
beforeBuildCommand: '',
|
||||
beforeBundleCommand: '',
|
||||
},
|
||||
bundle: {
|
||||
...existingBundle,
|
||||
active: true,
|
||||
targets: ['nsis'],
|
||||
windows: nextWindows,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── addMacosAdhocToOverride ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
45
app/scripts/signing-guidance.test.ts
Normal file
45
app/scripts/signing-guidance.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const playbook = readFileSync(
|
||||
new URL('../../docs/code-signing-pilot-and-launch.md', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const pilotScript = readFileSync(
|
||||
new URL('./sign-windows-pilot.ps1', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const macPilotScript = readFileSync(
|
||||
new URL('./sign-macos-adhoc.sh', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('internal pilot signing guidance', () => {
|
||||
it('routes Windows builds through the explicit pilot-signing override', () => {
|
||||
expect(playbook).toContain('npm run tauri:build:win:pilot-signed');
|
||||
expect(playbook).not.toMatch(/^npm run tauri:build:win$/m);
|
||||
expect(pilotScript).toContain(
|
||||
"Write-Host ' 1. npm run tauri:build:win:pilot-signed'",
|
||||
);
|
||||
expect(pilotScript).not.toMatch(
|
||||
/Write-Host '[ ]{2}1\. npm run tauri:build:win'\s*$/m,
|
||||
);
|
||||
expect(pilotScript).not.toMatch(/Write-Host '[ ]+1\. cd app/);
|
||||
});
|
||||
|
||||
it('does not claim an ordinary macOS build loads the signing override', () => {
|
||||
expect(playbook).toContain('macOS is deferred');
|
||||
expect(playbook).toContain('npm run tauri:sign:pilot:mac:adhoc');
|
||||
expect(playbook).not.toContain(
|
||||
'so every `npm run tauri:build:mac` produces an ad-hoc-signed `.app` automatically',
|
||||
);
|
||||
expect(playbook).not.toContain(
|
||||
'ships the macOS ad-hoc identity in the build-override config by default',
|
||||
);
|
||||
expect(macPilotScript).toContain(
|
||||
'Treat the input as unsigned until this script signs and verifies it.',
|
||||
);
|
||||
expect(macPilotScript).not.toContain('already passes');
|
||||
});
|
||||
});
|
||||
1
app/src-tauri/Cargo.lock
generated
1
app/src-tauri/Cargo.lock
generated
@@ -5018,6 +5018,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -27,3 +27,11 @@ serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
urlencoding = "2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
@@ -1,69 +1,25 @@
|
||||
; ─── Waggle NSIS Installer Template ──────────────────────────────────────────
|
||||
; Waggle-specific extensions for Tauri's NSIS installer.
|
||||
;
|
||||
; Custom hooks for the Tauri NSIS installer:
|
||||
; 1. Welcome message with Waggle branding
|
||||
; 2. Desktop shortcut creation
|
||||
; 3. Start Menu entry
|
||||
; 4. "Launch Waggle" on finish
|
||||
; 5. Uninstaller with optional ~/.waggle/ data removal
|
||||
; Tauri owns install location, shortcuts, finish-page launch, silent /R launch,
|
||||
; registry entries, and uninstaller cleanup. Do not duplicate those here: doing
|
||||
; so double-launched normal installs and made silent repair nondeterministic.
|
||||
; Autostart is handled by tauri-plugin-autostart at runtime.
|
||||
; Personal data is always preserved by the package uninstaller. Tauri's base
|
||||
; uninstaller exposes a generic "Delete app data" checkbox, so PREUNINSTALL
|
||||
; explicitly neutralizes that state. Destructive erasure is available only
|
||||
; through Waggle's authenticated, phrase-gated UI.
|
||||
;
|
||||
; Tauri injects NSIS defines: PRODUCT_NAME, PRODUCT_VERSION, MAINBINARYNAME,
|
||||
; DEFAULT_INSTALL_DIR. Autostart is handled by tauri-plugin-autostart at
|
||||
; runtime, not by the installer.
|
||||
;
|
||||
; Reference: https://tauri.app/distribute/windows-installer/#nsis
|
||||
; ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
InstallDir "${DEFAULT_INSTALL_DIR}"
|
||||
; Reference: https://v2.tauri.app/distribute/windows-installer/#extending-the-installer
|
||||
|
||||
!macro NSIS_HOOK_PREINSTALL
|
||||
DetailPrint "Installing ${PRODUCT_NAME} v${PRODUCT_VERSION}..."
|
||||
DetailPrint "Your personal AI agent workspace — powered by Waggle."
|
||||
DetailPrint "Installing Waggle..."
|
||||
DetailPrint "Your personal AI agent workspace - powered by Waggle."
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_POSTINSTALL
|
||||
; ── Desktop shortcut ──────────────────────────────────────────────────────
|
||||
CreateShortcut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" \
|
||||
"" "$INSTDIR\${MAINBINARYNAME}.exe" 0
|
||||
DetailPrint "Desktop shortcut created."
|
||||
|
||||
; ── Start Menu entry ──────────────────────────────────────────────────────
|
||||
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
|
||||
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" \
|
||||
"$INSTDIR\${MAINBINARYNAME}.exe" "" "$INSTDIR\${MAINBINARYNAME}.exe" 0
|
||||
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk" \
|
||||
"$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0
|
||||
DetailPrint "Start Menu entry created."
|
||||
|
||||
; ── Launch after install ──────────────────────────────────────────────────
|
||||
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
|
||||
DetailPrint "Launching ${PRODUCT_NAME}..."
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_POSTUNINSTALL
|
||||
; ── Remove desktop shortcut ─────────────────────────────────────────────
|
||||
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
|
||||
|
||||
; ── Remove Start Menu entries ───────────────────────────────────────────
|
||||
Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
|
||||
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk"
|
||||
RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
|
||||
|
||||
; ── Ask about user data removal ─────────────────────────────────────────
|
||||
MessageBox MB_YESNO|MB_ICONQUESTION \
|
||||
"Waggle stores your data (agents, memories, configuration) in:$\r$\n$\r$\n\
|
||||
$PROFILE\.waggle$\r$\n$\r$\n\
|
||||
Do you want to remove this data as well?$\r$\n$\r$\n\
|
||||
Choose $\"Yes$\" to delete all data, or $\"No$\" to keep it for future use." \
|
||||
IDYES removeData IDNO skipData
|
||||
|
||||
removeData:
|
||||
RMDir /r "$PROFILE\.waggle"
|
||||
DetailPrint "User data removed: $PROFILE\.waggle"
|
||||
Goto doneData
|
||||
|
||||
skipData:
|
||||
DetailPrint "User data preserved: $PROFILE\.waggle"
|
||||
|
||||
doneData:
|
||||
!macro NSIS_HOOK_PREUNINSTALL
|
||||
StrCmp $DeleteAppDataCheckboxState "1" 0 +2
|
||||
MessageBox MB_OK|MB_ICONINFORMATION \
|
||||
"For safety, Waggle always preserves app data during uninstall. Data can only be erased from Settings > Data & Privacy while Waggle is installed."
|
||||
StrCpy $DeleteAppDataCheckboxState 0
|
||||
DetailPrint "Preserving Waggle app data."
|
||||
!macroend
|
||||
|
||||
@@ -42,7 +42,7 @@ pub async fn run_agent_query(
|
||||
session: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
let request_id = format!("agent-{}", Uuid::new_v4());
|
||||
let port = state.port;
|
||||
let port = state.verified_port()?;
|
||||
let app_clone = app.clone();
|
||||
let req_id_clone = request_id.clone();
|
||||
|
||||
|
||||
@@ -31,9 +31,10 @@ pub async fn recall_memory(
|
||||
limit: Option<u32>,
|
||||
workspace_id: Option<String>,
|
||||
) -> Result<Value, String> {
|
||||
let port = state.verified_port()?;
|
||||
let mut url = format!(
|
||||
"{}?q={}",
|
||||
sidecar_url(state.port, "/api/memory/search"),
|
||||
sidecar_url(port, "/api/memory/search"),
|
||||
urlencoding::encode(&query)
|
||||
);
|
||||
if let Some(s) = scope {
|
||||
@@ -61,6 +62,7 @@ pub async fn save_memory(
|
||||
importance: Option<String>,
|
||||
source: Option<String>,
|
||||
) -> Result<Value, String> {
|
||||
let port = state.verified_port()?;
|
||||
let mut body = json!({ "content": content });
|
||||
if let Some(ws) = workspace_id {
|
||||
body["workspace"] = json!(ws);
|
||||
@@ -72,7 +74,7 @@ pub async fn save_memory(
|
||||
body["source"] = json!(src);
|
||||
}
|
||||
|
||||
let url = sidecar_url(state.port, "/api/memory/frames");
|
||||
let url = sidecar_url(port, "/api/memory/frames");
|
||||
let resp = http_post(&url, &body).await?;
|
||||
parse_json(resp).await
|
||||
}
|
||||
@@ -85,7 +87,8 @@ pub async fn search_entities(
|
||||
workspace_id: Option<String>,
|
||||
scope: Option<String>,
|
||||
) -> Result<Value, String> {
|
||||
let mut url = sidecar_url(state.port, "/api/memory/graph").to_string();
|
||||
let port = state.verified_port()?;
|
||||
let mut url = sidecar_url(port, "/api/memory/graph").to_string();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
if let Some(ws) = workspace_id {
|
||||
params.push(format!("workspace={}", urlencoding::encode(&ws)));
|
||||
@@ -111,7 +114,7 @@ pub async fn search_entities(
|
||||
/// pre-A1.1 placeholders and now only fire on hard sidecar outages.
|
||||
#[tauri::command]
|
||||
pub async fn get_identity(state: State<'_, ServiceState>) -> Result<Value, String> {
|
||||
let url = sidecar_url(state.port, "/api/identity");
|
||||
let url = sidecar_url(state.verified_port()?, "/api/identity");
|
||||
match http_get(&url).await {
|
||||
Ok(resp) if resp.status().as_u16() == 404 => Ok(identity_placeholder(
|
||||
"sidecar route 404 (unexpected post-A1.1)",
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::service::ServiceState;
|
||||
/// index (slugs + titles + metadata); call get_wiki_page_content for the body.
|
||||
#[tauri::command]
|
||||
pub async fn get_wiki_pages(state: State<'_, ServiceState>) -> Result<Value, String> {
|
||||
let url = sidecar_url(state.port, "/api/wiki/pages");
|
||||
let url = sidecar_url(state.verified_port()?, "/api/wiki/pages");
|
||||
let resp = http_get(&url).await?;
|
||||
parse_json(resp).await
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub async fn get_wiki_pages(state: State<'_, ServiceState>) -> Result<Value, Str
|
||||
#[tauri::command]
|
||||
pub async fn get_wiki_page(state: State<'_, ServiceState>, slug: String) -> Result<Value, String> {
|
||||
let url = sidecar_url(
|
||||
state.port,
|
||||
state.verified_port()?,
|
||||
&format!("/api/wiki/pages/{}", urlencoding::encode(&slug)),
|
||||
);
|
||||
let resp = http_get(&url).await?;
|
||||
@@ -51,7 +51,7 @@ pub async fn get_wiki_page_content(
|
||||
slug: String,
|
||||
) -> Result<Value, String> {
|
||||
let url = sidecar_url(
|
||||
state.port,
|
||||
state.verified_port()?,
|
||||
&format!("/api/wiki/pages/{}/content", urlencoding::encode(&slug)),
|
||||
);
|
||||
let resp = http_get(&url).await?;
|
||||
@@ -69,7 +69,7 @@ pub async fn compile_wiki_section(
|
||||
if let Some(ws) = workspace_id {
|
||||
body["workspace"] = json!(ws);
|
||||
}
|
||||
let url = sidecar_url(state.port, "/api/wiki/compile");
|
||||
let url = sidecar_url(state.verified_port()?, "/api/wiki/compile");
|
||||
let resp = http_post(&url, &body).await?;
|
||||
parse_json(resp).await
|
||||
}
|
||||
|
||||
@@ -59,6 +59,52 @@ pub fn run() {
|
||||
commands::onboarding::reset_first_launch,
|
||||
])
|
||||
.setup(|app| {
|
||||
// Create the configured window here so the Windows certifier can
|
||||
// opt into a loopback-only WebView CDP port without shipping
|
||||
// remote debugging enabled for normal launches.
|
||||
let main_window_config = app
|
||||
.config()
|
||||
.app
|
||||
.windows
|
||||
.iter()
|
||||
.find(|window| window.label == "main")
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"configured main window is missing",
|
||||
)
|
||||
})?;
|
||||
let mut main_window = tauri::WebviewWindowBuilder::from_config(
|
||||
app.handle(),
|
||||
&main_window_config,
|
||||
)?;
|
||||
#[cfg(windows)]
|
||||
if let Some(raw_port) = std::env::var_os("WAGGLE_CERTIFIER_WEBVIEW_DEBUG_PORT") {
|
||||
let raw_port = raw_port.to_string_lossy();
|
||||
let port = raw_port.parse::<u16>().map_err(|_| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"WAGGLE_CERTIFIER_WEBVIEW_DEBUG_PORT must be an integer",
|
||||
)
|
||||
})?;
|
||||
if port < 1024 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"WAGGLE_CERTIFIER_WEBVIEW_DEBUG_PORT must be >= 1024",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let browser_args = format!(
|
||||
"--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --remote-debugging-port={port}"
|
||||
);
|
||||
main_window = main_window.additional_browser_args(&browser_args);
|
||||
eprintln!(
|
||||
"[waggle] WebView certifier debug endpoint enabled on 127.0.0.1:{port}"
|
||||
);
|
||||
}
|
||||
main_window.build()?;
|
||||
|
||||
tray::setup_tray(app.handle())?;
|
||||
|
||||
// Register global hotkey: Ctrl+Shift+W to toggle window visibility
|
||||
@@ -90,18 +136,17 @@ pub fn run() {
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-start the sidecar service before the webview loads so the
|
||||
// React app finds it already healthy on localhost:3333.
|
||||
// Auto-start an owned sidecar launch before the webview loads.
|
||||
// Its verified endpoint may differ from the preferred port.
|
||||
let service_state = app.state::<ServiceState>();
|
||||
let port = service_state.port;
|
||||
match service::spawn_service_sync(port, &service_state.process) {
|
||||
Ok(()) => eprintln!("[waggle] Sidecar spawn initiated on port {}", port),
|
||||
match service::spawn_service_sync(&service_state) {
|
||||
Ok(()) => eprintln!("[waggle] Owned sidecar spawn initiated"),
|
||||
Err(e) => eprintln!("[waggle] Failed to auto-start sidecar: {}", e),
|
||||
}
|
||||
|
||||
// Start service watchdog
|
||||
let app_handle_watchdog = app.handle().clone();
|
||||
service::start_watchdog(app_handle_watchdog, port);
|
||||
service::start_watchdog(app_handle_watchdog);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -115,15 +160,10 @@ pub fn run() {
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| {
|
||||
// R7-002: kill the sidecar on app exit so it doesn't orphan and hold port 3333.
|
||||
// R7-002: kill only the owned sidecar launch on app exit.
|
||||
if let tauri::RunEvent::Exit = event {
|
||||
if let Some(state) = app_handle.try_state::<ServiceState>() {
|
||||
if let Ok(mut proc) = state.process.lock() {
|
||||
if let Some(mut child) = proc.take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
let _ = service::stop_service_sync(&state);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
use tauri::{
|
||||
image::Image,
|
||||
menu::{MenuBuilder, MenuItemBuilder},
|
||||
tray::TrayIconBuilder,
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder},
|
||||
AppHandle, Emitter, Manager,
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
TrayIconBuilder::new()
|
||||
.icon(icon)
|
||||
.menu(&menu)
|
||||
.show_menu_on_left_click(false)
|
||||
.tooltip("Waggle Agent Service")
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"show" => {
|
||||
@@ -65,7 +66,12 @@ pub fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let tauri::tray::TrayIconEvent::Click { .. } = event {
|
||||
if let tauri::tray::TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
show_main_window(tray.app_handle());
|
||||
}
|
||||
})
|
||||
|
||||
@@ -28,21 +28,19 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Waggle",
|
||||
"create": false,
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600,
|
||||
"dataDirectory": "webview",
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https://us.i.posthog.com; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:"
|
||||
},
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"tooltip": "Waggle - AI Agent Swarm"
|
||||
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https://us.i.posthog.com; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -101,8 +101,9 @@ describe('auto-update configuration', () => {
|
||||
expect(workflow).toContain('x86_64-apple-darwin');
|
||||
});
|
||||
|
||||
it('uses tauri-action for builds', () => {
|
||||
expect(workflow).toContain('tauri-apps/tauri-action');
|
||||
it('uses the app-lockfile-pinned Tauri CLI for builds', () => {
|
||||
expect(workflow).toContain('node node_modules/@tauri-apps/cli/tauri.js build');
|
||||
expect(workflow).not.toMatch(/^\s*uses:\s+tauri-apps\/tauri-action/m);
|
||||
});
|
||||
|
||||
it('does NOT publish a broken (empty-signature) updater manifest', () => {
|
||||
|
||||
Reference in New Issue
Block a user