@arcote.tech/arc-cli 0.8.1 → 0.8.2
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 +51 -26
- package/package.json +10 -10
- package/src/commands/platform-pairing-token.ts +53 -0
- package/src/index.ts +8 -0
- package/src/platform/server.ts +6 -8
- package/src/platform/startup.ts +15 -19
package/dist/index.js
CHANGED
|
@@ -38266,6 +38266,14 @@ async function createArcServer(config) {
|
|
|
38266
38266
|
if (contentLength > maxRequestBodySize) {
|
|
38267
38267
|
return Response.json({ error: "Payload too large" }, { status: 413, headers: corsHeaders2 });
|
|
38268
38268
|
}
|
|
38269
|
+
if (url.pathname === "/ws" && req.headers.get("Upgrade") === "websocket") {
|
|
38270
|
+
if (server2.upgrade(req, { data: { clientId: "" } }))
|
|
38271
|
+
return;
|
|
38272
|
+
return new Response("WebSocket upgrade failed", {
|
|
38273
|
+
status: 500,
|
|
38274
|
+
headers: corsHeaders2
|
|
38275
|
+
});
|
|
38276
|
+
}
|
|
38269
38277
|
const authHeader = req.headers.get("Authorization");
|
|
38270
38278
|
let rawToken = authHeader?.replace("Bearer ", "") || null;
|
|
38271
38279
|
if (!rawToken && config.allowTokenInQuery) {
|
|
@@ -38283,14 +38291,6 @@ async function createArcServer(config) {
|
|
|
38283
38291
|
if (rawToken && !tokenPayload) {
|
|
38284
38292
|
return Response.json({ error: "Unauthorized" }, { status: 401, headers: corsHeaders2 });
|
|
38285
38293
|
}
|
|
38286
|
-
if (url.pathname === "/ws" && req.headers.get("Upgrade") === "websocket") {
|
|
38287
|
-
if (server2.upgrade(req, { data: { clientId: "" } }))
|
|
38288
|
-
return;
|
|
38289
|
-
return new Response("WebSocket upgrade failed", {
|
|
38290
|
-
status: 500,
|
|
38291
|
-
headers: corsHeaders2
|
|
38292
|
-
});
|
|
38293
|
-
}
|
|
38294
38294
|
const reqCtx = { rawToken, tokenPayload, corsHeaders: corsHeaders2 };
|
|
38295
38295
|
for (const handler of httpHandlers) {
|
|
38296
38296
|
const response = await handler(req, url, reqCtx);
|
|
@@ -38404,6 +38404,13 @@ init_i18n();
|
|
|
38404
38404
|
import { timingSafeEqual } from "crypto";
|
|
38405
38405
|
import { existsSync as existsSync19, mkdirSync as mkdirSync15 } from "fs";
|
|
38406
38406
|
import { join as join22 } from "path";
|
|
38407
|
+
async function initContextHandler(context2, dbPath) {
|
|
38408
|
+
const factory = await resolveDbAdapterFactory(dbPath);
|
|
38409
|
+
const dbAdapter = factory(context2);
|
|
38410
|
+
const handler = new ContextHandler(context2, dbAdapter);
|
|
38411
|
+
await handler.init();
|
|
38412
|
+
return handler;
|
|
38413
|
+
}
|
|
38407
38414
|
async function resolveDbAdapterFactory(dbPath) {
|
|
38408
38415
|
const databaseUrl = process.env.DATABASE_URL;
|
|
38409
38416
|
if (databaseUrl && databaseUrl.length > 0) {
|
|
@@ -38684,13 +38691,6 @@ function handleDiscovery(req, ctx, ws, manifest, federation, mapUrl) {
|
|
|
38684
38691
|
if (!federation) {
|
|
38685
38692
|
return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
|
|
38686
38693
|
}
|
|
38687
|
-
const presented = req.headers.get("X-Arc-Pairing-Token");
|
|
38688
|
-
if (!presented || !safeEqual(presented, federation.registrationToken)) {
|
|
38689
|
-
return new Response("Unauthorized", {
|
|
38690
|
-
status: 401,
|
|
38691
|
-
headers: ctx.corsHeaders
|
|
38692
|
-
});
|
|
38693
|
-
}
|
|
38694
38694
|
const fed = manifest.federation;
|
|
38695
38695
|
const platformUrl = process.env.ARC_PUBLIC_URL || new URL(req.url).origin;
|
|
38696
38696
|
return Response.json({
|
|
@@ -39136,18 +39136,12 @@ async function startMapTool(ws, port, authToken, platformServer, platform3) {
|
|
|
39136
39136
|
function assertProdFederationSecrets(devMode, hasFederation) {
|
|
39137
39137
|
if (devMode || !hasFederation)
|
|
39138
39138
|
return;
|
|
39139
|
-
|
|
39140
|
-
if (!process.env.ARC_APP_SECRET)
|
|
39141
|
-
missing.push("ARC_APP_SECRET");
|
|
39142
|
-
if (!process.env.ARC_PAIRING_TOKEN && !process.env.ARC_MAP_TOKEN) {
|
|
39143
|
-
missing.push("ARC_PAIRING_TOKEN");
|
|
39144
|
-
}
|
|
39145
|
-
if (missing.length === 0)
|
|
39139
|
+
if (process.env.ARC_PAIRING_TOKEN || process.env.ARC_MAP_TOKEN)
|
|
39146
39140
|
return;
|
|
39147
|
-
err(`Federacja w produkcji wymaga
|
|
39148
|
-
` + ` Bez
|
|
39149
|
-
` + ` recreate \u2014
|
|
39150
|
-
` + ` Ustaw
|
|
39141
|
+
err(`Federacja w produkcji wymaga zmiennej \u015Brodowiskowej ARC_PAIRING_TOKEN.
|
|
39142
|
+
` + ` Bez niej token parowania jest generowany w kontenerze i GINIE przy
|
|
39143
|
+
` + ` pierwszym recreate \u2014 parowania host\xF3w staj\u0105 si\u0119 martwe, bez b\u0142\u0119du.
|
|
39144
|
+
` + ` Ustaw j\u0105 w deployu (deploy.arc.json \u2192 envVars + deploy.arc.<env>.env).`);
|
|
39151
39145
|
process.exit(1);
|
|
39152
39146
|
}
|
|
39153
39147
|
function resolveAppSecret(rootDir) {
|
|
@@ -39258,6 +39252,36 @@ async function platformDev(opts = {}) {
|
|
|
39258
39252
|
});
|
|
39259
39253
|
}
|
|
39260
39254
|
|
|
39255
|
+
// src/commands/platform-pairing-token.ts
|
|
39256
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
39257
|
+
import { join as join24 } from "path";
|
|
39258
|
+
async function platformPairingToken() {
|
|
39259
|
+
const ws = resolveWorkspace();
|
|
39260
|
+
const { context: context2 } = await loadServerContext(ws);
|
|
39261
|
+
if (!context2) {
|
|
39262
|
+
err("Brak zbudowanego kontekstu. Uruchom najpierw `arc platform build`.");
|
|
39263
|
+
process.exit(1);
|
|
39264
|
+
}
|
|
39265
|
+
const isProd = false;
|
|
39266
|
+
const dbPath = join24(ws.rootDir, ".arc", "data", isProd ? "prod.db" : "dev.db");
|
|
39267
|
+
const handler = await initContextHandler(context2, dbPath);
|
|
39268
|
+
const code = randomBytes3(24).toString("base64url");
|
|
39269
|
+
const res = await handler.executeCommand("pairingCode.issue", { code }, null, { internal: true });
|
|
39270
|
+
if (!res?.ok) {
|
|
39271
|
+
err(`Nie uda\u0142o si\u0119 zapisa\u0107 kodu parowania${res?.error ? `: ${res.error}` : ""}. ` + "Czy aplikacja wystawia agregat `pairingCode` (federacja)?");
|
|
39272
|
+
process.exit(1);
|
|
39273
|
+
}
|
|
39274
|
+
process.stdout.write(`
|
|
39275
|
+
Kod parowania (jednorazowy):
|
|
39276
|
+
|
|
39277
|
+
${code}
|
|
39278
|
+
|
|
39279
|
+
` + ` Podaj go w ho\u015Bcie przy instalacji aplikacji. Wygasa po pierwszym u\u017Cyciu.
|
|
39280
|
+
|
|
39281
|
+
`);
|
|
39282
|
+
process.exit(0);
|
|
39283
|
+
}
|
|
39284
|
+
|
|
39261
39285
|
// src/commands/platform-start.ts
|
|
39262
39286
|
async function platformStart() {
|
|
39263
39287
|
const ws = resolveWorkspace();
|
|
@@ -39277,6 +39301,7 @@ platform3.command("dev").description("Start platform in dev mode (Bun server + V
|
|
|
39277
39301
|
}));
|
|
39278
39302
|
platform3.command("build").description("Build platform for production").option("--no-cache", "Force full rebuild").action((opts) => platformBuild({ noCache: opts.cache === false }));
|
|
39279
39303
|
platform3.command("start").description("Start platform in production mode (requires prior build)").action(platformStart);
|
|
39304
|
+
platform3.command("pairing-token").description("Generate a one-time federation pairing code (writes to DB, prints it)").action(platformPairingToken);
|
|
39280
39305
|
platform3.command("deploy [env]").description("Deploy platform to a remote server (reads deploy.arc.json, surveys if missing)").option("--skip-build", "Skip local build step").option("--rebuild", "Force rebuild before deploy").option("--build-only", "Build the Docker image locally, then exit (no remote push)").option("--image-tag <hash>", "Roll back / pin to an existing image tag instead of building a new one").option("--force-bootstrap", "Re-run Ansible host bootstrap even if the server is already configured").action((env2, opts) => platformDeploy(env2, opts));
|
|
39281
39306
|
program2.parse(process.argv);
|
|
39282
39307
|
if (process.argv.length === 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform --external @arcote.tech/arc-map --external '@opentelemetry/*' && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.8.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.8.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.8.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.8.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.8.
|
|
20
|
-
"@arcote.tech/arc-adapter-db-postgres": "^0.8.
|
|
21
|
-
"@arcote.tech/arc-otel": "^0.8.
|
|
15
|
+
"@arcote.tech/arc": "^0.8.2",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.8.2",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.8.2",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.8.2",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.8.2",
|
|
20
|
+
"@arcote.tech/arc-adapter-db-postgres": "^0.8.2",
|
|
21
|
+
"@arcote.tech/arc-otel": "^0.8.2",
|
|
22
22
|
"@opentelemetry/api": "^1.9.0",
|
|
23
23
|
"@opentelemetry/api-logs": "^0.57.0",
|
|
24
24
|
"@opentelemetry/core": "^1.30.0",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
32
32
|
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.27.0",
|
|
34
|
-
"@arcote.tech/platform": "^0.8.
|
|
35
|
-
"@arcote.tech/arc-map": "^0.8.
|
|
34
|
+
"@arcote.tech/platform": "^0.8.2",
|
|
35
|
+
"@arcote.tech/arc-map": "^0.8.2",
|
|
36
36
|
"@clack/prompts": "^0.9.0",
|
|
37
37
|
"commander": "^11.1.0",
|
|
38
38
|
"chokidar": "^3.5.3",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { initContextHandler } from "../platform/server";
|
|
4
|
+
import { err, loadServerContext, resolveWorkspace } from "../platform/shared";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `arc platform pairing-token` — generuje JEDNORAZOWY kod parowania federacji
|
|
8
|
+
* i zapisuje go do bazy (agregat `pairingCode`), po czym wypisuje go operatorowi.
|
|
9
|
+
*
|
|
10
|
+
* Kod podaje się w HOŚCIE przy instalacji aplikacji; host rejestruje nim swój
|
|
11
|
+
* klucz publiczny (komenda `federationHost.register`), a kod zostaje zużyty.
|
|
12
|
+
* Uruchamiane po SSH na serwerze aplikacji — czyta ten sam kontekst i DB co
|
|
13
|
+
* serwer (postgres przez `DATABASE_URL`, inaczej sqlite w `.arc/data`).
|
|
14
|
+
*/
|
|
15
|
+
export async function platformPairingToken(): Promise<void> {
|
|
16
|
+
const ws = resolveWorkspace();
|
|
17
|
+
const { context } = await loadServerContext(ws);
|
|
18
|
+
if (!context) {
|
|
19
|
+
err("Brak zbudowanego kontekstu. Uruchom najpierw `arc platform build`.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// TEN SAM plik/baza co serwer platformy (startup.ts): sqlite
|
|
24
|
+
// `.arc/data/<mode>.db` w dev/prod-sqlite, albo postgres przez DATABASE_URL
|
|
25
|
+
// (wtedy dbPath jest ignorowany). Bez tego kod trafiłby do innej bazy niż
|
|
26
|
+
// serwer i rejestracja by go nie znalazła.
|
|
27
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
28
|
+
const dbPath = join(ws.rootDir, ".arc", "data", isProd ? "prod.db" : "dev.db");
|
|
29
|
+
const handler = await initContextHandler(context, dbPath);
|
|
30
|
+
|
|
31
|
+
// Kod = sekret pokazywany raz operatorowi. `_id` agregatu generuje handler.
|
|
32
|
+
const code = randomBytes(24).toString("base64url");
|
|
33
|
+
const res = (await handler.executeCommand(
|
|
34
|
+
"pairingCode.issue",
|
|
35
|
+
{ code },
|
|
36
|
+
null,
|
|
37
|
+
{ internal: true },
|
|
38
|
+
)) as { ok?: boolean; error?: string } | undefined;
|
|
39
|
+
|
|
40
|
+
if (!res?.ok) {
|
|
41
|
+
err(
|
|
42
|
+
`Nie udało się zapisać kodu parowania${res?.error ? `: ${res.error}` : ""}. ` +
|
|
43
|
+
"Czy aplikacja wystawia agregat `pairingCode` (federacja)?",
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
`\n Kod parowania (jednorazowy):\n\n ${code}\n\n` +
|
|
50
|
+
" Podaj go w hoście przy instalacji aplikacji. Wygasa po pierwszym użyciu.\n\n",
|
|
51
|
+
);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { dev } from "./commands/dev";
|
|
|
6
6
|
import { platformBuild } from "./commands/platform-build";
|
|
7
7
|
import { platformDeploy } from "./commands/platform-deploy";
|
|
8
8
|
import { platformDev } from "./commands/platform-dev";
|
|
9
|
+
import { platformPairingToken } from "./commands/platform-pairing-token";
|
|
9
10
|
import { platformStart } from "./commands/platform-start";
|
|
10
11
|
|
|
11
12
|
// Create the program
|
|
@@ -60,6 +61,13 @@ platform
|
|
|
60
61
|
.description("Start platform in production mode (requires prior build)")
|
|
61
62
|
.action(platformStart);
|
|
62
63
|
|
|
64
|
+
platform
|
|
65
|
+
.command("pairing-token")
|
|
66
|
+
.description(
|
|
67
|
+
"Generate a one-time federation pairing code (writes to DB, prints it)",
|
|
68
|
+
)
|
|
69
|
+
.action(platformPairingToken);
|
|
70
|
+
|
|
63
71
|
platform
|
|
64
72
|
.command("deploy [env]")
|
|
65
73
|
.description(
|
package/src/platform/server.ts
CHANGED
|
@@ -573,14 +573,12 @@ function handleDiscovery(
|
|
|
573
573
|
if (!federation) {
|
|
574
574
|
return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
|
|
575
575
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
576
|
+
// Discovery jest PUBLICZNE (bez bramy tokenem). Zwraca wyłącznie metadane
|
|
577
|
+
// federacji (slug, moduły, uprawnienia, peers) — a te i tak wskazują na
|
|
578
|
+
// publiczne chunki `/browser-fed/`. Bramą PAROWANIA jest osobny JEDNORAZOWY
|
|
579
|
+
// kod sprawdzany przy rejestracji klucza (`federationHost.register`), nie ten
|
|
580
|
+
// odczyt. Host dzięki temu może pokazać ekran zgody (co instaluje), zanim
|
|
581
|
+
// operator poda kod.
|
|
584
582
|
const fed = manifest.federation;
|
|
585
583
|
// Adres własny: za proxy `req.url` jest wewnętrzny, więc pozwalamy nadpisać
|
|
586
584
|
// przez env. Konsument i tak zna nas z adresu, którego użył — to pole jest
|
package/src/platform/startup.ts
CHANGED
|
@@ -232,37 +232,33 @@ async function startMapTool(
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
|
-
* PROD + federacja →
|
|
235
|
+
* PROD + federacja → token parowania MUSI pochodzić z env. Twarda odmowa startu.
|
|
236
236
|
*
|
|
237
237
|
* DLACZEGO tak ostro: `.arc/` nie trafia ani do obrazu (deploy kopiuje tylko
|
|
238
|
-
* `.arc/platform/`), ani na wolumen (montowane jest tylko `.arc/data`).
|
|
238
|
+
* `.arc/platform/`), ani na wolumen (montowane jest tylko `.arc/data`). Token
|
|
239
239
|
* wygenerowany wewnątrz kontenera przeżywa `restart`, ale **ginie przy pierwszym
|
|
240
|
-
* `recreate`**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* bez jednego błędu przy starcie,
|
|
244
|
-
* - nowy token parowania → istniejące parowania martwe.
|
|
240
|
+
* `recreate`** — a wtedy nowy token parowania unieważnia istniejące parowania,
|
|
241
|
+
* bez żadnego błędu przy starcie. Cicha awaria dzień po deployu jest gorsza niż
|
|
242
|
+
* głośna odmowa startu.
|
|
245
243
|
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
244
|
+
* `ARC_APP_SECRET` NIE jest już wymagany: tożsamość federacji jest asymetryczna
|
|
245
|
+
* (host podpisuje kluczem prywatnym, partner weryfikuje publicznym z rejestru w
|
|
246
|
+
* bazie — patrz app-identity.md), więc partner nie trzyma współdzielonego
|
|
247
|
+
* sekretu HMAC. Klucze publiczne żyją w projekcji (wolumen `.arc/data`), nie w
|
|
248
|
+
* ulotnym pliku.
|
|
248
249
|
*/
|
|
249
250
|
function assertProdFederationSecrets(
|
|
250
251
|
devMode: boolean,
|
|
251
252
|
hasFederation: boolean,
|
|
252
253
|
): void {
|
|
253
254
|
if (devMode || !hasFederation) return;
|
|
254
|
-
|
|
255
|
-
if (!process.env.ARC_APP_SECRET) missing.push("ARC_APP_SECRET");
|
|
256
|
-
if (!process.env.ARC_PAIRING_TOKEN && !process.env.ARC_MAP_TOKEN) {
|
|
257
|
-
missing.push("ARC_PAIRING_TOKEN");
|
|
258
|
-
}
|
|
259
|
-
if (missing.length === 0) return;
|
|
255
|
+
if (process.env.ARC_PAIRING_TOKEN || process.env.ARC_MAP_TOKEN) return;
|
|
260
256
|
|
|
261
257
|
err(
|
|
262
|
-
`Federacja w produkcji wymaga
|
|
263
|
-
` Bez
|
|
264
|
-
` recreate —
|
|
265
|
-
` Ustaw
|
|
258
|
+
`Federacja w produkcji wymaga zmiennej środowiskowej ARC_PAIRING_TOKEN.\n` +
|
|
259
|
+
` Bez niej token parowania jest generowany w kontenerze i GINIE przy\n` +
|
|
260
|
+
` pierwszym recreate — parowania hostów stają się martwe, bez błędu.\n` +
|
|
261
|
+
` Ustaw ją w deployu (deploy.arc.json → envVars + deploy.arc.<env>.env).`,
|
|
266
262
|
);
|
|
267
263
|
process.exit(1);
|
|
268
264
|
}
|