everything-dev 1.7.2 → 1.8.1

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.
Files changed (134) hide show
  1. package/dist/api.cjs +1 -1
  2. package/dist/api.mjs +1 -1
  3. package/dist/app.cjs +82 -51
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.mjs +82 -51
  6. package/dist/app.mjs.map +1 -1
  7. package/dist/cli/upgrade.cjs.map +1 -1
  8. package/dist/cli/upgrade.mjs.map +1 -1
  9. package/dist/components/dev-view.cjs +6 -3
  10. package/dist/components/dev-view.cjs.map +1 -1
  11. package/dist/components/dev-view.mjs +6 -3
  12. package/dist/components/dev-view.mjs.map +1 -1
  13. package/dist/components/streaming-view.cjs +5 -2
  14. package/dist/components/streaming-view.cjs.map +1 -1
  15. package/dist/components/streaming-view.mjs +5 -2
  16. package/dist/components/streaming-view.mjs.map +1 -1
  17. package/dist/config.cjs +28 -5
  18. package/dist/config.cjs.map +1 -1
  19. package/dist/config.d.cts.map +1 -1
  20. package/dist/config.d.mts.map +1 -1
  21. package/dist/config.mjs +28 -5
  22. package/dist/config.mjs.map +1 -1
  23. package/dist/contract.cjs +1 -0
  24. package/dist/contract.cjs.map +1 -1
  25. package/dist/contract.d.cts +10 -2
  26. package/dist/contract.d.cts.map +1 -1
  27. package/dist/contract.d.mts +10 -2
  28. package/dist/contract.d.mts.map +1 -1
  29. package/dist/contract.mjs +1 -0
  30. package/dist/contract.mjs.map +1 -1
  31. package/dist/dev-logs.cjs +6 -2
  32. package/dist/dev-logs.cjs.map +1 -1
  33. package/dist/dev-logs.mjs +7 -2
  34. package/dist/dev-logs.mjs.map +1 -1
  35. package/dist/dev-session.cjs +27 -23
  36. package/dist/dev-session.cjs.map +1 -1
  37. package/dist/dev-session.mjs +27 -24
  38. package/dist/dev-session.mjs.map +1 -1
  39. package/dist/federation.server.cjs +1 -1
  40. package/dist/federation.server.mjs +1 -1
  41. package/dist/host.cjs +4 -3
  42. package/dist/host.cjs.map +1 -1
  43. package/dist/host.d.cts.map +1 -1
  44. package/dist/host.d.mts.map +1 -1
  45. package/dist/host.mjs +4 -3
  46. package/dist/host.mjs.map +1 -1
  47. package/dist/integrity.cjs +68 -2
  48. package/dist/integrity.cjs.map +1 -1
  49. package/dist/integrity.d.cts +14 -1
  50. package/dist/integrity.d.cts.map +1 -1
  51. package/dist/integrity.d.mts +14 -1
  52. package/dist/integrity.d.mts.map +1 -1
  53. package/dist/integrity.mjs +66 -3
  54. package/dist/integrity.mjs.map +1 -1
  55. package/dist/mf.cjs +32 -0
  56. package/dist/mf.cjs.map +1 -1
  57. package/dist/mf.d.cts +3 -1
  58. package/dist/mf.d.cts.map +1 -1
  59. package/dist/mf.d.mts +3 -1
  60. package/dist/mf.d.mts.map +1 -1
  61. package/dist/mf.mjs +32 -1
  62. package/dist/mf.mjs.map +1 -1
  63. package/dist/orchestrator.cjs +167 -317
  64. package/dist/orchestrator.cjs.map +1 -1
  65. package/dist/orchestrator.d.cts +24 -21
  66. package/dist/orchestrator.d.cts.map +1 -1
  67. package/dist/orchestrator.d.mts +24 -21
  68. package/dist/orchestrator.d.mts.map +1 -1
  69. package/dist/orchestrator.mjs +168 -316
  70. package/dist/orchestrator.mjs.map +1 -1
  71. package/dist/plugin.cjs +38 -107
  72. package/dist/plugin.cjs.map +1 -1
  73. package/dist/plugin.d.cts +16 -2
  74. package/dist/plugin.d.cts.map +1 -1
  75. package/dist/plugin.d.mts +16 -2
  76. package/dist/plugin.d.mts.map +1 -1
  77. package/dist/plugin.mjs +39 -108
  78. package/dist/plugin.mjs.map +1 -1
  79. package/dist/service-descriptor.cjs +188 -0
  80. package/dist/service-descriptor.cjs.map +1 -0
  81. package/dist/service-descriptor.d.cts +107 -0
  82. package/dist/service-descriptor.d.cts.map +1 -0
  83. package/dist/service-descriptor.d.mts +107 -0
  84. package/dist/service-descriptor.d.mts.map +1 -0
  85. package/dist/service-descriptor.mjs +182 -0
  86. package/dist/service-descriptor.mjs.map +1 -0
  87. package/dist/types.cjs +8 -1
  88. package/dist/types.cjs.map +1 -1
  89. package/dist/types.d.cts +16 -1
  90. package/dist/types.d.cts.map +1 -1
  91. package/dist/types.d.mts +16 -1
  92. package/dist/types.d.mts.map +1 -1
  93. package/dist/types.mjs +8 -1
  94. package/dist/types.mjs.map +1 -1
  95. package/dist/ui/index.cjs +1 -0
  96. package/dist/ui/index.d.cts +2 -2
  97. package/dist/ui/index.d.mts +2 -2
  98. package/dist/ui/index.mjs +2 -2
  99. package/dist/ui/runtime.cjs +4 -0
  100. package/dist/ui/runtime.cjs.map +1 -1
  101. package/dist/ui/runtime.d.cts +2 -1
  102. package/dist/ui/runtime.d.cts.map +1 -1
  103. package/dist/ui/runtime.d.mts +2 -1
  104. package/dist/ui/runtime.d.mts.map +1 -1
  105. package/dist/ui/runtime.mjs +4 -1
  106. package/dist/ui/runtime.mjs.map +1 -1
  107. package/package.json +12 -4
  108. package/skills/dev-workflow/SKILL.md +105 -0
  109. package/skills/publish-sync/SKILL.md +130 -0
  110. package/src/app.ts +98 -204
  111. package/src/cli/upgrade.ts +20 -4
  112. package/src/components/dev-view.tsx +8 -3
  113. package/src/components/streaming-view.ts +7 -2
  114. package/src/config.ts +40 -8
  115. package/src/contract.ts +1 -0
  116. package/src/dev-logs.ts +8 -1
  117. package/src/dev-session.ts +56 -79
  118. package/src/host.ts +4 -3
  119. package/src/integrity.ts +96 -10
  120. package/src/mf.ts +42 -0
  121. package/src/orchestrator.ts +232 -411
  122. package/src/plugin.ts +48 -136
  123. package/src/service-descriptor.ts +258 -0
  124. package/src/types.ts +8 -1
  125. package/src/ui/runtime.ts +5 -0
  126. package/dist/process-registry.cjs +0 -120
  127. package/dist/process-registry.cjs.map +0 -1
  128. package/dist/process-registry.d.cts +0 -25
  129. package/dist/process-registry.d.cts.map +0 -1
  130. package/dist/process-registry.d.mts +0 -25
  131. package/dist/process-registry.d.mts.map +0 -1
  132. package/dist/process-registry.mjs +0 -119
  133. package/dist/process-registry.mjs.map +0 -1
  134. 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
