@moneysiren/app 0.1.0-alpha.10
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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/apps/cli/src/cli.d.ts +59 -0
- package/dist/apps/cli/src/cli.js +199 -0
- package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
- package/dist/apps/cli/src/commands/dashboard.js +239 -0
- package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
- package/dist/apps/cli/src/commands/doctor.js +25 -0
- package/dist/apps/cli/src/commands/init.d.ts +3 -0
- package/dist/apps/cli/src/commands/init.js +18 -0
- package/dist/apps/cli/src/commands/install.d.ts +3 -0
- package/dist/apps/cli/src/commands/install.js +244 -0
- package/dist/apps/cli/src/commands/modes.d.ts +3 -0
- package/dist/apps/cli/src/commands/modes.js +73 -0
- package/dist/apps/cli/src/commands/notify.d.ts +3 -0
- package/dist/apps/cli/src/commands/notify.js +430 -0
- package/dist/apps/cli/src/commands/report.d.ts +3 -0
- package/dist/apps/cli/src/commands/report.js +206 -0
- package/dist/apps/cli/src/commands/runtime.d.ts +10 -0
- package/dist/apps/cli/src/commands/runtime.js +499 -0
- package/dist/apps/cli/src/commands/shared.d.ts +9 -0
- package/dist/apps/cli/src/commands/shared.js +29 -0
- package/dist/apps/cli/src/commands/summary.d.ts +3 -0
- package/dist/apps/cli/src/commands/summary.js +15 -0
- package/dist/apps/cli/src/commands/sync.d.ts +3 -0
- package/dist/apps/cli/src/commands/sync.js +393 -0
- package/dist/apps/cli/src/commands/theme.d.ts +3 -0
- package/dist/apps/cli/src/commands/theme.js +181 -0
- package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
- package/dist/apps/cli/src/desktop-runtime.js +720 -0
- package/dist/apps/cli/src/home.d.ts +7 -0
- package/dist/apps/cli/src/home.js +124 -0
- package/dist/apps/cli/src/index.d.ts +3 -0
- package/dist/apps/cli/src/index.js +14 -0
- package/dist/apps/cli/src/install-profile.d.ts +35 -0
- package/dist/apps/cli/src/install-profile.js +124 -0
- package/dist/apps/cli/src/install-selector.d.ts +10 -0
- package/dist/apps/cli/src/install-selector.js +66 -0
- package/dist/apps/cli/src/interactive.d.ts +3 -0
- package/dist/apps/cli/src/interactive.js +32 -0
- package/dist/apps/cli/src/postinstall.d.ts +3 -0
- package/dist/apps/cli/src/postinstall.js +42 -0
- package/dist/apps/cli/src/release-installer.d.ts +57 -0
- package/dist/apps/cli/src/release-installer.js +432 -0
- package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
- package/dist/apps/cli/src/runtime-adapter.js +185 -0
- package/dist/apps/cli/src/slash.d.ts +15 -0
- package/dist/apps/cli/src/slash.js +229 -0
- package/dist/apps/cli/src/summary-model.d.ts +51 -0
- package/dist/apps/cli/src/summary-model.js +136 -0
- package/dist/apps/cli/src/theme.d.ts +18 -0
- package/dist/apps/cli/src/theme.js +118 -0
- package/dist/apps/cli/src/version.d.ts +2 -0
- package/dist/apps/cli/src/version.js +2 -0
- package/dist/packages/config/src/index.d.ts +3 -0
- package/dist/packages/config/src/index.js +3 -0
- package/dist/packages/config/src/load.d.ts +3 -0
- package/dist/packages/config/src/load.js +80 -0
- package/dist/packages/config/src/schema.d.ts +49 -0
- package/dist/packages/config/src/schema.js +28 -0
- package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
- package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
- package/dist/packages/connectors/aws/src/index.d.ts +35 -0
- package/dist/packages/connectors/aws/src/index.js +67 -0
- package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
- package/dist/packages/connectors/aws/src/normalize.js +141 -0
- package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
- package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
- package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
- package/dist/packages/connectors/cloudflare/src/client.js +107 -0
- package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
- package/dist/packages/connectors/cloudflare/src/index.js +81 -0
- package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
- package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
- package/dist/packages/connectors/mock/src/index.d.ts +58 -0
- package/dist/packages/connectors/mock/src/index.js +66 -0
- package/dist/packages/connectors/openai/src/index.d.ts +55 -0
- package/dist/packages/connectors/openai/src/index.js +169 -0
- package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
- package/dist/packages/connectors/openai/src/normalize.js +180 -0
- package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
- package/dist/packages/connectors/supabase/src/client.js +132 -0
- package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
- package/dist/packages/connectors/supabase/src/index.js +87 -0
- package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
- package/dist/packages/connectors/supabase/src/normalize.js +266 -0
- package/dist/packages/core/src/collector.d.ts +12 -0
- package/dist/packages/core/src/collector.js +68 -0
- package/dist/packages/core/src/index.d.ts +5 -0
- package/dist/packages/core/src/index.js +4 -0
- package/dist/packages/core/src/provider.d.ts +18 -0
- package/dist/packages/core/src/provider.js +2 -0
- package/dist/packages/core/src/risk-engine.d.ts +9 -0
- package/dist/packages/core/src/risk-engine.js +4 -0
- package/dist/packages/core/src/snapshots.d.ts +49 -0
- package/dist/packages/core/src/snapshots.js +9 -0
- package/dist/packages/db/src/client.d.ts +11 -0
- package/dist/packages/db/src/client.js +14 -0
- package/dist/packages/db/src/index.d.ts +6 -0
- package/dist/packages/db/src/index.js +6 -0
- package/dist/packages/db/src/local-store.d.ts +161 -0
- package/dist/packages/db/src/local-store.js +623 -0
- package/dist/packages/db/src/migrate.d.ts +17 -0
- package/dist/packages/db/src/migrate.js +35 -0
- package/dist/packages/db/src/schema.d.ts +5 -0
- package/dist/packages/db/src/schema.js +120 -0
- package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
- package/dist/packages/db/src/sqlite-bin.js +16 -0
- package/dist/packages/local-api/src/index.d.ts +2 -0
- package/dist/packages/local-api/src/index.js +2 -0
- package/dist/packages/local-api/src/server.d.ts +36 -0
- package/dist/packages/local-api/src/server.js +310 -0
- package/dist/packages/report/src/daily.d.ts +24 -0
- package/dist/packages/report/src/daily.js +9 -0
- package/dist/packages/report/src/index.d.ts +4 -0
- package/dist/packages/report/src/index.js +4 -0
- package/dist/packages/report/src/korean.d.ts +3 -0
- package/dist/packages/report/src/korean.js +62 -0
- package/dist/packages/report/src/slack.d.ts +34 -0
- package/dist/packages/report/src/slack.js +134 -0
- package/dist/packages/runtime/src/index.d.ts +2 -0
- package/dist/packages/runtime/src/index.js +2 -0
- package/dist/packages/runtime/src/runtime.d.ts +26 -0
- package/dist/packages/runtime/src/runtime.js +182 -0
- package/dist/packages/view-model/src/hud-model.d.ts +74 -0
- package/dist/packages/view-model/src/hud-model.js +295 -0
- package/dist/packages/view-model/src/index.d.ts +6 -0
- package/dist/packages/view-model/src/index.js +6 -0
- package/dist/packages/view-model/src/notification-preferences-model.d.ts +75 -0
- package/dist/packages/view-model/src/notification-preferences-model.js +400 -0
- package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
- package/dist/packages/view-model/src/notification-preferences.js +36 -0
- package/dist/packages/view-model/src/sync-state.d.ts +47 -0
- package/dist/packages/view-model/src/sync-state.js +140 -0
- package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
- package/dist/packages/view-model/src/usage-progress.js +57 -0
- package/dist/packages/view-model/src/view-model.d.ts +215 -0
- package/dist/packages/view-model/src/view-model.js +826 -0
- package/package.json +40 -0
- package/scripts/postinstall.mjs +69 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, join, posix, resolve, win32 } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export const DEFAULT_RELEASE_REPOSITORY = "ztwz11/moneysiren";
|
|
9
|
+
// Keep the source-free installer pinned to the latest published desktop/web release tag.
|
|
10
|
+
export const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.10";
|
|
11
|
+
const RELEASE_REPOSITORY_ENV_KEY = "MONEYSIREN_RELEASE_REPOSITORY";
|
|
12
|
+
const RELEASE_TAG_ENV_KEY = "MONEYSIREN_RELEASE_TAG";
|
|
13
|
+
const RELEASE_INSTALL_DIR_ENV_KEY = "MONEYSIREN_RELEASE_INSTALL_DIR";
|
|
14
|
+
const RELEASE_PLATFORM_ENV_KEY = "MONEYSIREN_RELEASE_PLATFORM";
|
|
15
|
+
const WINDOWS_SIGNER_THUMBPRINTS_ENV_KEY = "MONEYSIREN_WINDOWS_SIGNER_THUMBPRINTS";
|
|
16
|
+
const ALLOW_UNSIGNED_HUD_ENV_KEY = "MONEYSIREN_ALLOW_UNSIGNED_HUD";
|
|
17
|
+
export async function installReleaseAssets(options) {
|
|
18
|
+
const env = options.env ?? process.env;
|
|
19
|
+
const repository = normalizeRepository(options.repository ?? env[RELEASE_REPOSITORY_ENV_KEY] ?? DEFAULT_RELEASE_REPOSITORY);
|
|
20
|
+
const tag = normalizeTag(options.tag ?? env[RELEASE_TAG_ENV_KEY] ?? DEFAULT_RELEASE_TAG);
|
|
21
|
+
const platform = normalizePlatform(options.platform ?? env[RELEASE_PLATFORM_ENV_KEY] ?? process.platform);
|
|
22
|
+
const configuredInstallDir = options.installDir ?? env[RELEASE_INSTALL_DIR_ENV_KEY];
|
|
23
|
+
const installDir = resolveReleaseInstallDir({
|
|
24
|
+
env,
|
|
25
|
+
...(configuredInstallDir === undefined ? {} : { installDir: configuredInstallDir }),
|
|
26
|
+
platform,
|
|
27
|
+
tag,
|
|
28
|
+
});
|
|
29
|
+
const release = await fetchRelease({
|
|
30
|
+
fetchImpl: options.fetchImpl,
|
|
31
|
+
repository,
|
|
32
|
+
tag,
|
|
33
|
+
});
|
|
34
|
+
const releaseAssets = parseReleaseAssets(release.assets);
|
|
35
|
+
const checksumAssets = releaseAssets.filter((asset) => asset.name.toLowerCase().includes("sha256sums"));
|
|
36
|
+
const requestedSurfaces = options.selectedSurfaces.filter((surface) => surface === "web" || surface === "hud");
|
|
37
|
+
const installedAssets = [];
|
|
38
|
+
await mkdir(installDir, { recursive: true });
|
|
39
|
+
for (const surface of requestedSurfaces) {
|
|
40
|
+
const asset = selectSurfaceAsset(surface, platform, releaseAssets);
|
|
41
|
+
if (asset === null) {
|
|
42
|
+
throw new Error(`No ${surface} release asset found for ${platform} in ${repository}@${tag}.`);
|
|
43
|
+
}
|
|
44
|
+
const downloaded = await downloadAsset(options.fetchImpl, asset.browser_download_url);
|
|
45
|
+
const sha256 = sha256Hex(downloaded);
|
|
46
|
+
const checksum = await findChecksum({
|
|
47
|
+
assetName: asset.name,
|
|
48
|
+
checksumAssets,
|
|
49
|
+
fetchImpl: options.fetchImpl,
|
|
50
|
+
});
|
|
51
|
+
if (checksumAssets.length > 0 && checksum === null) {
|
|
52
|
+
throw new Error(`SHA256 checksum entry missing for ${asset.name}.`);
|
|
53
|
+
}
|
|
54
|
+
if (checksum !== null && checksum.toLowerCase() !== sha256) {
|
|
55
|
+
throw new Error(`SHA256 mismatch for ${asset.name}.`);
|
|
56
|
+
}
|
|
57
|
+
const outputPath = join(installDir, sanitizeAssetFileName(asset.name));
|
|
58
|
+
await writeFile(outputPath, downloaded);
|
|
59
|
+
let signature;
|
|
60
|
+
try {
|
|
61
|
+
signature = await verifyReleaseAssetSignature({
|
|
62
|
+
assetName: asset.name,
|
|
63
|
+
env,
|
|
64
|
+
fetchImpl: options.fetchImpl,
|
|
65
|
+
path: outputPath,
|
|
66
|
+
platform,
|
|
67
|
+
releaseAssets,
|
|
68
|
+
surface,
|
|
69
|
+
tag,
|
|
70
|
+
...(options.signatureVerifier === undefined ? {} : { signatureVerifier: options.signatureVerifier }),
|
|
71
|
+
...(options.trustedWindowsSignerThumbprints === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: { trustedWindowsSignerThumbprints: options.trustedWindowsSignerThumbprints }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
await unlink(outputPath).catch(() => undefined);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (!signature.verified) {
|
|
81
|
+
await unlink(outputPath).catch(() => undefined);
|
|
82
|
+
throw new Error(`Release asset signature verification failed for ${asset.name}: ${signature.status} ${signature.message}`.trim());
|
|
83
|
+
}
|
|
84
|
+
installedAssets.push({
|
|
85
|
+
surface,
|
|
86
|
+
name: asset.name,
|
|
87
|
+
path: outputPath,
|
|
88
|
+
size: downloaded.byteLength,
|
|
89
|
+
sha256,
|
|
90
|
+
checksumVerified: checksum !== null,
|
|
91
|
+
signatureVerified: isVerifiedSignatureStatus(signature.status),
|
|
92
|
+
signatureStatus: signature.status,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
await writeFile(join(installDir, "install-manifest.json"), `${JSON.stringify({
|
|
96
|
+
version: 1,
|
|
97
|
+
repository,
|
|
98
|
+
tag,
|
|
99
|
+
releaseUrl: typeof release.html_url === "string" ? release.html_url : releaseUrl(repository, tag),
|
|
100
|
+
installedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
101
|
+
selectedSurfaces: options.selectedSurfaces,
|
|
102
|
+
assets: installedAssets.map((asset) => ({
|
|
103
|
+
surface: asset.surface,
|
|
104
|
+
name: asset.name,
|
|
105
|
+
path: asset.path,
|
|
106
|
+
size: asset.size,
|
|
107
|
+
sha256: asset.sha256,
|
|
108
|
+
checksumVerified: asset.checksumVerified,
|
|
109
|
+
signatureVerified: asset.signatureVerified,
|
|
110
|
+
signatureStatus: asset.signatureStatus,
|
|
111
|
+
})),
|
|
112
|
+
}, null, 2)}\n`, "utf8");
|
|
113
|
+
return {
|
|
114
|
+
repository,
|
|
115
|
+
tag,
|
|
116
|
+
installDir,
|
|
117
|
+
releaseUrl: typeof release.html_url === "string" ? release.html_url : releaseUrl(repository, tag),
|
|
118
|
+
assets: installedAssets,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function resolveReleaseInstallDir(input = {}) {
|
|
122
|
+
const env = input.env ?? process.env;
|
|
123
|
+
const platform = input.platform ?? process.platform;
|
|
124
|
+
const tag = input.tag ?? DEFAULT_RELEASE_TAG;
|
|
125
|
+
const configured = trimToNull(input.installDir ?? env[RELEASE_INSTALL_DIR_ENV_KEY]);
|
|
126
|
+
if (configured !== null) {
|
|
127
|
+
return isAbsoluteForPlatform(platform, configured) ? configured : resolve(process.cwd(), configured);
|
|
128
|
+
}
|
|
129
|
+
const root = platform === "win32"
|
|
130
|
+
? joinForPlatform(platform, trimToNull(env.APPDATA) ?? win32.join(resolveHomeDirectory(env), "AppData", "Roaming"), "MoneySiren")
|
|
131
|
+
: platform === "darwin"
|
|
132
|
+
? joinForPlatform(platform, resolveHomeDirectory(env), "Library", "Application Support", "MoneySiren")
|
|
133
|
+
: joinForPlatform(platform, trimToNull(env.XDG_DATA_HOME) ?? joinForPlatform(platform, resolveHomeDirectory(env), ".local", "share"), "moneysiren");
|
|
134
|
+
return joinForPlatform(platform, root, "releases", sanitizePathSegment(tag));
|
|
135
|
+
}
|
|
136
|
+
function normalizeRepository(repository) {
|
|
137
|
+
const normalized = repository.trim();
|
|
138
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
139
|
+
throw new Error("Release repository must be in owner/name form.");
|
|
140
|
+
}
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
function normalizeTag(tag) {
|
|
144
|
+
const normalized = tag.trim();
|
|
145
|
+
if (normalized.length === 0 || normalized.length > 128) {
|
|
146
|
+
throw new Error("Release tag is empty or too long.");
|
|
147
|
+
}
|
|
148
|
+
return normalized;
|
|
149
|
+
}
|
|
150
|
+
function normalizePlatform(platform) {
|
|
151
|
+
if (platform === "win32" || platform === "darwin" || platform === "linux") {
|
|
152
|
+
return platform;
|
|
153
|
+
}
|
|
154
|
+
return process.platform;
|
|
155
|
+
}
|
|
156
|
+
async function fetchRelease(input) {
|
|
157
|
+
const response = await input.fetchImpl(`https://api.github.com/repos/${input.repository}/releases/tags/${encodeURIComponent(input.tag)}`, {
|
|
158
|
+
headers: {
|
|
159
|
+
Accept: "application/vnd.github+json",
|
|
160
|
+
"User-Agent": "moneysiren-cli-release-installer",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`Could not read GitHub Release ${input.repository}@${input.tag}: ${response.status} ${response.statusText}`);
|
|
165
|
+
}
|
|
166
|
+
const body = await response.json();
|
|
167
|
+
if (!isRecord(body)) {
|
|
168
|
+
throw new Error("GitHub Release response was not an object.");
|
|
169
|
+
}
|
|
170
|
+
return body;
|
|
171
|
+
}
|
|
172
|
+
function parseReleaseAssets(value) {
|
|
173
|
+
if (!Array.isArray(value)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
return value
|
|
177
|
+
.filter(isRecord)
|
|
178
|
+
.flatMap((asset) => {
|
|
179
|
+
const name = asset.name;
|
|
180
|
+
const browserDownloadUrl = asset.browser_download_url;
|
|
181
|
+
if (typeof name !== "string" || typeof browserDownloadUrl !== "string") {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
return [{
|
|
185
|
+
name,
|
|
186
|
+
browser_download_url: browserDownloadUrl,
|
|
187
|
+
...(typeof asset.size === "number" ? { size: asset.size } : {}),
|
|
188
|
+
}];
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function selectSurfaceAsset(surface, platform, assets) {
|
|
192
|
+
const candidates = assets.filter((asset) => !asset.name.toLowerCase().includes("sha256sums"));
|
|
193
|
+
if (surface === "web") {
|
|
194
|
+
return candidates.find((asset) => /^moneysiren-web-runtime-.+\.tar\.gz$/i.test(asset.name)) ?? null;
|
|
195
|
+
}
|
|
196
|
+
if (platform === "win32") {
|
|
197
|
+
return candidates.find(isDirectWindowsHudAsset) ??
|
|
198
|
+
candidates.find((asset) => isWindowsHudAsset(asset.name)) ??
|
|
199
|
+
null;
|
|
200
|
+
}
|
|
201
|
+
if (platform === "darwin") {
|
|
202
|
+
return candidates.find((asset) => /macos/i.test(asset.name) && /\.(tar\.gz|dmg)$/i.test(asset.name)) ?? null;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
function isDirectWindowsHudAsset(asset) {
|
|
207
|
+
return isWindowsHudAsset(asset.name) &&
|
|
208
|
+
/\.exe$/i.test(asset.name) &&
|
|
209
|
+
!isInstallerLikeWindowsAsset(asset.name);
|
|
210
|
+
}
|
|
211
|
+
function isWindowsHudAsset(name) {
|
|
212
|
+
return /\.(exe|msi)$/i.test(name);
|
|
213
|
+
}
|
|
214
|
+
function isInstallerLikeWindowsAsset(name) {
|
|
215
|
+
return /\.msi$/i.test(name) || /(?:^|[._ -])(?:setup|install|installer)(?:[._ -]|$)/i.test(name);
|
|
216
|
+
}
|
|
217
|
+
async function findChecksum(input) {
|
|
218
|
+
for (const checksumAsset of input.checksumAssets) {
|
|
219
|
+
const content = await downloadAsset(input.fetchImpl, checksumAsset.browser_download_url);
|
|
220
|
+
const checksum = parseChecksumFile(content.toString("utf8"), input.assetName);
|
|
221
|
+
if (checksum !== null) {
|
|
222
|
+
return checksum;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
async function downloadAsset(fetchImpl, url) {
|
|
228
|
+
const parsed = new URL(url);
|
|
229
|
+
if (parsed.protocol !== "https:") {
|
|
230
|
+
throw new Error("Refusing to download a non-HTTPS release asset.");
|
|
231
|
+
}
|
|
232
|
+
const response = await fetchImpl(url, {
|
|
233
|
+
headers: {
|
|
234
|
+
"User-Agent": "moneysiren-cli-release-installer",
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
throw new Error(`Could not download release asset: ${response.status} ${response.statusText}`);
|
|
239
|
+
}
|
|
240
|
+
return Buffer.from(await response.arrayBuffer());
|
|
241
|
+
}
|
|
242
|
+
function parseChecksumFile(content, assetName) {
|
|
243
|
+
for (const line of content.split(/\r?\n/)) {
|
|
244
|
+
const match = /^([a-f0-9]{64})\s+\*?(.+)$/i.exec(line.trim());
|
|
245
|
+
if (match !== null && basename(match[2] ?? "") === assetName) {
|
|
246
|
+
return match[1] ?? null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function sha256Hex(content) {
|
|
252
|
+
return createHash("sha256").update(content).digest("hex");
|
|
253
|
+
}
|
|
254
|
+
async function verifyReleaseAssetSignature(input) {
|
|
255
|
+
const verifier = input.signatureVerifier ?? defaultReleaseAssetSignatureVerifier;
|
|
256
|
+
const expectedSignerThumbprints = await findExpectedSignerThumbprints({
|
|
257
|
+
assetName: input.assetName,
|
|
258
|
+
env: input.env,
|
|
259
|
+
fetchImpl: input.fetchImpl,
|
|
260
|
+
platform: input.platform,
|
|
261
|
+
releaseAssets: input.releaseAssets,
|
|
262
|
+
surface: input.surface,
|
|
263
|
+
...(input.trustedWindowsSignerThumbprints === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { trustedWindowsSignerThumbprints: input.trustedWindowsSignerThumbprints }),
|
|
266
|
+
});
|
|
267
|
+
return verifier.verify({
|
|
268
|
+
assetName: input.assetName,
|
|
269
|
+
env: input.env,
|
|
270
|
+
...(expectedSignerThumbprints === null ? {} : { expectedSignerThumbprints }),
|
|
271
|
+
path: input.path,
|
|
272
|
+
platform: input.platform,
|
|
273
|
+
surface: input.surface,
|
|
274
|
+
tag: input.tag,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const defaultReleaseAssetSignatureVerifier = {
|
|
278
|
+
async verify(input) {
|
|
279
|
+
if (input.surface !== "hud" || input.platform !== "win32") {
|
|
280
|
+
return {
|
|
281
|
+
verified: true,
|
|
282
|
+
status: "not-required",
|
|
283
|
+
message: "No platform signature check is required for this release asset.",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (!/\.(exe|msi)$/i.test(input.assetName)) {
|
|
287
|
+
return {
|
|
288
|
+
verified: false,
|
|
289
|
+
status: "unsupported",
|
|
290
|
+
message: "Windows HUD release assets must be .exe or .msi artifacts.",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (input.expectedSignerThumbprints === undefined || input.expectedSignerThumbprints.length === 0) {
|
|
294
|
+
if (isUnsignedPrereleaseHudAllowed(input.env, input.tag)) {
|
|
295
|
+
return {
|
|
296
|
+
verified: true,
|
|
297
|
+
status: "unsigned-prerelease-accepted",
|
|
298
|
+
message: "Unsigned Windows HUD artifact accepted for alpha prerelease.",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
verified: false,
|
|
303
|
+
status: "missing-signature-metadata",
|
|
304
|
+
message: `Windows HUD release assets require ${WINDOWS_SIGNER_THUMBPRINTS_ENV_KEY} or moneysiren-tray-windows-SIGNATURE.json metadata.`,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return verifyWindowsAuthenticodeSignature(input.path, input.expectedSignerThumbprints);
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
async function findExpectedSignerThumbprints(input) {
|
|
311
|
+
if (input.surface !== "hud" || input.platform !== "win32") {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const trustedThumbprints = normalizeThumbprintList([
|
|
315
|
+
...(input.trustedWindowsSignerThumbprints ?? []),
|
|
316
|
+
...parseThumbprintEnv(input.env[WINDOWS_SIGNER_THUMBPRINTS_ENV_KEY]),
|
|
317
|
+
]);
|
|
318
|
+
if (trustedThumbprints.length > 0) {
|
|
319
|
+
return trustedThumbprints;
|
|
320
|
+
}
|
|
321
|
+
const metadataAsset = input.releaseAssets.find((asset) => /^moneysiren-tray-windows-SIGNATURE\.json$/i.test(asset.name));
|
|
322
|
+
if (metadataAsset === undefined) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const metadata = JSON.parse((await downloadAsset(input.fetchImpl, metadataAsset.browser_download_url)).toString("utf8"));
|
|
326
|
+
const entries = Array.isArray(metadata) ? metadata : [metadata];
|
|
327
|
+
for (const entry of entries) {
|
|
328
|
+
if (!isRecord(entry) || entry.assetName !== input.assetName || typeof entry.signerThumbprint !== "string") {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
return [normalizeThumbprint(entry.signerThumbprint)];
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
function isVerifiedSignatureStatus(status) {
|
|
336
|
+
return status !== "not-required" && status !== "unsigned-prerelease-accepted";
|
|
337
|
+
}
|
|
338
|
+
function isUnsignedPrereleaseHudAllowed(env, tag) {
|
|
339
|
+
const configured = env[ALLOW_UNSIGNED_HUD_ENV_KEY]?.trim().toLowerCase();
|
|
340
|
+
if (configured !== undefined && configured.length > 0) {
|
|
341
|
+
return ["1", "true", "yes", "on"].includes(configured);
|
|
342
|
+
}
|
|
343
|
+
return /-(?:alpha|beta|rc)(?:[.\d-]*)?$/i.test(tag);
|
|
344
|
+
}
|
|
345
|
+
async function verifyWindowsAuthenticodeSignature(path, expectedSignerThumbprints) {
|
|
346
|
+
const literalPath = powerShellSingleQuotedString(path);
|
|
347
|
+
try {
|
|
348
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
349
|
+
"-NoProfile",
|
|
350
|
+
"-NonInteractive",
|
|
351
|
+
"-Command",
|
|
352
|
+
[
|
|
353
|
+
`$signature = Get-AuthenticodeSignature -LiteralPath ${literalPath}`,
|
|
354
|
+
"$status = [string]$signature.Status",
|
|
355
|
+
"$message = [string]$signature.StatusMessage",
|
|
356
|
+
"if ($signature.Status -ne 'Valid' -or $null -eq $signature.SignerCertificate) {",
|
|
357
|
+
" Write-Output ($status + \"|\" + $message)",
|
|
358
|
+
" exit 1",
|
|
359
|
+
"}",
|
|
360
|
+
"Write-Output ($status + \"|\" + $signature.SignerCertificate.Thumbprint + \"|\" + $signature.SignerCertificate.Subject)",
|
|
361
|
+
].join("; "),
|
|
362
|
+
], {
|
|
363
|
+
windowsHide: true,
|
|
364
|
+
timeout: 30_000,
|
|
365
|
+
});
|
|
366
|
+
const [status, signerThumbprint, ...messageParts] = stdout.trim().split("|");
|
|
367
|
+
const normalizedSignerThumbprint = normalizeThumbprint(signerThumbprint ?? "");
|
|
368
|
+
const normalizedExpectedSignerThumbprints = expectedSignerThumbprints.map(normalizeThumbprint);
|
|
369
|
+
if (!normalizedExpectedSignerThumbprints.includes(normalizedSignerThumbprint)) {
|
|
370
|
+
return {
|
|
371
|
+
verified: false,
|
|
372
|
+
status: "signer-mismatch",
|
|
373
|
+
message: `Expected signer ${normalizedExpectedSignerThumbprints.join(", ")}, got ${normalizedSignerThumbprint || "unknown"}.`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
verified: true,
|
|
378
|
+
status: status ?? "Valid",
|
|
379
|
+
message: messageParts.join("|"),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
const output = isRecord(error) && typeof error.stdout === "string" ? error.stdout.trim() : "";
|
|
384
|
+
const [status, ...messageParts] = output.split("|");
|
|
385
|
+
return {
|
|
386
|
+
verified: false,
|
|
387
|
+
status: status && status.length > 0 ? status : "Unknown",
|
|
388
|
+
message: messageParts.join("|") || (error instanceof Error ? error.message : String(error)),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function normalizeThumbprint(value) {
|
|
393
|
+
return value.replaceAll(/\s/g, "").toUpperCase();
|
|
394
|
+
}
|
|
395
|
+
function normalizeThumbprintList(values) {
|
|
396
|
+
return Array.from(new Set(values.map(normalizeThumbprint).filter((value) => value.length > 0)));
|
|
397
|
+
}
|
|
398
|
+
function parseThumbprintEnv(value) {
|
|
399
|
+
if (value === undefined) {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
return value.split(/[,\s;]+/).map((part) => part.trim()).filter((part) => part.length > 0);
|
|
403
|
+
}
|
|
404
|
+
function powerShellSingleQuotedString(value) {
|
|
405
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
406
|
+
}
|
|
407
|
+
function sanitizeAssetFileName(name) {
|
|
408
|
+
return basename(name).replace(/[^A-Za-z0-9._ -]/g, "_");
|
|
409
|
+
}
|
|
410
|
+
function sanitizePathSegment(value) {
|
|
411
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
412
|
+
}
|
|
413
|
+
function releaseUrl(repository, tag) {
|
|
414
|
+
return `https://github.com/${repository}/releases/tag/${encodeURIComponent(tag)}`;
|
|
415
|
+
}
|
|
416
|
+
function resolveHomeDirectory(env) {
|
|
417
|
+
return trimToNull(env.HOME) ?? trimToNull(env.USERPROFILE) ?? homedir();
|
|
418
|
+
}
|
|
419
|
+
function trimToNull(value) {
|
|
420
|
+
const trimmed = value?.trim();
|
|
421
|
+
return trimmed === undefined || trimmed.length === 0 ? null : trimmed;
|
|
422
|
+
}
|
|
423
|
+
function joinForPlatform(platform, ...segments) {
|
|
424
|
+
return platform === "win32" ? win32.join(...segments) : posix.join(...segments);
|
|
425
|
+
}
|
|
426
|
+
function isAbsoluteForPlatform(platform, value) {
|
|
427
|
+
return platform === "win32" ? win32.isAbsolute(value) : posix.isAbsolute(value);
|
|
428
|
+
}
|
|
429
|
+
function isRecord(value) {
|
|
430
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
431
|
+
}
|
|
432
|
+
//# sourceMappingURL=release-installer.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type LocalRuntime } from "../../../packages/runtime/src/index.js";
|
|
2
|
+
import type { CliExecutionContext } from "./cli.js";
|
|
3
|
+
export type { LocalRuntime } from "../../../packages/runtime/src/index.js";
|
|
4
|
+
export interface StartRuntimeOptions {
|
|
5
|
+
openBrowser?: boolean;
|
|
6
|
+
port?: number;
|
|
7
|
+
headless?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type StartRuntimeResult = {
|
|
10
|
+
status: "running" | "started";
|
|
11
|
+
runtime: LocalRuntime;
|
|
12
|
+
} | {
|
|
13
|
+
status: "unavailable";
|
|
14
|
+
reason: string;
|
|
15
|
+
guidance: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
export interface CliLocalRuntimeAdapter {
|
|
18
|
+
findRuntime(): Promise<LocalRuntime | null>;
|
|
19
|
+
assertRuntimeHealthy(runtime: LocalRuntime): Promise<boolean>;
|
|
20
|
+
startRuntime(options: StartRuntimeOptions): Promise<StartRuntimeResult>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createFallbackLocalRuntimeAdapter(context: CliExecutionContext): CliLocalRuntimeAdapter;
|
|
23
|
+
export declare function openUrlInBrowser(url: string): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=runtime-adapter.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { startLocalApiServer } from "../../../packages/local-api/src/index.js";
|
|
4
|
+
import { assertRuntimeHealthy as assertPackageRuntimeHealthy, findRuntime, } from "../../../packages/runtime/src/index.js";
|
|
5
|
+
import { readLocalStore } from "../../../packages/db/src/index.js";
|
|
6
|
+
import { loadCliConfig, resolveDbPath } from "./commands/shared.js";
|
|
7
|
+
let activeLocalApiServer = null;
|
|
8
|
+
let registeredShutdownHandlers = false;
|
|
9
|
+
export function createFallbackLocalRuntimeAdapter(context) {
|
|
10
|
+
return {
|
|
11
|
+
async findRuntime() {
|
|
12
|
+
return findRuntime({
|
|
13
|
+
cwd: context.cwd,
|
|
14
|
+
env: context.env,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
async assertRuntimeHealthy(runtime) {
|
|
18
|
+
return assertPackageRuntimeHealthy(runtime, {
|
|
19
|
+
fetchImpl: context.fetch,
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
async startRuntime(options) {
|
|
23
|
+
const runtime = await findRuntime({
|
|
24
|
+
cwd: context.cwd,
|
|
25
|
+
env: context.env,
|
|
26
|
+
});
|
|
27
|
+
if (runtime !== null && await assertPackageRuntimeHealthy(runtime, {
|
|
28
|
+
fetchImpl: context.fetch,
|
|
29
|
+
})) {
|
|
30
|
+
return {
|
|
31
|
+
status: "running",
|
|
32
|
+
runtime,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (activeLocalApiServer !== null) {
|
|
36
|
+
return {
|
|
37
|
+
status: "running",
|
|
38
|
+
runtime: activeLocalApiServer.runtime,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const api = await startLocalApiServer({
|
|
42
|
+
...(options.port === undefined ? {} : { port: options.port }),
|
|
43
|
+
runtimeLock: {
|
|
44
|
+
cwd: context.cwd,
|
|
45
|
+
env: context.env,
|
|
46
|
+
},
|
|
47
|
+
viewModel: {
|
|
48
|
+
now: context.now,
|
|
49
|
+
readStore: () => readViewModelStore(context),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
activeLocalApiServer = api;
|
|
53
|
+
registerShutdownHandlers();
|
|
54
|
+
return {
|
|
55
|
+
status: "started",
|
|
56
|
+
runtime: api.runtime,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function openUrlInBrowser(url) {
|
|
62
|
+
const parsedUrl = new URL(url);
|
|
63
|
+
if (!isLoopbackHttpUrl(parsedUrl)) {
|
|
64
|
+
throw new Error("Refusing to open a non-loopback runtime URL.");
|
|
65
|
+
}
|
|
66
|
+
const child = process.platform === "win32"
|
|
67
|
+
? spawn("rundll32.exe", ["url.dll,FileProtocolHandler", parsedUrl.toString()], {
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: "ignore",
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
})
|
|
72
|
+
: process.platform === "darwin"
|
|
73
|
+
? spawn("open", [parsedUrl.toString()], {
|
|
74
|
+
detached: true,
|
|
75
|
+
stdio: "ignore",
|
|
76
|
+
})
|
|
77
|
+
: spawn("xdg-open", [parsedUrl.toString()], {
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: "ignore",
|
|
80
|
+
});
|
|
81
|
+
child.unref();
|
|
82
|
+
}
|
|
83
|
+
function isLoopbackHttpUrl(url) {
|
|
84
|
+
return url.protocol === "http:" &&
|
|
85
|
+
(url.hostname === "127.0.0.1" ||
|
|
86
|
+
url.hostname === "localhost" ||
|
|
87
|
+
url.hostname === "::1" ||
|
|
88
|
+
url.hostname === "[::1]");
|
|
89
|
+
}
|
|
90
|
+
async function readViewModelStore(context) {
|
|
91
|
+
const config = loadCliConfig(context.env);
|
|
92
|
+
const dbPath = resolveDbPath(context.cwd, config.dbPath);
|
|
93
|
+
if (!await pathExists(dbPath)) {
|
|
94
|
+
return emptyViewModelStore();
|
|
95
|
+
}
|
|
96
|
+
return localStoreToViewModelStore(await readLocalStore({ dbPath }));
|
|
97
|
+
}
|
|
98
|
+
function localStoreToViewModelStore(store) {
|
|
99
|
+
return {
|
|
100
|
+
providers: store.providers.map((provider) => ({
|
|
101
|
+
key: provider.key,
|
|
102
|
+
displayName: provider.displayName,
|
|
103
|
+
})),
|
|
104
|
+
usageSnapshots: store.usageSnapshots.map((snapshot) => ({
|
|
105
|
+
providerKey: snapshot.providerKey,
|
|
106
|
+
collectedAt: snapshot.collectedAt,
|
|
107
|
+
service: snapshot.service,
|
|
108
|
+
metric: snapshot.metric,
|
|
109
|
+
unit: snapshot.unit,
|
|
110
|
+
value: snapshot.value,
|
|
111
|
+
})),
|
|
112
|
+
billingSnapshots: store.billingSnapshots.map((snapshot) => ({
|
|
113
|
+
providerKey: snapshot.providerKey,
|
|
114
|
+
collectedAt: snapshot.collectedAt,
|
|
115
|
+
amountMinor: snapshot.amountMinor,
|
|
116
|
+
currency: snapshot.currency,
|
|
117
|
+
status: snapshot.status,
|
|
118
|
+
})),
|
|
119
|
+
serviceHealthSnapshots: store.serviceHealthSnapshots.map((snapshot) => ({
|
|
120
|
+
providerKey: snapshot.providerKey,
|
|
121
|
+
collectedAt: snapshot.collectedAt,
|
|
122
|
+
service: snapshot.service,
|
|
123
|
+
status: snapshot.status,
|
|
124
|
+
...(snapshot.region === undefined ? {} : { region: snapshot.region }),
|
|
125
|
+
...(snapshot.message === undefined ? {} : { message: snapshot.message }),
|
|
126
|
+
})),
|
|
127
|
+
costEstimates: store.costEstimates.map((estimate) => ({
|
|
128
|
+
providerKey: estimate.providerKey,
|
|
129
|
+
collectedAt: estimate.collectedAt,
|
|
130
|
+
estimatedAmountMinor: estimate.estimatedAmountMinor,
|
|
131
|
+
currency: estimate.currency,
|
|
132
|
+
confidence: estimate.confidence,
|
|
133
|
+
})),
|
|
134
|
+
alerts: store.alerts.map((alert) => ({
|
|
135
|
+
...(alert.providerKey === undefined ? {} : { providerKey: alert.providerKey }),
|
|
136
|
+
createdAt: alert.createdAt,
|
|
137
|
+
severity: alert.severity,
|
|
138
|
+
category: alert.category,
|
|
139
|
+
title: alert.title,
|
|
140
|
+
message: alert.message,
|
|
141
|
+
})),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function emptyViewModelStore() {
|
|
145
|
+
return {
|
|
146
|
+
providers: [],
|
|
147
|
+
usageSnapshots: [],
|
|
148
|
+
billingSnapshots: [],
|
|
149
|
+
serviceHealthSnapshots: [],
|
|
150
|
+
costEstimates: [],
|
|
151
|
+
alerts: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function pathExists(path) {
|
|
155
|
+
try {
|
|
156
|
+
await stat(path);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function registerShutdownHandlers() {
|
|
164
|
+
if (registeredShutdownHandlers) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
registeredShutdownHandlers = true;
|
|
168
|
+
const closeActiveServer = () => {
|
|
169
|
+
const server = activeLocalApiServer;
|
|
170
|
+
activeLocalApiServer = null;
|
|
171
|
+
if (server !== null) {
|
|
172
|
+
void server.close();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
process.once("beforeExit", closeActiveServer);
|
|
176
|
+
process.once("SIGINT", () => {
|
|
177
|
+
closeActiveServer();
|
|
178
|
+
process.exit(0);
|
|
179
|
+
});
|
|
180
|
+
process.once("SIGTERM", () => {
|
|
181
|
+
closeActiveServer();
|
|
182
|
+
process.exit(0);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=runtime-adapter.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type SlashDispatch = {
|
|
2
|
+
kind: "dispatch";
|
|
3
|
+
args: readonly string[];
|
|
4
|
+
} | {
|
|
5
|
+
kind: "quit";
|
|
6
|
+
} | {
|
|
7
|
+
kind: "error";
|
|
8
|
+
message: string;
|
|
9
|
+
usage?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function parseSlashInput(input: string): readonly string[];
|
|
12
|
+
export declare function isSlashQuit(args: readonly string[]): boolean;
|
|
13
|
+
export declare function resolveSlashCommand(args: readonly string[]): SlashDispatch;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=slash.d.ts.map
|