@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 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
- const missing = [];
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 zmiennych \u015Brodowiskowych: ${missing.join(", ")}.
39148
- ` + ` Bez nich sekret i token s\u0105 generowane w kontenerze i GIN\u0104 przy pierwszym
39149
- ` + ` recreate \u2014 hosty musia\u0142yby przeinstalowa\u0107 aplikacj\u0119, bez \u017Cadnego b\u0142\u0119du.
39150
- ` + ` Ustaw je w deployu (deploy.arc.json \u2192 envVars + deploy.arc.<env>.env).`);
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.1",
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.1",
16
- "@arcote.tech/arc-ds": "^0.8.1",
17
- "@arcote.tech/arc-react": "^0.8.1",
18
- "@arcote.tech/arc-host": "^0.8.1",
19
- "@arcote.tech/arc-adapter-db-sqlite": "^0.8.1",
20
- "@arcote.tech/arc-adapter-db-postgres": "^0.8.1",
21
- "@arcote.tech/arc-otel": "^0.8.1",
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.1",
35
- "@arcote.tech/arc-map": "^0.8.1",
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(
@@ -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
- const presented = req.headers.get("X-Arc-Pairing-Token");
577
- if (!presented || !safeEqual(presented, federation.registrationToken)) {
578
- return new Response("Unauthorized", {
579
- status: 401,
580
- headers: ctx.corsHeaders,
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
@@ -232,37 +232,33 @@ async function startMapTool(
232
232
  }
233
233
 
234
234
  /**
235
- * PROD + federacja → sekrety MUSZĄ pochodzić z env. Twarda odmowa startu.
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`). Plik
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`** (nowy obraz = nowy kontener). Wtedy:
241
- * - nowy `ARC_APP_SECRET` hosty trzymają STARY sekret, więc żadna asercja
242
- * tożsamości się nie zweryfikuje i aplikacja przestaje działać u wszystkich,
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
- * Cicha awaria dzień po deployu jest dużo gorsza niż głośna odmowa startu.
247
- * Wymieniamy WSZYSTKIE braki naraz inaczej operator poprawia po jednej na deploy.
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
- const missing: string[] = [];
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 zmiennych środowiskowych: ${missing.join(", ")}.\n` +
263
- ` Bez nich sekret i token generowane w kontenerze i GINĄ przy pierwszym\n` +
264
- ` recreate — hosty musiałyby przeinstalować aplikację, bez żadnego błędu.\n` +
265
- ` Ustaw je w deployu (deploy.arc.json → envVars + deploy.arc.<env>.env).`,
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 w deployu (deploy.arc.json → envVars + deploy.arc.<env>.env).`,
266
262
  );
267
263
  process.exit(1);
268
264
  }