- ? resolveRuntimeTarget(uiConfig.development, baseDir)
148
+ ? resolveDevelopmentTarget(uiConfig.development, uiConfig.production, baseDir)
137
149
  : resolveRuntimeTarget(uiConfig.production, baseDir, "remote");
138
150
  const apiRuntime =
139
151
  env === "development"
140
- ? resolveRuntimeTarget(apiConfig.development, baseDir)
152
+ ? resolveDevelopmentTarget(apiConfig.development, apiConfig.production, baseDir)
141
153
  : resolveRuntimeTarget(apiConfig.production, baseDir, "remote");
142
154
  const authRuntime = authConfig
143
155
  ? env === "development"
144
- ? resolveRuntimeTarget(authConfig.development, baseDir)
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
- hostUrl:
155
- env === "development"
156
- ? resolveDevelopmentHostUrl(config.app.host.development)
157
- : config.app.host.production,
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: resolve(baseDir, localTarget),
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
- return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`;
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> {
@@ -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
- getProcessConfig,
13
+ getProcessStates,
13
14
  makeDevProcess,
14
15
  type ProcessCallbacks,
15
16
  type ProcessHandle,
16
17
  } from "./orchestrator";
17
- import { makeProcessRegistry, type ProcessRegistry } from "./process-registry";
18
- import type { BosConfig, RuntimeConfig, SourceMode } from "./types";
19
-
20
- export interface AppConfig {
21
- host: SourceMode;
22
- ui: SourceMode;
23
- api: SourceMode;
24
- proxy?: boolean;
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[] = orderedPackages.map((pkg) => {
112
- const portOverride = pkg === "host" ? orchestrator.port : undefined;
113
- const config = getProcessConfig(
114
- pkg,
115
- undefined,
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 scopedProcess(
199
- pkg,
200
- orchestrator.env,
201
- callbacks,
202
- portOverride,
203
- orchestrator.bosConfig,
204
- orchestrator.runtimeConfig,
205
- registry,
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
- export const startApp = (orchestrator: AppOrchestrator) => {
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 server error:", e.message);
263
+ console.error("App error:", e.message);
294
264
  if (e.stack) console.error(e.stack);
295
265
  } else {
296
- console.error("App server error:", e);
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, 8000);
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.hostUrl,
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.hostUrl,
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.hostUrl,
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.endsWith("/remoteEntry.js")
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.endsWith("/remoteEntry.js")
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