everything-dev 1.7.2 → 1.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/dist/api.cjs +1 -1
- package/dist/api.mjs +1 -1
- package/dist/app.cjs +82 -51
- package/dist/app.cjs.map +1 -1
- package/dist/app.mjs +82 -51
- package/dist/app.mjs.map +1 -1
- package/dist/cli/upgrade.cjs.map +1 -1
- package/dist/cli/upgrade.mjs.map +1 -1
- package/dist/components/dev-view.cjs +6 -3
- package/dist/components/dev-view.cjs.map +1 -1
- package/dist/components/dev-view.mjs +6 -3
- package/dist/components/dev-view.mjs.map +1 -1
- package/dist/components/streaming-view.cjs +5 -2
- package/dist/components/streaming-view.cjs.map +1 -1
- package/dist/components/streaming-view.mjs +5 -2
- package/dist/components/streaming-view.mjs.map +1 -1
- package/dist/config.cjs +28 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +28 -5
- package/dist/config.mjs.map +1 -1
- package/dist/contract.cjs +1 -0
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +14 -6
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +14 -6
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.mjs +1 -0
- package/dist/contract.mjs.map +1 -1
- package/dist/dev-logs.cjs +6 -2
- package/dist/dev-logs.cjs.map +1 -1
- package/dist/dev-logs.mjs +7 -2
- package/dist/dev-logs.mjs.map +1 -1
- package/dist/dev-session.cjs +27 -23
- package/dist/dev-session.cjs.map +1 -1
- package/dist/dev-session.mjs +27 -24
- package/dist/dev-session.mjs.map +1 -1
- package/dist/federation.server.cjs +1 -1
- package/dist/federation.server.mjs +1 -1
- package/dist/host.cjs +4 -3
- package/dist/host.cjs.map +1 -1
- package/dist/host.d.cts.map +1 -1
- package/dist/host.d.mts.map +1 -1
- package/dist/host.mjs +4 -3
- package/dist/host.mjs.map +1 -1
- package/dist/integrity.cjs +68 -2
- package/dist/integrity.cjs.map +1 -1
- package/dist/integrity.d.cts +14 -1
- package/dist/integrity.d.cts.map +1 -1
- package/dist/integrity.d.mts +14 -1
- package/dist/integrity.d.mts.map +1 -1
- package/dist/integrity.mjs +66 -3
- package/dist/integrity.mjs.map +1 -1
- package/dist/mf.cjs +32 -0
- package/dist/mf.cjs.map +1 -1
- package/dist/mf.d.cts +3 -1
- package/dist/mf.d.cts.map +1 -1
- package/dist/mf.d.mts +3 -1
- package/dist/mf.d.mts.map +1 -1
- package/dist/mf.mjs +32 -1
- package/dist/mf.mjs.map +1 -1
- package/dist/orchestrator.cjs +167 -317
- package/dist/orchestrator.cjs.map +1 -1
- package/dist/orchestrator.d.cts +24 -21
- package/dist/orchestrator.d.cts.map +1 -1
- package/dist/orchestrator.d.mts +24 -21
- package/dist/orchestrator.d.mts.map +1 -1
- package/dist/orchestrator.mjs +168 -316
- package/dist/orchestrator.mjs.map +1 -1
- package/dist/plugin.cjs +38 -107
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +19 -5
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +19 -5
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +39 -108
- package/dist/plugin.mjs.map +1 -1
- package/dist/service-descriptor.cjs +188 -0
- package/dist/service-descriptor.cjs.map +1 -0
- package/dist/service-descriptor.d.cts +107 -0
- package/dist/service-descriptor.d.cts.map +1 -0
- package/dist/service-descriptor.d.mts +107 -0
- package/dist/service-descriptor.d.mts.map +1 -0
- package/dist/service-descriptor.mjs +182 -0
- package/dist/service-descriptor.mjs.map +1 -0
- package/dist/types.cjs +8 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +18 -3
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +18 -3
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs +8 -1
- package/dist/types.mjs.map +1 -1
- package/dist/ui/index.cjs +1 -0
- package/dist/ui/index.d.cts +2 -2
- package/dist/ui/index.d.mts +2 -2
- package/dist/ui/index.mjs +2 -2
- package/dist/ui/runtime.cjs +4 -0
- package/dist/ui/runtime.cjs.map +1 -1
- package/dist/ui/runtime.d.cts +2 -1
- package/dist/ui/runtime.d.cts.map +1 -1
- package/dist/ui/runtime.d.mts +2 -1
- package/dist/ui/runtime.d.mts.map +1 -1
- package/dist/ui/runtime.mjs +4 -1
- package/dist/ui/runtime.mjs.map +1 -1
- package/package.json +12 -4
- package/skills/dev-workflow/SKILL.md +105 -0
- package/skills/publish-sync/SKILL.md +130 -0
- package/src/app.ts +98 -204
- package/src/cli/upgrade.ts +20 -4
- package/src/components/dev-view.tsx +8 -3
- package/src/components/streaming-view.ts +7 -2
- package/src/config.ts +40 -8
- package/src/contract.ts +1 -0
- package/src/dev-logs.ts +8 -1
- package/src/dev-session.ts +56 -79
- package/src/host.ts +4 -3
- package/src/integrity.ts +96 -10
- package/src/mf.ts +42 -0
- package/src/orchestrator.ts +232 -411
- package/src/plugin.ts +48 -136
- package/src/service-descriptor.ts +258 -0
- package/src/types.ts +8 -1
- package/src/ui/runtime.ts +5 -0
- package/dist/process-registry.cjs +0 -120
- package/dist/process-registry.cjs.map +0 -1
- package/dist/process-registry.d.cts +0 -25
- package/dist/process-registry.d.cts.map +0 -1
- package/dist/process-registry.d.mts +0 -25
- package/dist/process-registry.d.mts.map +0 -1
- package/dist/process-registry.mjs +0 -119
- package/dist/process-registry.mjs.map +0 -1
- package/src/process-registry.ts +0 -154
package/src/config.ts
CHANGED
|
@@ -122,6 +122,18 @@ export async function buildRuntimePluginsForConfig(
|
|
|
122
122
|
return Object.keys(plugins).length > 0 ? plugins : undefined;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function resolveDevelopmentTarget(
|
|
126
|
+
development: string | undefined,
|
|
127
|
+
production: string | undefined,
|
|
128
|
+
baseDir: string,
|
|
129
|
+
): RuntimeTarget {
|
|
130
|
+
const devTarget = resolveRuntimeTarget(development, baseDir);
|
|
131
|
+
if (devTarget.source === "local" && (!devTarget.localPath || !existsSync(devTarget.localPath))) {
|
|
132
|
+
return resolveRuntimeTarget(production, baseDir, "remote");
|
|
133
|
+
}
|
|
134
|
+
return devTarget;
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
function buildRuntimeConfig(
|
|
126
138
|
config: BosConfig,
|
|
127
139
|
baseDir: string,
|
|
@@ -133,28 +145,43 @@ function buildRuntimeConfig(
|
|
|
133
145
|
const authConfig = config.app.auth;
|
|
134
146
|
const uiRuntime =
|
|
135
147
|
env === "development"
|
|
136
|
-
?
|
|
148
|
+
? resolveDevelopmentTarget(uiConfig.development, uiConfig.production, baseDir)
|
|
137
149
|
: resolveRuntimeTarget(uiConfig.production, baseDir, "remote");
|
|
138
150
|
const apiRuntime =
|
|
139
151
|
env === "development"
|
|
140
|
-
?
|
|
152
|
+
? resolveDevelopmentTarget(apiConfig.development, apiConfig.production, baseDir)
|
|
141
153
|
: resolveRuntimeTarget(apiConfig.production, baseDir, "remote");
|
|
142
154
|
const authRuntime = authConfig
|
|
143
155
|
? env === "development"
|
|
144
|
-
?
|
|
156
|
+
? resolveDevelopmentTarget(authConfig.development, authConfig.production, baseDir)
|
|
145
157
|
: resolveRuntimeTarget(authConfig.production, baseDir, "remote")
|
|
146
158
|
: undefined;
|
|
147
159
|
|
|
160
|
+
const hostConfig = config.app.host;
|
|
161
|
+
const hostRuntime =
|
|
162
|
+
env === "development"
|
|
163
|
+
? resolveDevelopmentTarget(hostConfig.development, hostConfig.production, baseDir)
|
|
164
|
+
: resolveRuntimeTarget(hostConfig.production, baseDir, "remote");
|
|
165
|
+
|
|
166
|
+
const hostListeningUrl = resolveDevelopmentHostUrl(hostConfig.development);
|
|
167
|
+
|
|
148
168
|
return {
|
|
149
169
|
env,
|
|
150
170
|
account: config.account,
|
|
151
171
|
domain: config.domain,
|
|
152
172
|
networkId: getNetworkIdForAccount(config.account),
|
|
153
173
|
repository: config.repository,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
host: {
|
|
175
|
+
name: "host",
|
|
176
|
+
url: hostListeningUrl,
|
|
177
|
+
entry: `${hostListeningUrl}/mf-manifest.json`,
|
|
178
|
+
localPath: hostRuntime.localPath,
|
|
179
|
+
port: hostRuntime.port ?? DEFAULT_HOST_PORT,
|
|
180
|
+
secrets: hostConfig.secrets,
|
|
181
|
+
integrity: env === "production" ? hostConfig.integrity : undefined,
|
|
182
|
+
source: hostRuntime.source,
|
|
183
|
+
remoteUrl: hostRuntime.source === "remote" ? hostRuntime.url : undefined,
|
|
184
|
+
},
|
|
158
185
|
shared: config.shared,
|
|
159
186
|
ui: {
|
|
160
187
|
name: uiConfig.name,
|
|
@@ -464,10 +491,15 @@ function resolveRuntimeTarget(
|
|
|
464
491
|
throw new Error(`Invalid local development target: ${value}`);
|
|
465
492
|
}
|
|
466
493
|
|
|
494
|
+
const localPath = resolve(baseDir, localTarget);
|
|
495
|
+
if (!existsSync(localPath)) {
|
|
496
|
+
return { source: defaultSource, url: "" };
|
|
497
|
+
}
|
|
498
|
+
|
|
467
499
|
return {
|
|
468
500
|
source: "local",
|
|
469
501
|
url: "",
|
|
470
|
-
localPath
|
|
502
|
+
localPath,
|
|
471
503
|
};
|
|
472
504
|
}
|
|
473
505
|
|
package/src/contract.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const DevOptionsSchema = z.object({
|
|
|
5
5
|
host: SourceModeSchema.default("local"),
|
|
6
6
|
ui: SourceModeSchema.default("local"),
|
|
7
7
|
api: SourceModeSchema.default("local"),
|
|
8
|
+
auth: SourceModeSchema.default("local"),
|
|
8
9
|
proxy: z.boolean().default(false),
|
|
9
10
|
ssr: z.boolean().default(false),
|
|
10
11
|
port: z.number().optional(),
|
package/src/dev-logs.ts
CHANGED
|
@@ -2,6 +2,12 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
+
const ESC = "\x1b";
|
|
6
|
+
const BEL = "\x07";
|
|
7
|
+
const ANSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]|${ESC}\\][^${BEL}]*${BEL}`, "g");
|
|
8
|
+
|
|
9
|
+
const stripAnsi = (input: string): string => input.replace(ANSI_RE, "");
|
|
10
|
+
|
|
5
11
|
export interface LogEntry {
|
|
6
12
|
timestamp: number;
|
|
7
13
|
source: string;
|
|
@@ -27,7 +33,8 @@ export function getLogsDir(configDir: string): string {
|
|
|
27
33
|
function formatLogLine(entry: LogEntry): string {
|
|
28
34
|
const ts = new Date(entry.timestamp).toISOString();
|
|
29
35
|
const prefix = entry.isError ? "ERR" : "OUT";
|
|
30
|
-
|
|
36
|
+
const clean = stripAnsi(entry.line);
|
|
37
|
+
return `[${ts}] [${entry.source}] [${prefix}] ${clean}`;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export async function createDevLogger(configDir: string, description: string): Promise<DevLogger> {
|
package/src/dev-session.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NodeContext } from "@effect/platform-node";
|
|
1
2
|
import { Deferred, Effect, Exit } from "effect";
|
|
2
3
|
import {
|
|
3
4
|
type DevViewHandle,
|
|
@@ -9,33 +10,19 @@ import { renderStreamingView } from "./components/streaming-view";
|
|
|
9
10
|
import { getProjectRoot } from "./config";
|
|
10
11
|
import { createDevLogger } from "./dev-logs";
|
|
11
12
|
import {
|
|
12
|
-
|
|
13
|
+
getProcessStates,
|
|
13
14
|
makeDevProcess,
|
|
14
15
|
type ProcessCallbacks,
|
|
15
16
|
type ProcessHandle,
|
|
16
17
|
} from "./orchestrator";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ssr?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface AppOrchestrator {
|
|
29
|
-
packages: string[];
|
|
30
|
-
env: Record<string, string>;
|
|
31
|
-
description: string;
|
|
32
|
-
appConfig: AppConfig;
|
|
33
|
-
bosConfig: BosConfig;
|
|
34
|
-
runtimeConfig: RuntimeConfig;
|
|
35
|
-
port?: number;
|
|
36
|
-
interactive?: boolean;
|
|
37
|
-
noLogs?: boolean;
|
|
38
|
-
}
|
|
18
|
+
import {
|
|
19
|
+
type AppOrchestrator,
|
|
20
|
+
DevRuntimeConfigLive,
|
|
21
|
+
type ServiceDescriptor,
|
|
22
|
+
ServiceDescriptorMap,
|
|
23
|
+
ServiceDescriptorMapLive,
|
|
24
|
+
} from "./service-descriptor";
|
|
25
|
+
import type { RuntimeConfig } from "./types";
|
|
39
26
|
|
|
40
27
|
const LOG_NOISE_PATTERNS = [
|
|
41
28
|
/\[ Federation Runtime \] Version .* from (host|ui) of shared singleton module/,
|
|
@@ -64,7 +51,7 @@ const isInteractiveSupported = (): boolean => {
|
|
|
64
51
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
65
52
|
};
|
|
66
53
|
|
|
67
|
-
const STARTUP_ORDER = ["ui-ssr", "ui", "api", "plugin", "host-build", "host"];
|
|
54
|
+
const STARTUP_ORDER = ["ui-ssr", "ui", "auth", "api", "plugin", "host-build", "host"];
|
|
68
55
|
|
|
69
56
|
const sortByOrder = (packages: string[]): string[] => {
|
|
70
57
|
return [...packages].sort((a, b) => {
|
|
@@ -87,54 +74,19 @@ function formatLogLine(entry: LogEntry): string {
|
|
|
87
74
|
return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`;
|
|
88
75
|
}
|
|
89
76
|
|
|
90
|
-
const scopedProcess = (
|
|
91
|
-
pkg: string,
|
|
92
|
-
env: Record<string, string> | undefined,
|
|
93
|
-
callbacks: ProcessCallbacks,
|
|
94
|
-
portOverride: number | undefined,
|
|
95
|
-
bosConfig: BosConfig | undefined,
|
|
96
|
-
runtimeConfig: RuntimeConfig | undefined,
|
|
97
|
-
registry: ProcessRegistry | undefined,
|
|
98
|
-
) =>
|
|
99
|
-
Effect.acquireRelease(
|
|
100
|
-
makeDevProcess(pkg, env, callbacks, portOverride, bosConfig, runtimeConfig, registry),
|
|
101
|
-
(handle) => handle.kill.pipe(Effect.ignore),
|
|
102
|
-
);
|
|
103
|
-
|
|
104
77
|
export const runDevSession = (
|
|
105
78
|
orchestrator: AppOrchestrator,
|
|
106
79
|
onShutdownReady?: (requestShutdown: () => void) => void,
|
|
107
80
|
) =>
|
|
108
81
|
Effect.gen(function* () {
|
|
109
82
|
const configDir = getProjectRoot();
|
|
83
|
+
const services = yield* ServiceDescriptorMap;
|
|
110
84
|
const orderedPackages = sortByOrder(orchestrator.packages);
|
|
111
|
-
const initialProcesses: ProcessState[] =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
portOverride,
|
|
117
|
-
orchestrator.bosConfig,
|
|
118
|
-
orchestrator.runtimeConfig,
|
|
119
|
-
);
|
|
120
|
-
const source =
|
|
121
|
-
pkg === "host"
|
|
122
|
-
? orchestrator.appConfig.host
|
|
123
|
-
: pkg === "ui"
|
|
124
|
-
? orchestrator.appConfig.ui
|
|
125
|
-
: pkg === "api"
|
|
126
|
-
? orchestrator.appConfig.api
|
|
127
|
-
: undefined;
|
|
128
|
-
return {
|
|
129
|
-
name: pkg,
|
|
130
|
-
status: "pending" as const,
|
|
131
|
-
port: config?.port ?? 0,
|
|
132
|
-
source,
|
|
133
|
-
};
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const registry = yield* makeProcessRegistry(configDir);
|
|
137
|
-
yield* registry.killAll().pipe(Effect.ignore);
|
|
85
|
+
const initialProcesses: ProcessState[] = getProcessStates(
|
|
86
|
+
orderedPackages,
|
|
87
|
+
services,
|
|
88
|
+
orchestrator.port,
|
|
89
|
+
);
|
|
138
90
|
|
|
139
91
|
const logger = yield* Effect.promise(() =>
|
|
140
92
|
createDevLogger(configDir, orchestrator.description),
|
|
@@ -195,14 +147,22 @@ export const runDevSession = (
|
|
|
195
147
|
|
|
196
148
|
const startProcess = (pkg: string) => {
|
|
197
149
|
const portOverride = pkg === "host" ? orchestrator.port : undefined;
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
150
|
+
return makeDevProcess(pkg, callbacks, portOverride).pipe(
|
|
151
|
+
Effect.tapError((err) =>
|
|
152
|
+
Effect.sync(() => {
|
|
153
|
+
callbacks.onLog(pkg, `Failed to start: ${err}`, true);
|
|
154
|
+
callbacks.onStatus(pkg, "error");
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
Effect.catchAll(() =>
|
|
158
|
+
Effect.succeed({
|
|
159
|
+
name: pkg,
|
|
160
|
+
pid: undefined,
|
|
161
|
+
kill: Effect.void,
|
|
162
|
+
waitForReady: Effect.void,
|
|
163
|
+
waitForExit: Effect.never,
|
|
164
|
+
} satisfies ProcessHandle),
|
|
165
|
+
),
|
|
206
166
|
);
|
|
207
167
|
};
|
|
208
168
|
|
|
@@ -243,8 +203,16 @@ export const runDevSession = (
|
|
|
243
203
|
{ concurrency: "unbounded" },
|
|
244
204
|
);
|
|
245
205
|
|
|
206
|
+
const allHandles = [...nonHostHandles, ...hostHandles];
|
|
207
|
+
|
|
246
208
|
yield* Effect.addFinalizer(() =>
|
|
247
209
|
Effect.gen(function* () {
|
|
210
|
+
yield* Effect.forEach(allHandles, (h) => h.kill.pipe(Effect.ignore), {
|
|
211
|
+
concurrency: "unbounded",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
yield* Effect.sleep("200 millis");
|
|
215
|
+
|
|
248
216
|
view?.unmount();
|
|
249
217
|
|
|
250
218
|
if (shouldExportLogs) {
|
|
@@ -264,15 +232,17 @@ export const runDevSession = (
|
|
|
264
232
|
console.log("═".repeat(70));
|
|
265
233
|
console.log("");
|
|
266
234
|
}
|
|
267
|
-
|
|
268
|
-
yield* registry.killAll(true).pipe(Effect.ignore);
|
|
269
235
|
}),
|
|
270
236
|
);
|
|
271
237
|
|
|
272
238
|
yield* Deferred.await(shutdown);
|
|
273
239
|
});
|
|
274
240
|
|
|
275
|
-
|
|
241
|
+
const runApp = (
|
|
242
|
+
orchestrator: AppOrchestrator,
|
|
243
|
+
services: Map<string, ServiceDescriptor>,
|
|
244
|
+
runtimeConfig: RuntimeConfig,
|
|
245
|
+
) => {
|
|
276
246
|
let requestShutdown: (() => void) | null = null;
|
|
277
247
|
let signalCount = 0;
|
|
278
248
|
let forceExitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -290,13 +260,16 @@ export const startApp = (orchestrator: AppOrchestrator) => {
|
|
|
290
260
|
Effect.catchAll((e) =>
|
|
291
261
|
Effect.sync(() => {
|
|
292
262
|
if (e instanceof Error) {
|
|
293
|
-
console.error("App
|
|
263
|
+
console.error("App error:", e.message);
|
|
294
264
|
if (e.stack) console.error(e.stack);
|
|
295
265
|
} else {
|
|
296
|
-
console.error("App
|
|
266
|
+
console.error("App error:", e);
|
|
297
267
|
}
|
|
298
268
|
}),
|
|
299
269
|
),
|
|
270
|
+
Effect.provide(ServiceDescriptorMapLive(services)),
|
|
271
|
+
Effect.provide(DevRuntimeConfigLive(runtimeConfig)),
|
|
272
|
+
Effect.provide(NodeContext.layer),
|
|
300
273
|
);
|
|
301
274
|
|
|
302
275
|
const handleSignal = () => {
|
|
@@ -306,7 +279,7 @@ export const startApp = (orchestrator: AppOrchestrator) => {
|
|
|
306
279
|
return;
|
|
307
280
|
}
|
|
308
281
|
console.log("\n[Dev] Shutting down...");
|
|
309
|
-
forceExitTimer = setTimeout(forceExit,
|
|
282
|
+
forceExitTimer = setTimeout(forceExit, 5000);
|
|
310
283
|
requestShutdown?.();
|
|
311
284
|
};
|
|
312
285
|
|
|
@@ -318,3 +291,7 @@ export const startApp = (orchestrator: AppOrchestrator) => {
|
|
|
318
291
|
process.exit(Exit.isSuccess(exit) ? 0 : 0);
|
|
319
292
|
});
|
|
320
293
|
};
|
|
294
|
+
|
|
295
|
+
export const devApp = runApp;
|
|
296
|
+
|
|
297
|
+
export const startApp = runApp;
|
package/src/host.ts
CHANGED
|
@@ -32,10 +32,11 @@ function buildClientRuntimeConfig(runtimeConfig: RuntimeConfig): ClientRuntimeCo
|
|
|
32
32
|
env: runtimeConfig.env,
|
|
33
33
|
account: runtimeConfig.account,
|
|
34
34
|
networkId: runtimeConfig.networkId,
|
|
35
|
-
hostUrl: runtimeConfig.
|
|
35
|
+
hostUrl: runtimeConfig.host.url,
|
|
36
36
|
assetsUrl: runtimeConfig.ui.url,
|
|
37
37
|
apiBase: "/api",
|
|
38
38
|
rpcBase: "/api/rpc",
|
|
39
|
+
authAvailable: !!runtimeConfig.auth,
|
|
39
40
|
ui: runtimeConfig.ui
|
|
40
41
|
? {
|
|
41
42
|
name: runtimeConfig.ui.name,
|
|
@@ -240,7 +241,7 @@ async function runHostServer(opts: {
|
|
|
240
241
|
const app = new Hono();
|
|
241
242
|
|
|
242
243
|
const corsOrigins = process.env.CORS_ORIGIN?.split(",").map((o) => o.trim()) ?? [
|
|
243
|
-
runtimeConfig.
|
|
244
|
+
runtimeConfig.host.url,
|
|
244
245
|
...(runtimeConfig.ui?.url ? [runtimeConfig.ui.url] : []),
|
|
245
246
|
];
|
|
246
247
|
|
|
@@ -373,7 +374,7 @@ async function runHostServer(opts: {
|
|
|
373
374
|
{
|
|
374
375
|
status: allRequiredOk ? "ready" : "not_ready",
|
|
375
376
|
host: {
|
|
376
|
-
url: runtimeConfig.
|
|
377
|
+
url: runtimeConfig.host.url,
|
|
377
378
|
env: runtimeConfig.env,
|
|
378
379
|
},
|
|
379
380
|
checks,
|
package/src/integrity.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { fetchBosConfigFromFastKv } from "./fastkv";
|
|
2
3
|
|
|
3
4
|
export function computeSriHash(content: string | Buffer): string {
|
|
4
5
|
return `sha384-${createHash("sha384").update(content).digest("base64")}`;
|
|
@@ -6,11 +7,7 @@ export function computeSriHash(content: string | Buffer): string {
|
|
|
6
7
|
|
|
7
8
|
export async function computeSriHashForUrl(url: string): Promise<string | null> {
|
|
8
9
|
try {
|
|
9
|
-
const entryUrl = url
|
|
10
|
-
? url
|
|
11
|
-
: url.endsWith("/mf-manifest.json")
|
|
12
|
-
? `${url.replace(/\/mf-manifest\.json$/, "")}/remoteEntry.js`
|
|
13
|
-
: `${url.replace(/\/$/, "")}/remoteEntry.js`;
|
|
10
|
+
const entryUrl = resolveEntryUrl(url);
|
|
14
11
|
|
|
15
12
|
const response = await fetch(entryUrl);
|
|
16
13
|
if (!response.ok) {
|
|
@@ -28,12 +25,15 @@ export async function computeSriHashForUrl(url: string): Promise<string | null>
|
|
|
28
25
|
}
|
|
29
26
|
}
|
|
30
27
|
|
|
28
|
+
export function resolveEntryUrl(url: string): string {
|
|
29
|
+
if (url.endsWith("/remoteEntry.js")) return url;
|
|
30
|
+
if (url.endsWith("/mf-manifest.json"))
|
|
31
|
+
return `${url.replace(/\/mf-manifest\.json$/, "")}/remoteEntry.js`;
|
|
32
|
+
return `${url.replace(/\/$/, "")}/remoteEntry.js`;
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
export async function verifySriForUrl(url: string, expectedIntegrity: string): Promise<void> {
|
|
32
|
-
const entryUrl = url
|
|
33
|
-
? url
|
|
34
|
-
: url.endsWith("/mf-manifest.json")
|
|
35
|
-
? `${url.replace(/\/mf-manifest\.json$/, "")}/remoteEntry.js`
|
|
36
|
-
: `${url.replace(/\/$/, "")}/remoteEntry.js`;
|
|
36
|
+
const entryUrl = resolveEntryUrl(url);
|
|
37
37
|
|
|
38
38
|
const response = await fetch(entryUrl);
|
|
39
39
|
if (!response.ok) {
|
|
@@ -52,3 +52,89 @@ export async function verifySriForUrl(url: string, expectedIntegrity: string): P
|
|
|
52
52
|
|
|
53
53
|
console.log(`[SRI] Integrity verified for ${entryUrl}`);
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export class IntegrityRegistry {
|
|
57
|
+
private hashes = new Map<string, string>();
|
|
58
|
+
|
|
59
|
+
register(url: string, integrity: string): void {
|
|
60
|
+
this.hashes.set(url, integrity);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
registerEntry(baseUrl: string, integrity: string): void {
|
|
64
|
+
this.hashes.set(resolveEntryUrl(baseUrl), integrity);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get(url: string): string | undefined {
|
|
68
|
+
return this.hashes.get(url);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
has(url: string): boolean {
|
|
72
|
+
return this.hashes.has(url);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
entries(): IterableIterator<[string, string]> {
|
|
76
|
+
return this.hashes.entries();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractIntegrityHashes(config: Record<string, unknown>): Map<string, string> {
|
|
81
|
+
const hashes = new Map<string, string>();
|
|
82
|
+
const app = config.app as Record<string, Record<string, unknown>> | undefined;
|
|
83
|
+
const plugins = config.plugins as Record<string, Record<string, unknown>> | undefined;
|
|
84
|
+
|
|
85
|
+
if (app) {
|
|
86
|
+
for (const [, entry] of Object.entries(app)) {
|
|
87
|
+
if (entry?.integrity && entry?.production) {
|
|
88
|
+
hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (plugins) {
|
|
94
|
+
for (const [, entry] of Object.entries(plugins)) {
|
|
95
|
+
if (entry?.integrity && entry?.production) {
|
|
96
|
+
hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return hashes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function verifyConfigAgainstChain(
|
|
105
|
+
localConfig: Record<string, unknown>,
|
|
106
|
+
bosUrl: string,
|
|
107
|
+
): Promise<{ verified: boolean; mismatches: string[] }> {
|
|
108
|
+
const mismatches: string[] = [];
|
|
109
|
+
|
|
110
|
+
let chainConfig: Record<string, unknown>;
|
|
111
|
+
try {
|
|
112
|
+
chainConfig = await fetchBosConfigFromFastKv<Record<string, unknown>>(bosUrl);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.warn(
|
|
115
|
+
`[Attestation] Failed to fetch on-chain config: ${error instanceof Error ? error.message : String(error)}`,
|
|
116
|
+
);
|
|
117
|
+
return { verified: false, mismatches: ["chain-fetch-failed"] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const localHashes = extractIntegrityHashes(localConfig);
|
|
121
|
+
const chainHashes = extractIntegrityHashes(chainConfig);
|
|
122
|
+
|
|
123
|
+
for (const [url, chainHash] of chainHashes) {
|
|
124
|
+
const localHash = localHashes.get(url);
|
|
125
|
+
if (localHash && localHash !== chainHash) {
|
|
126
|
+
mismatches.push(url);
|
|
127
|
+
console.error(
|
|
128
|
+
`[Attestation] Integrity mismatch for ${url}\n Local: ${localHash}\n Chain: ${chainHash}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (mismatches.length === 0 && localHashes.size > 0) {
|
|
134
|
+
console.log(
|
|
135
|
+
`[Attestation] Local config verified against on-chain anchor (${localHashes.size} entries checked)`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { verified: mismatches.length === 0, mismatches };
|
|
140
|
+
}
|
package/src/mf.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInstance, getInstance } from "@module-federation/enhanced/runtime";
|
|
2
2
|
import { setGlobalFederationInstance } from "@module-federation/runtime-core";
|
|
3
|
+
import { computeSriHash, type IntegrityRegistry } from "./integrity";
|
|
3
4
|
|
|
4
5
|
type FederationInstance = ReturnType<typeof createInstance>;
|
|
5
6
|
|
|
@@ -27,6 +28,47 @@ export function patchManifestFetchForSsrPublicPath(mf: FederationInstance): void
|
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export function installIntegrityFetchHook(
|
|
32
|
+
mf: FederationInstance,
|
|
33
|
+
registry: IntegrityRegistry,
|
|
34
|
+
): void {
|
|
35
|
+
if (!mf || !(mf as any).loaderHook?.lifecycle?.fetch?.on) {
|
|
36
|
+
console.warn("[SRI] MF lifecycle fetch hook not available, skipping integrity-in-pipeline");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if ((mf as any).__everythingDevIntegrityHook === true) return;
|
|
40
|
+
(mf as any).__everythingDevIntegrityHook = true;
|
|
41
|
+
|
|
42
|
+
(mf as any).loaderHook.lifecycle.fetch.on((url: unknown, init: unknown) => {
|
|
43
|
+
if (typeof url !== "string") return;
|
|
44
|
+
|
|
45
|
+
const expectedHash = registry.get(url);
|
|
46
|
+
if (!expectedHash) return;
|
|
47
|
+
|
|
48
|
+
return fetch(url, init as any).then(async (res) => {
|
|
49
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
50
|
+
const computed = computeSriHash(buffer);
|
|
51
|
+
|
|
52
|
+
if (computed !== expectedHash) {
|
|
53
|
+
console.error(
|
|
54
|
+
`[SRI] Integrity check failed in MF fetch pipeline for ${url}\n Expected: ${expectedHash}\n Computed: ${computed}`,
|
|
55
|
+
);
|
|
56
|
+
return new Response(`Integrity check failed for ${url}`, {
|
|
57
|
+
status: 500,
|
|
58
|
+
statusText: "Integrity Check Failed",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`[SRI] Integrity verified in pipeline for ${url}`);
|
|
63
|
+
return new Response(buffer, {
|
|
64
|
+
status: res.status,
|
|
65
|
+
statusText: res.statusText,
|
|
66
|
+
headers: res.headers,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
30
72
|
export function getFederationInstance(): FederationInstance {
|
|
31
73
|
if (mfInstance) return mfInstance;
|
|
32
74
|
|