everything-dev 1.27.0 → 1.28.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/cli/infra.cjs +1 -1
- package/dist/cli/infra.mjs +1 -1
- package/dist/cli/init.cjs +7 -9
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +1 -1
- package/dist/cli/init.d.cts.map +1 -1
- package/dist/cli/init.d.mts +1 -1
- package/dist/cli/init.d.mts.map +1 -1
- package/dist/cli/init.mjs +7 -9
- package/dist/cli/init.mjs.map +1 -1
- package/dist/cli/prompts.cjs +28 -24
- package/dist/cli/prompts.cjs.map +1 -1
- package/dist/cli/prompts.mjs +27 -24
- package/dist/cli/prompts.mjs.map +1 -1
- package/dist/cli/sync.cjs +4 -1
- package/dist/cli/sync.cjs.map +1 -1
- package/dist/cli/sync.mjs +4 -1
- package/dist/cli/sync.mjs.map +1 -1
- package/dist/cli.cjs +187 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +186 -11
- package/dist/cli.mjs.map +1 -1
- package/dist/contract.cjs +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +38 -34
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +38 -34
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.mjs +1 -0
- package/dist/contract.mjs.map +1 -1
- package/dist/dev-session.cjs +0 -1
- package/dist/dev-session.mjs +1 -1
- package/dist/index.cjs +0 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +0 -1
- package/dist/near-cli.cjs +1 -1
- package/dist/near-cli.mjs +1 -1
- package/dist/orchestrator.cjs +1 -1
- package/dist/orchestrator.mjs +1 -1
- package/dist/plugin.cjs +163 -139
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +67 -34
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +66 -34
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +153 -130
- package/dist/plugin.mjs.map +1 -1
- package/dist/service-descriptor.d.cts +34 -0
- package/dist/service-descriptor.d.cts.map +1 -0
- package/dist/service-descriptor.d.mts +36 -0
- package/dist/service-descriptor.d.mts.map +1 -0
- package/dist/types.d.cts +2 -2
- package/dist/types.d.mts +2 -2
- package/package.json +2 -2
- package/src/api-contract.ts +0 -623
- package/src/app.ts +0 -193
- package/src/cli/catalog.ts +0 -49
- package/src/cli/framework-version.ts +0 -61
- package/src/cli/help.ts +0 -13
- package/src/cli/infra.ts +0 -190
- package/src/cli/init.ts +0 -1145
- package/src/cli/parse.ts +0 -147
- package/src/cli/prompts.ts +0 -135
- package/src/cli/snapshot.ts +0 -46
- package/src/cli/status.ts +0 -99
- package/src/cli/sync.ts +0 -429
- package/src/cli/timing.ts +0 -63
- package/src/cli/upgrade.ts +0 -869
- package/src/cli.ts +0 -516
- package/src/components/dev-view.tsx +0 -352
- package/src/components/streaming-view.ts +0 -177
- package/src/config.ts +0 -893
- package/src/contract.meta.ts +0 -140
- package/src/contract.ts +0 -326
- package/src/dev-logs.ts +0 -92
- package/src/dev-session.ts +0 -283
- package/src/fastkv.ts +0 -181
- package/src/index.ts +0 -8
- package/src/integrity.ts +0 -138
- package/src/internal/manifest-normalizer.ts +0 -290
- package/src/merge.ts +0 -187
- package/src/mf.ts +0 -147
- package/src/near-cli.ts +0 -259
- package/src/network.ts +0 -3
- package/src/orchestrator.ts +0 -493
- package/src/plugin.ts +0 -1799
- package/src/sdk.ts +0 -14
- package/src/service-descriptor.ts +0 -281
- package/src/shared.ts +0 -249
- package/src/sidebar.ts +0 -140
- package/src/types.ts +0 -330
- package/src/ui/head.ts +0 -83
- package/src/ui/index.ts +0 -5
- package/src/ui/metadata.ts +0 -95
- package/src/ui/router.ts +0 -88
- package/src/ui/runtime.ts +0 -42
- package/src/ui/types.ts +0 -65
- package/src/utils/banner.ts +0 -21
- package/src/utils/linkify.ts +0 -11
- package/src/utils/path-match.ts +0 -16
- package/src/utils/run.ts +0 -31
- package/src/utils/save-config.ts +0 -20
- package/src/utils/theme.ts +0 -39
package/src/dev-session.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import * as NodeContext from "@effect/platform-node/NodeContext";
|
|
2
|
-
import { Deferred, Effect, Exit } from "effect";
|
|
3
|
-
import {
|
|
4
|
-
type DevViewHandle,
|
|
5
|
-
type LogEntry,
|
|
6
|
-
type ProcessState,
|
|
7
|
-
renderDevView,
|
|
8
|
-
} from "./components/dev-view";
|
|
9
|
-
import { renderStreamingView } from "./components/streaming-view";
|
|
10
|
-
import { getProjectRoot } from "./config";
|
|
11
|
-
import { createDevLogger } from "./dev-logs";
|
|
12
|
-
import {
|
|
13
|
-
getProcessStates,
|
|
14
|
-
makeDevProcess,
|
|
15
|
-
type ProcessCallbacks,
|
|
16
|
-
type ProcessHandle,
|
|
17
|
-
} from "./orchestrator";
|
|
18
|
-
import {
|
|
19
|
-
type AppOrchestrator,
|
|
20
|
-
DevRuntimeConfigLive,
|
|
21
|
-
type ServiceDescriptor,
|
|
22
|
-
ServiceDescriptorMap,
|
|
23
|
-
ServiceDescriptorMapLive,
|
|
24
|
-
} from "./service-descriptor";
|
|
25
|
-
import type { RuntimeConfig } from "./types";
|
|
26
|
-
|
|
27
|
-
const LOG_NOISE_PATTERNS = [
|
|
28
|
-
/\[ Federation Runtime \] Version .* from (host|ui) of shared singleton module/,
|
|
29
|
-
/Executing an Effect versioned \d+\.\d+\.\d+ with a Runtime of version/,
|
|
30
|
-
/you may want to dedupe the effect dependencies/,
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const SSR_LOG_ALLOWLIST = [
|
|
34
|
-
/\bready\s+built in\b/i,
|
|
35
|
-
/\bcompiled\b.*successfully/i,
|
|
36
|
-
/\berror\b/i,
|
|
37
|
-
/\bfailed\b/i,
|
|
38
|
-
/\bexception\b/i,
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const shouldDisplayLog = (source: string, line: string, isError?: boolean): boolean => {
|
|
42
|
-
if (process.env.DEBUG === "true" || process.env.DEBUG === "1") return true;
|
|
43
|
-
if (source === "ui-ssr") {
|
|
44
|
-
if (isError) return true;
|
|
45
|
-
return SSR_LOG_ALLOWLIST.some((pattern) => pattern.test(line));
|
|
46
|
-
}
|
|
47
|
-
return !LOG_NOISE_PATTERNS.some((pattern) => pattern.test(line));
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const isInteractiveSupported = (): boolean => {
|
|
51
|
-
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const STARTUP_ORDER = ["ui-ssr", "ui", "auth", "api", "plugin", "host-build", "host"];
|
|
55
|
-
|
|
56
|
-
const sortByOrder = (packages: string[]): string[] => {
|
|
57
|
-
return [...packages].sort((a, b) => {
|
|
58
|
-
const aIdx = a.startsWith("plugin:")
|
|
59
|
-
? STARTUP_ORDER.indexOf("plugin")
|
|
60
|
-
: STARTUP_ORDER.indexOf(a);
|
|
61
|
-
const bIdx = b.startsWith("plugin:")
|
|
62
|
-
? STARTUP_ORDER.indexOf("plugin")
|
|
63
|
-
: STARTUP_ORDER.indexOf(b);
|
|
64
|
-
if (aIdx === -1 && bIdx === -1) return 0;
|
|
65
|
-
if (aIdx === -1) return 1;
|
|
66
|
-
if (bIdx === -1) return -1;
|
|
67
|
-
return aIdx - bIdx;
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
function formatLogLine(entry: LogEntry): string {
|
|
72
|
-
const ts = new Date(entry.timestamp).toISOString();
|
|
73
|
-
const prefix = entry.isError ? "ERR" : "OUT";
|
|
74
|
-
return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const runDevSession = (
|
|
78
|
-
orchestrator: AppOrchestrator,
|
|
79
|
-
onShutdownReady?: (requestShutdown: () => void) => void,
|
|
80
|
-
) =>
|
|
81
|
-
Effect.gen(function* () {
|
|
82
|
-
const configDir = getProjectRoot();
|
|
83
|
-
const services = yield* ServiceDescriptorMap;
|
|
84
|
-
const orderedPackages = sortByOrder(orchestrator.packages);
|
|
85
|
-
const initialProcesses: ProcessState[] = getProcessStates(
|
|
86
|
-
orderedPackages,
|
|
87
|
-
services,
|
|
88
|
-
orchestrator.port,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const logger = yield* Effect.promise(() =>
|
|
92
|
-
createDevLogger(configDir, orchestrator.description),
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const shutdown = yield* Deferred.make<void>();
|
|
96
|
-
|
|
97
|
-
onShutdownReady?.(() => {
|
|
98
|
-
void Effect.runPromise(Deferred.succeed(shutdown, undefined));
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const allLogs: LogEntry[] = [];
|
|
102
|
-
let view: DevViewHandle | null = null;
|
|
103
|
-
let shouldExportLogs = false;
|
|
104
|
-
|
|
105
|
-
const requestShutdownAndExport = () => {
|
|
106
|
-
shouldExportLogs = true;
|
|
107
|
-
void Effect.runPromise(Deferred.succeed(shutdown, undefined));
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const useInteractive = orchestrator.interactive ?? isInteractiveSupported();
|
|
111
|
-
view = useInteractive
|
|
112
|
-
? renderDevView(
|
|
113
|
-
initialProcesses,
|
|
114
|
-
orchestrator.description,
|
|
115
|
-
orchestrator.env,
|
|
116
|
-
() => void Effect.runPromise(Deferred.succeed(shutdown, undefined)),
|
|
117
|
-
requestShutdownAndExport,
|
|
118
|
-
)
|
|
119
|
-
: renderStreamingView(
|
|
120
|
-
initialProcesses,
|
|
121
|
-
orchestrator.description,
|
|
122
|
-
orchestrator.env,
|
|
123
|
-
() => void Effect.runPromise(Deferred.succeed(shutdown, undefined)),
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const callbacks: ProcessCallbacks = {
|
|
127
|
-
onStatus: (name, status, message) => {
|
|
128
|
-
view?.updateProcess(name, status, message);
|
|
129
|
-
},
|
|
130
|
-
onLog: (name, line, isError) => {
|
|
131
|
-
const entry: LogEntry = {
|
|
132
|
-
id: `${Date.now()}-${allLogs.length + 1}`,
|
|
133
|
-
source: name,
|
|
134
|
-
line,
|
|
135
|
-
timestamp: Date.now(),
|
|
136
|
-
isError,
|
|
137
|
-
};
|
|
138
|
-
allLogs.push(entry);
|
|
139
|
-
if (shouldDisplayLog(name, line, isError)) {
|
|
140
|
-
view?.addLog(name, line, isError);
|
|
141
|
-
}
|
|
142
|
-
if (!orchestrator.noLogs) {
|
|
143
|
-
void logger.write(entry);
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const startProcess = (pkg: string) => {
|
|
149
|
-
const portOverride = pkg === "host" ? orchestrator.port : undefined;
|
|
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
|
-
),
|
|
166
|
-
);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const startGroup = (packages: string[]) =>
|
|
170
|
-
Effect.forEach(packages, startProcess, { concurrency: "unbounded" });
|
|
171
|
-
|
|
172
|
-
const awaitReady = (_pkg: string, handle: ProcessHandle) =>
|
|
173
|
-
handle.waitForReady.pipe(Effect.catchAll(() => Effect.void));
|
|
174
|
-
|
|
175
|
-
const nonHostPackages = orderedPackages.filter((pkg) => pkg !== "host");
|
|
176
|
-
const hostPackages = orderedPackages.filter((pkg) => pkg === "host");
|
|
177
|
-
|
|
178
|
-
const nonHostHandles = yield* startGroup(nonHostPackages);
|
|
179
|
-
|
|
180
|
-
yield* Effect.forEach(
|
|
181
|
-
nonHostHandles.map((handle, index) => ({
|
|
182
|
-
handle,
|
|
183
|
-
pkg: nonHostPackages[index] ?? handle.name,
|
|
184
|
-
})),
|
|
185
|
-
({ handle, pkg }) => awaitReady(pkg, handle),
|
|
186
|
-
{ concurrency: "unbounded" },
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
const hostHandles = yield* startGroup(hostPackages);
|
|
190
|
-
|
|
191
|
-
yield* Effect.forEach(
|
|
192
|
-
hostHandles.map((handle, index) => ({ handle, pkg: hostPackages[index] ?? handle.name })),
|
|
193
|
-
({ handle, pkg }) => awaitReady(pkg, handle),
|
|
194
|
-
{ concurrency: "unbounded" },
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const allHandles = [...nonHostHandles, ...hostHandles];
|
|
198
|
-
|
|
199
|
-
yield* Effect.addFinalizer(() =>
|
|
200
|
-
Effect.gen(function* () {
|
|
201
|
-
yield* Effect.forEach(allHandles, (h) => h.kill.pipe(Effect.ignore), {
|
|
202
|
-
concurrency: "unbounded",
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
yield* Effect.sleep("200 millis");
|
|
206
|
-
|
|
207
|
-
view?.unmount();
|
|
208
|
-
|
|
209
|
-
if (shouldExportLogs) {
|
|
210
|
-
console.log("\n");
|
|
211
|
-
console.log("═".repeat(70));
|
|
212
|
-
console.log(` SESSION LOGS: ${orchestrator.description}`);
|
|
213
|
-
console.log(` Started: ${new Date(allLogs[0]?.timestamp || Date.now()).toISOString()}`);
|
|
214
|
-
console.log(` Total entries: ${allLogs.length}`);
|
|
215
|
-
console.log("═".repeat(70));
|
|
216
|
-
console.log("");
|
|
217
|
-
for (const entry of allLogs) {
|
|
218
|
-
console.log(formatLogLine(entry));
|
|
219
|
-
}
|
|
220
|
-
console.log("");
|
|
221
|
-
console.log("═".repeat(70));
|
|
222
|
-
console.log(` Full logs saved to: ${logger.logFile}`);
|
|
223
|
-
console.log("═".repeat(70));
|
|
224
|
-
console.log("");
|
|
225
|
-
}
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
yield* Deferred.await(shutdown);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const runApp = (
|
|
233
|
-
orchestrator: AppOrchestrator,
|
|
234
|
-
services: Map<string, ServiceDescriptor>,
|
|
235
|
-
runtimeConfig: RuntimeConfig,
|
|
236
|
-
) => {
|
|
237
|
-
let requestShutdown: (() => void) | null = null;
|
|
238
|
-
let signalCount = 0;
|
|
239
|
-
let forceExitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
240
|
-
|
|
241
|
-
const forceExit = () => {
|
|
242
|
-
console.log("\n[Dev] Force exit");
|
|
243
|
-
process.exit(0);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const program = Effect.scoped(
|
|
247
|
-
runDevSession(orchestrator, (shutdown) => {
|
|
248
|
-
requestShutdown = shutdown;
|
|
249
|
-
}),
|
|
250
|
-
).pipe(
|
|
251
|
-
Effect.provide(ServiceDescriptorMapLive(services)),
|
|
252
|
-
Effect.provide(DevRuntimeConfigLive(runtimeConfig)),
|
|
253
|
-
Effect.provide(NodeContext.layer),
|
|
254
|
-
Effect.catchAllDefect((defect) =>
|
|
255
|
-
Effect.sync(() => {
|
|
256
|
-
console.error("[Dev] Unhandled defect in orchestrator:", defect);
|
|
257
|
-
}),
|
|
258
|
-
),
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
const handleSignal = () => {
|
|
262
|
-
signalCount++;
|
|
263
|
-
if (signalCount > 1) {
|
|
264
|
-
forceExit();
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
console.log("\n[Dev] Shutting down...");
|
|
268
|
-
forceExitTimer = setTimeout(forceExit, 5000);
|
|
269
|
-
requestShutdown?.();
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
process.on("SIGINT", handleSignal);
|
|
273
|
-
process.on("SIGTERM", handleSignal);
|
|
274
|
-
|
|
275
|
-
Effect.runPromiseExit(program).then((exit) => {
|
|
276
|
-
if (forceExitTimer) clearTimeout(forceExitTimer);
|
|
277
|
-
process.exit(Exit.isSuccess(exit) ? 0 : 0);
|
|
278
|
-
});
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
export const devApp = runApp;
|
|
282
|
-
|
|
283
|
-
export const startApp = runApp;
|
package/src/fastkv.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
export type NetworkId = "mainnet" | "testnet";
|
|
2
|
-
|
|
3
|
-
interface FastKvEntry {
|
|
4
|
-
value: unknown;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface FastKvListResponse {
|
|
8
|
-
entries?: Array<FastKvEntry | null>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const FASTKV_TIMEOUT_MS = 10_000;
|
|
12
|
-
|
|
13
|
-
function getNetworkIdForAccount(accountId: string): NetworkId {
|
|
14
|
-
return accountId.endsWith(".testnet") ? "testnet" : "mainnet";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function getFastKvBaseUrlForNetwork(network: NetworkId): string {
|
|
18
|
-
return network === "testnet"
|
|
19
|
-
? process.env.REGISTRY_FASTKV_TESTNET_URL || "https://kv.test.fastnear.com"
|
|
20
|
-
: process.env.REGISTRY_FASTKV_MAINNET_URL || "https://kv.main.fastnear.com";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getFastKvBaseUrlForAccount(accountId: string): string {
|
|
24
|
-
return getNetworkIdForAccount(accountId) === "testnet"
|
|
25
|
-
? getFastKvBaseUrlForNetwork("testnet")
|
|
26
|
-
: getFastKvBaseUrlForNetwork("mainnet");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function buildRegistryConfigUrl(accountId: string, gatewayId: string): string {
|
|
30
|
-
const baseUrl = getFastKvBaseUrlForAccount(accountId);
|
|
31
|
-
const namespace = getRegistryNamespaceForAccount(accountId);
|
|
32
|
-
const key = encodeURIComponent(getRegistryConfigKey(accountId, gatewayId));
|
|
33
|
-
return `${baseUrl}/v0/latest/${encodeURIComponent(namespace)}/${encodeURIComponent(accountId)}/${key}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function buildRegistryConfigUrlForNetwork(
|
|
37
|
-
network: NetworkId,
|
|
38
|
-
accountId: string,
|
|
39
|
-
gatewayId: string,
|
|
40
|
-
): string {
|
|
41
|
-
const baseUrl = getFastKvBaseUrlForNetwork(network);
|
|
42
|
-
const namespace = getRegistryNamespaceForNetwork(network);
|
|
43
|
-
const key = encodeURIComponent(getRegistryConfigKey(accountId, gatewayId));
|
|
44
|
-
return `${baseUrl}/v0/latest/${encodeURIComponent(namespace)}/${encodeURIComponent(accountId)}/${key}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function getRegistryNamespaceForAccount(accountId: string): string {
|
|
48
|
-
return accountId.endsWith(".testnet")
|
|
49
|
-
? process.env.REGISTRY_FASTKV_TESTNET_NAMESPACE || "dev.everything.near"
|
|
50
|
-
: process.env.REGISTRY_FASTKV_MAINNET_NAMESPACE || "dev.everything.near";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function getRegistryNamespaceForNetwork(network: NetworkId): string {
|
|
54
|
-
return network === "testnet"
|
|
55
|
-
? process.env.REGISTRY_FASTKV_TESTNET_NAMESPACE || "dev.everything.near"
|
|
56
|
-
: process.env.REGISTRY_FASTKV_MAINNET_NAMESPACE || "dev.everything.near";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getRegistryConfigKey(
|
|
60
|
-
accountId: string,
|
|
61
|
-
gatewayId: string,
|
|
62
|
-
pathSegments: string[] = [],
|
|
63
|
-
): string {
|
|
64
|
-
const suffix =
|
|
65
|
-
pathSegments.length > 0
|
|
66
|
-
? `/${pathSegments.map((segment) => encodeURIComponent(segment)).join("/")}`
|
|
67
|
-
: "";
|
|
68
|
-
return `apps/${accountId}/${gatewayId}${suffix}/bos.config.json`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseBosUrl(bosUrl: string): {
|
|
72
|
-
accountId: string;
|
|
73
|
-
gatewayId: string;
|
|
74
|
-
pathSegments: string[];
|
|
75
|
-
} {
|
|
76
|
-
const match = bosUrl.match(/^bos:\/\/([^/]+)\/(.+)$/);
|
|
77
|
-
if (!match?.[1] || !match[2]) {
|
|
78
|
-
throw new Error(`Invalid BOS URL: ${bosUrl}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const pathSegments = match[2]
|
|
82
|
-
.split("/")
|
|
83
|
-
.filter(Boolean)
|
|
84
|
-
.map((segment) => decodeURIComponent(segment));
|
|
85
|
-
if (pathSegments.length === 0) {
|
|
86
|
-
throw new Error(`Invalid BOS URL: ${bosUrl}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const [gatewayId, ...pathSegmentsTail] = pathSegments;
|
|
90
|
-
if (!gatewayId) {
|
|
91
|
-
throw new Error(`Invalid BOS URL: ${bosUrl}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
accountId: match[1],
|
|
96
|
-
gatewayId,
|
|
97
|
-
pathSegments: pathSegmentsTail,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function fetchBosConfigFromFastKv<T>(bosUrl: string): Promise<T> {
|
|
102
|
-
const { accountId, gatewayId, pathSegments } = parseBosUrl(bosUrl);
|
|
103
|
-
const payload = await fetchJson<FastKvListResponse>(
|
|
104
|
-
`${getFastKvBaseUrlForAccount(accountId)}/v0/latest/${encodeURIComponent(getRegistryNamespaceForAccount(accountId))}/${encodeURIComponent(accountId)}`,
|
|
105
|
-
{
|
|
106
|
-
method: "POST",
|
|
107
|
-
body: JSON.stringify({
|
|
108
|
-
key: getRegistryConfigKey(accountId, gatewayId, pathSegments),
|
|
109
|
-
limit: 1,
|
|
110
|
-
}),
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
const value = payload?.entries?.find(Boolean)?.value;
|
|
114
|
-
|
|
115
|
-
if (!value) {
|
|
116
|
-
throw new Error(`No config found for ${bosUrl}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (typeof value === "string") {
|
|
120
|
-
return JSON.parse(value) as T;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (typeof value !== "object") {
|
|
124
|
-
throw new Error(`Invalid config value for ${bosUrl}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return value as T;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface PluginManifest {
|
|
131
|
-
schemaVersion: number;
|
|
132
|
-
kind: string;
|
|
133
|
-
plugin: { name: string; version: string };
|
|
134
|
-
runtime: { remoteEntry: string };
|
|
135
|
-
contract: {
|
|
136
|
-
kind: string;
|
|
137
|
-
types: { path: string; exportName: string; typeName: string; sha256: string };
|
|
138
|
-
};
|
|
139
|
-
additionalExports?: Array<{ path: string; exports: string[]; sha256: string }>;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function fetchRemotePluginManifest(cdnUrl: string): Promise<PluginManifest | null> {
|
|
143
|
-
try {
|
|
144
|
-
const controller = new AbortController();
|
|
145
|
-
const timeout = setTimeout(() => controller.abort(), FASTKV_TIMEOUT_MS);
|
|
146
|
-
const baseUrl = cdnUrl.replace(/\/$/, "");
|
|
147
|
-
const response = await fetch(`${baseUrl}/plugin.manifest.json`, {
|
|
148
|
-
signal: controller.signal,
|
|
149
|
-
});
|
|
150
|
-
clearTimeout(timeout);
|
|
151
|
-
if (!response.ok) return null;
|
|
152
|
-
return (await response.json()) as PluginManifest;
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null> {
|
|
159
|
-
const controller = new AbortController();
|
|
160
|
-
const timeout = setTimeout(() => controller.abort(), FASTKV_TIMEOUT_MS);
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const response = await fetch(url, {
|
|
164
|
-
...init,
|
|
165
|
-
headers: {
|
|
166
|
-
accept: "application/json",
|
|
167
|
-
"content-type": "application/json",
|
|
168
|
-
...(init?.headers ?? {}),
|
|
169
|
-
},
|
|
170
|
-
signal: controller.signal,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (!response.ok) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return (await response.json()) as T;
|
|
178
|
-
} finally {
|
|
179
|
-
clearTimeout(timeout);
|
|
180
|
-
}
|
|
181
|
-
}
|
package/src/index.ts
DELETED
package/src/integrity.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { fetchBosConfigFromFastKv } from "./fastkv";
|
|
3
|
-
|
|
4
|
-
export function computeSriHash(content: string | Buffer): string {
|
|
5
|
-
return `sha384-${createHash("sha384").update(content).digest("base64")}`;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export async function computeSriHashForUrl(url: string): Promise<string | null> {
|
|
9
|
-
try {
|
|
10
|
-
const entryUrl = resolveEntryUrl(url);
|
|
11
|
-
|
|
12
|
-
const response = await fetch(entryUrl);
|
|
13
|
-
if (!response.ok) {
|
|
14
|
-
console.warn(`[SRI] Failed to fetch ${entryUrl}: ${response.status} ${response.statusText}`);
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
18
|
-
return computeSriHash(buffer);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
console.warn(
|
|
21
|
-
`[SRI] Error computing integrity for ${url}:`,
|
|
22
|
-
error instanceof Error ? error.message : error,
|
|
23
|
-
);
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
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
|
-
|
|
35
|
-
export async function verifySriForUrl(url: string, expectedIntegrity: string): Promise<void> {
|
|
36
|
-
const entryUrl = resolveEntryUrl(url);
|
|
37
|
-
|
|
38
|
-
const response = await fetch(entryUrl);
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
console.warn(`[SRI] Failed to fetch ${entryUrl} for verification: ${response.status}`);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
45
|
-
const computed = computeSriHash(buffer);
|
|
46
|
-
|
|
47
|
-
if (computed !== expectedIntegrity) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`[SRI] Integrity check failed for ${entryUrl}\n Expected: ${expectedIntegrity}\n Computed: ${computed}`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class IntegrityRegistry {
|
|
55
|
-
private hashes = new Map<string, string>();
|
|
56
|
-
|
|
57
|
-
register(url: string, integrity: string): void {
|
|
58
|
-
this.hashes.set(url, integrity);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
registerEntry(baseUrl: string, integrity: string): void {
|
|
62
|
-
this.hashes.set(resolveEntryUrl(baseUrl), integrity);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
get(url: string): string | undefined {
|
|
66
|
-
return this.hashes.get(url);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
has(url: string): boolean {
|
|
70
|
-
return this.hashes.has(url);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
entries(): IterableIterator<[string, string]> {
|
|
74
|
-
return this.hashes.entries();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function extractIntegrityHashes(config: Record<string, unknown>): Map<string, string> {
|
|
79
|
-
const hashes = new Map<string, string>();
|
|
80
|
-
const app = config.app as Record<string, Record<string, unknown>> | undefined;
|
|
81
|
-
const plugins = config.plugins as Record<string, Record<string, unknown>> | undefined;
|
|
82
|
-
|
|
83
|
-
if (app) {
|
|
84
|
-
for (const [, entry] of Object.entries(app)) {
|
|
85
|
-
if (entry?.integrity && entry?.production) {
|
|
86
|
-
hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (plugins) {
|
|
92
|
-
for (const [, entry] of Object.entries(plugins)) {
|
|
93
|
-
if (entry?.integrity && entry?.production) {
|
|
94
|
-
hashes.set(resolveEntryUrl(entry.production as string), entry.integrity as string);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return hashes;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export async function verifyConfigAgainstChain(
|
|
103
|
-
localConfig: Record<string, unknown>,
|
|
104
|
-
bosUrl: string,
|
|
105
|
-
): Promise<{ verified: boolean; mismatches: string[] }> {
|
|
106
|
-
const mismatches: string[] = [];
|
|
107
|
-
|
|
108
|
-
let chainConfig: Record<string, unknown>;
|
|
109
|
-
try {
|
|
110
|
-
chainConfig = await fetchBosConfigFromFastKv<Record<string, unknown>>(bosUrl);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.warn(
|
|
113
|
-
`[Attestation] Failed to fetch on-chain config: ${error instanceof Error ? error.message : String(error)}`,
|
|
114
|
-
);
|
|
115
|
-
return { verified: false, mismatches: ["chain-fetch-failed"] };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const localHashes = extractIntegrityHashes(localConfig);
|
|
119
|
-
const chainHashes = extractIntegrityHashes(chainConfig);
|
|
120
|
-
|
|
121
|
-
for (const [url, chainHash] of chainHashes) {
|
|
122
|
-
const localHash = localHashes.get(url);
|
|
123
|
-
if (localHash && localHash !== chainHash) {
|
|
124
|
-
mismatches.push(url);
|
|
125
|
-
console.error(
|
|
126
|
-
`[Attestation] Integrity mismatch for ${url}\n Local: ${localHash}\n Chain: ${chainHash}`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (mismatches.length === 0 && localHashes.size > 0) {
|
|
132
|
-
console.log(
|
|
133
|
-
`[Attestation] Local config verified against on-chain anchor (${localHashes.size} entries checked)`,
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { verified: mismatches.length === 0, mismatches };
|
|
138
|
-
}
|