@arcote.tech/arc-cli 0.7.20 → 0.7.22
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/index.js +225 -123
- package/package.json +9 -9
- package/src/builder/access-extractor.ts +11 -15
- package/src/builder/module-builder.ts +210 -35
- package/src/commands/platform-deploy.ts +42 -35
- package/src/deploy/deploy-env.ts +23 -46
- package/src/deploy/observability-configs.ts +14 -0
- package/src/deploy/remote-state.ts +39 -20
- package/src/deploy/ssh.ts +49 -0
- package/src/platform/server.ts +20 -3
- package/src/platform/shared.ts +34 -73
- package/src/platform/startup.ts +2 -2
package/src/deploy/ssh.ts
CHANGED
|
@@ -43,6 +43,27 @@ export interface SshExecResult {
|
|
|
43
43
|
exitCode: number;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* SSH connection multiplexing. Without this every remote command spawns a
|
|
48
|
+
* fresh `ssh` (full TCP + auth handshake) — a single deploy fires ~15-25 of
|
|
49
|
+
* them, so the handshake overhead alone runs into minutes. `ControlMaster=auto`
|
|
50
|
+
* makes the first connection open a master socket; every later ssh/scp to the
|
|
51
|
+
* same host reuses it with no handshake. `ControlPersist` keeps the master
|
|
52
|
+
* alive for the whole (short) deploy. The socket lives in ~/.ssh (private) and
|
|
53
|
+
* `%C` is ssh's hash of host+port+user — short enough for the ~104-byte socket
|
|
54
|
+
* path limit, and distinct per target (so the root fallback gets its own).
|
|
55
|
+
*/
|
|
56
|
+
function sshMuxArgs(): string[] {
|
|
57
|
+
return [
|
|
58
|
+
"-o",
|
|
59
|
+
"ControlMaster=auto",
|
|
60
|
+
"-o",
|
|
61
|
+
`ControlPath=${join(homedir(), ".ssh", "cm-arc-%C")}`,
|
|
62
|
+
"-o",
|
|
63
|
+
"ControlPersist=120",
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
export function baseSshArgs(target: DeployTarget): string[] {
|
|
47
68
|
// Pin to a single identity to avoid "Too many authentication failures":
|
|
48
69
|
// the server's MaxAuthTries=3 (set by ansible) trips when ssh-agent has
|
|
@@ -50,6 +71,7 @@ export function baseSshArgs(target: DeployTarget): string[] {
|
|
|
50
71
|
// PreferredAuthentications=publickey skips gssapi/keyboard prompts entirely.
|
|
51
72
|
const key = pickSshKey(target);
|
|
52
73
|
const args = [
|
|
74
|
+
...sshMuxArgs(),
|
|
53
75
|
"-o",
|
|
54
76
|
"BatchMode=yes",
|
|
55
77
|
"-o",
|
|
@@ -159,6 +181,8 @@ export async function scpUpload(
|
|
|
159
181
|
): Promise<void> {
|
|
160
182
|
const key = pickSshKey(target);
|
|
161
183
|
const args = [
|
|
184
|
+
// Same ControlPath as baseSshArgs — scp reuses the ssh master socket.
|
|
185
|
+
...sshMuxArgs(),
|
|
162
186
|
"-o",
|
|
163
187
|
"BatchMode=yes",
|
|
164
188
|
"-o",
|
|
@@ -184,3 +208,28 @@ export async function scpUpload(
|
|
|
184
208
|
throw new Error(`scp failed (${exitCode}): ${stderr}`);
|
|
185
209
|
}
|
|
186
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Close the multiplexed SSH master connection (best-effort). Call once at the
|
|
214
|
+
* end of a deploy so the control socket doesn't linger for ControlPersist
|
|
215
|
+
* seconds after the process exits. Safe to call even if no master was opened —
|
|
216
|
+
* `ssh -O exit` against a missing socket just returns non-zero, which we ignore.
|
|
217
|
+
*/
|
|
218
|
+
export async function closeSshMaster(target: DeployTarget): Promise<void> {
|
|
219
|
+
try {
|
|
220
|
+
const proc = spawn({
|
|
221
|
+
cmd: [
|
|
222
|
+
"ssh",
|
|
223
|
+
...baseSshArgs(target),
|
|
224
|
+
"-O",
|
|
225
|
+
"exit",
|
|
226
|
+
`${target.user}@${target.host}`,
|
|
227
|
+
],
|
|
228
|
+
stdout: "ignore",
|
|
229
|
+
stderr: "ignore",
|
|
230
|
+
});
|
|
231
|
+
await proc.exited;
|
|
232
|
+
} catch {
|
|
233
|
+
// best-effort cleanup — a missing/already-closed master is fine.
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/platform/server.ts
CHANGED
|
@@ -29,6 +29,13 @@ export interface PlatformServerOptions {
|
|
|
29
29
|
dbPath?: string;
|
|
30
30
|
/** If true, enables SSE reload stream + mutable manifest (dev mode) */
|
|
31
31
|
devMode?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Cross-origin isolation policy. Default `"unsafe-none"`. Apps z SQLite
|
|
34
|
+
* WASM/OPFS lub SharedArrayBuffer ustawiają `"require-corp"` w
|
|
35
|
+
* `deploy.arc.json` — wtedy każdy cross-origin resource (3rd-party
|
|
36
|
+
* widgets) musi mieć `Cross-Origin-Resource-Policy`. Patrz `ArcServerConfig.coep`.
|
|
37
|
+
*/
|
|
38
|
+
coep?: "unsafe-none" | "credentialless" | "require-corp";
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export interface PlatformServer {
|
|
@@ -507,6 +514,15 @@ export async function startPlatformServer(
|
|
|
507
514
|
opts: PlatformServerOptions,
|
|
508
515
|
): Promise<PlatformServer> {
|
|
509
516
|
const { ws, port, devMode, context } = opts;
|
|
517
|
+
// Default COEP: env var > opts > unsafe-none. Apps z SQLite WASM/OPFS
|
|
518
|
+
// muszą explicit ustawić "require-corp" (przez ARC_COEP w .env / Docker
|
|
519
|
+
// envVars). Default unsafe-none pozwala apps używać 3rd-party widgetów
|
|
520
|
+
// (PayU Secure Form, Stripe Elements, Google/Apple Pay) bez proxy.
|
|
521
|
+
const coep = (process.env.ARC_COEP as
|
|
522
|
+
| "unsafe-none"
|
|
523
|
+
| "credentialless"
|
|
524
|
+
| "require-corp"
|
|
525
|
+
| undefined) ?? opts.coep ?? "unsafe-none";
|
|
510
526
|
ensureModuleSigSecret(ws, !!devMode);
|
|
511
527
|
|
|
512
528
|
// OpenTelemetry — only when explicitly enabled (deploy injects the env
|
|
@@ -567,10 +583,10 @@ export async function startPlatformServer(
|
|
|
567
583
|
"Access-Control-Allow-Methods":
|
|
568
584
|
"GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
569
585
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Arc-Tokens",
|
|
570
|
-
// Cross-origin isolation —
|
|
571
|
-
//
|
|
586
|
+
// Cross-origin isolation — domyślnie unsafe-none (3rd-party widgets);
|
|
587
|
+
// require-corp tylko dla apps używających SharedArrayBuffer/OPFS.
|
|
572
588
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
573
|
-
"Cross-Origin-Embedder-Policy": "
|
|
589
|
+
"Cross-Origin-Embedder-Policy": coep ?? "unsafe-none",
|
|
574
590
|
"Cross-Origin-Resource-Policy": "cross-origin",
|
|
575
591
|
};
|
|
576
592
|
|
|
@@ -641,6 +657,7 @@ export async function startPlatformServer(
|
|
|
641
657
|
context,
|
|
642
658
|
dbAdapterFactory,
|
|
643
659
|
port,
|
|
660
|
+
coep,
|
|
644
661
|
httpHandlers: [
|
|
645
662
|
// Platform-specific handlers (checked AFTER arc handlers)
|
|
646
663
|
apiEndpointsHandler(ws, getManifest, null, moduleAccessMap, telemetry),
|
package/src/platform/shared.ts
CHANGED
|
@@ -4,10 +4,12 @@ import { dirname, join } from "path";
|
|
|
4
4
|
import {
|
|
5
5
|
buildBrowserApp,
|
|
6
6
|
buildContextPackages,
|
|
7
|
+
buildServerApp,
|
|
7
8
|
buildStyles,
|
|
8
9
|
buildTranslations,
|
|
9
10
|
discoverPackages,
|
|
10
11
|
isContextPackage,
|
|
12
|
+
SERVER_ENTRY_FILE,
|
|
11
13
|
type BrowserAppResult,
|
|
12
14
|
type BuildManifest,
|
|
13
15
|
type ModuleDescriptor,
|
|
@@ -161,20 +163,30 @@ export async function buildAll(
|
|
|
161
163
|
// else chunk grouping (per-package) silently mis-assigns the extra module.
|
|
162
164
|
assertOneModulePerPackage(ws.packages);
|
|
163
165
|
|
|
164
|
-
// Phase 1 — context
|
|
165
|
-
// subprocess imports workspace packages by name, which resolve through
|
|
166
|
-
// node_modules to packages' `main` field (typically `dist/server/main/`).
|
|
166
|
+
// Phase 1 — per-package BROWSER context bundles + type declarations.
|
|
167
167
|
await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
|
|
168
168
|
|
|
169
|
-
// Phase 1b —
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
169
|
+
// Phase 1b — combined server bundle at `.arc/platform/server/_server.js`.
|
|
170
|
+
// ONE Bun.build for all context modules (deduped from source, cycle-safe)
|
|
171
|
+
// replaces the old per-package server bundles that nested each other's dist
|
|
172
|
+
// into multi-hundred-MB files. The deploy image COPYs this dir wholesale;
|
|
173
|
+
// loadServerContext + the access extractor import the entry, chunks ride along.
|
|
174
|
+
const serverDir = join(ws.arcDir, "server");
|
|
175
|
+
const { entryFile: serverEntry } = await buildServerApp(
|
|
176
|
+
ws.rootDir,
|
|
177
|
+
serverDir,
|
|
178
|
+
ws.packages,
|
|
179
|
+
cache,
|
|
180
|
+
noCache,
|
|
181
|
+
);
|
|
173
182
|
|
|
174
183
|
// Phase 2 — extract access metadata (token name + hasCheck per module) in
|
|
175
|
-
// an isolated subprocess
|
|
176
|
-
// which token group each module belongs to.
|
|
177
|
-
const accessMap = await extractAccessMap(
|
|
184
|
+
// an isolated subprocess that imports the combined server bundle. MUST run
|
|
185
|
+
// before chunk planning so we know which token group each module belongs to.
|
|
186
|
+
const accessMap = await extractAccessMap(
|
|
187
|
+
ws.rootDir,
|
|
188
|
+
join(serverDir, serverEntry),
|
|
189
|
+
);
|
|
178
190
|
|
|
179
191
|
// Persist access map for the runtime host (server.ts reads at startup to
|
|
180
192
|
// wire up moduleAccessMap for filterManifestForTokens / signed URLs).
|
|
@@ -242,34 +254,6 @@ function assembleManifest(
|
|
|
242
254
|
};
|
|
243
255
|
}
|
|
244
256
|
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
// Context server bundles — flatten to `<arcDir>/server/<safeName>.js`
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Copy each context package's compiled server bundle from
|
|
251
|
-
* `packages/<pkg>/dist/server/main/index.js` to a flat location at
|
|
252
|
-
* `<arcDir>/server/<safeName>.js`. The flat layout makes the deploy image
|
|
253
|
-
* self-contained — `COPY .arc/platform/` pulls everything server-side, no
|
|
254
|
-
* need to drag the entire `packages/` tree into the image.
|
|
255
|
-
*/
|
|
256
|
-
function copyContextServerBundles(ws: WorkspaceInfo): void {
|
|
257
|
-
const outDir = join(ws.arcDir, "server");
|
|
258
|
-
mkdirSync(outDir, { recursive: true });
|
|
259
|
-
|
|
260
|
-
for (const pkg of ws.packages) {
|
|
261
|
-
if (!isContextPackage(pkg.packageJson)) continue;
|
|
262
|
-
const src = join(pkg.path, "dist", "server", "main", "index.js");
|
|
263
|
-
if (!existsSync(src)) {
|
|
264
|
-
err(`Server bundle missing for ${pkg.name}: ${src}`);
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const safeName = pkg.path.split("/").pop()!;
|
|
268
|
-
const dst = join(outDir, `${safeName}.js`);
|
|
269
|
-
copyFileSync(src, dst);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
257
|
// ---------------------------------------------------------------------------
|
|
274
258
|
// Browser assets — @arcote.tech/* deps deklarują w `arc.browserAssets` jakie
|
|
275
259
|
// pliki muszą być dostępne w przeglądarce (np. SQLite WASM worker + .wasm).
|
|
@@ -419,44 +403,21 @@ export async function loadServerContext(
|
|
|
419
403
|
|
|
420
404
|
await import(platformEntry);
|
|
421
405
|
|
|
422
|
-
//
|
|
406
|
+
// The combined server bundle lives at `<arcDir>/server/_server.js` (entry)
|
|
407
|
+
// next to its `chunk-<hash>.js` siblings. Importing the entry pulls the
|
|
408
|
+
// chunks transitively and registers every module via the platform singleton.
|
|
423
409
|
// The deploy image only has this directory — there's no workspace `packages/`
|
|
424
|
-
// tree
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
? readdirSync(serverDir).filter((f) => f.endsWith(".js"))
|
|
429
|
-
: [];
|
|
430
|
-
|
|
431
|
-
if (bundles.length > 0) {
|
|
432
|
-
for (const file of bundles) {
|
|
433
|
-
const bundlePath = join(serverDir, file);
|
|
434
|
-
try {
|
|
435
|
-
await import(bundlePath);
|
|
436
|
-
} catch (e) {
|
|
437
|
-
err(`Failed to load server bundle ${file}: ${e}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
} else if (ws.packages.length > 0) {
|
|
441
|
-
// Fallback for the "no .arc/platform/server/ yet" case (e.g. somebody
|
|
442
|
-
// wired up loadServerContext before running the build). This path goes
|
|
443
|
-
// through workspace packages directly — only meaningful in dev.
|
|
444
|
-
const ctxPackages = ws.packages.filter((p) => isContextPackage(p.packageJson));
|
|
445
|
-
for (const ctx of ctxPackages) {
|
|
446
|
-
const serverDist = join(ctx.path, "dist", "server", "main", "index.js");
|
|
447
|
-
if (!existsSync(serverDist)) {
|
|
448
|
-
err(`Context server dist not found: ${serverDist}`);
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
try {
|
|
452
|
-
await import(serverDist);
|
|
453
|
-
} catch (e) {
|
|
454
|
-
err(`Failed to load server context from ${ctx.name}: ${e}`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
} else {
|
|
410
|
+
// tree; dev and prod go through the exact same path.
|
|
411
|
+
const serverEntry = join(ws.arcDir, "server", SERVER_ENTRY_FILE);
|
|
412
|
+
if (!existsSync(serverEntry)) {
|
|
413
|
+
// No build yet (or a static-only workspace) — nothing to register.
|
|
458
414
|
return { context: null, moduleAccess: new Map() };
|
|
459
415
|
}
|
|
416
|
+
try {
|
|
417
|
+
await import(serverEntry);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
err(`Failed to load server bundle ${SERVER_ENTRY_FILE}: ${e}`);
|
|
420
|
+
}
|
|
460
421
|
|
|
461
422
|
const { getContext, getAllModuleAccess } = await import(platformEntry);
|
|
462
423
|
return {
|
package/src/platform/startup.ts
CHANGED
|
@@ -62,8 +62,8 @@ export async function startPlatform(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// 2. Server context — same code path in both modes; loadServerContext
|
|
65
|
-
//
|
|
66
|
-
//
|
|
65
|
+
// imports the combined server bundle at .arc/platform/server/_server.js
|
|
66
|
+
// (produced by the buildServerApp step in buildAll).
|
|
67
67
|
log("Loading server context...");
|
|
68
68
|
const { context, moduleAccess } = await loadServerContext(ws);
|
|
69
69
|
if (context) {
|