@arcote.tech/arc-cli 0.8.0 → 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
@@ -10204,11 +10204,18 @@ function notifyTokenChange(scope) {
10204
10204
 
10205
10205
  class AuthAdapter {
10206
10206
  scopes = new Map;
10207
+ namespace;
10208
+ constructor(options) {
10209
+ this.namespace = options?.storageNamespace || undefined;
10210
+ }
10211
+ storageKey(scope) {
10212
+ return this.namespace ? `${TOKEN_PREFIX}${this.namespace}:${scope}` : TOKEN_PREFIX + scope;
10213
+ }
10207
10214
  setToken(token, scope = "default") {
10208
10215
  if (!token) {
10209
10216
  this.scopes.delete(scope);
10210
10217
  if (hasLocalStorage()) {
10211
- localStorage.removeItem(TOKEN_PREFIX + scope);
10218
+ localStorage.removeItem(this.storageKey(scope));
10212
10219
  notifyTokenChange(scope);
10213
10220
  }
10214
10221
  return;
@@ -10218,7 +10225,7 @@ class AuthAdapter {
10218
10225
  if (parts.length !== 3) {
10219
10226
  this.scopes.delete(scope);
10220
10227
  if (hasLocalStorage())
10221
- localStorage.removeItem(TOKEN_PREFIX + scope);
10228
+ localStorage.removeItem(this.storageKey(scope));
10222
10229
  return;
10223
10230
  }
10224
10231
  const payload = JSON.parse(atob(parts[1]));
@@ -10232,13 +10239,13 @@ class AuthAdapter {
10232
10239
  }
10233
10240
  });
10234
10241
  if (hasLocalStorage()) {
10235
- localStorage.setItem(TOKEN_PREFIX + scope, token);
10242
+ localStorage.setItem(this.storageKey(scope), token);
10236
10243
  notifyTokenChange(scope);
10237
10244
  }
10238
10245
  } catch {
10239
10246
  this.scopes.delete(scope);
10240
10247
  if (hasLocalStorage())
10241
- localStorage.removeItem(TOKEN_PREFIX + scope);
10248
+ localStorage.removeItem(this.storageKey(scope));
10242
10249
  }
10243
10250
  }
10244
10251
  setDecoded(decoded, scope = "default") {
@@ -10247,10 +10254,13 @@ class AuthAdapter {
10247
10254
  loadPersisted() {
10248
10255
  if (!hasLocalStorage())
10249
10256
  return;
10257
+ const prefix = this.namespace ? `${TOKEN_PREFIX}${this.namespace}:` : TOKEN_PREFIX;
10250
10258
  for (let i = 0;i < localStorage.length; i++) {
10251
10259
  const key = localStorage.key(i);
10252
- if (key?.startsWith(TOKEN_PREFIX)) {
10253
- const scope = key.slice(TOKEN_PREFIX.length);
10260
+ if (key?.startsWith(prefix)) {
10261
+ const scope = key.slice(prefix.length);
10262
+ if (!this.namespace && scope.includes(":"))
10263
+ continue;
10254
10264
  const raw = localStorage.getItem(key);
10255
10265
  if (raw) {
10256
10266
  try {
@@ -10304,7 +10314,7 @@ class AuthAdapter {
10304
10314
  clear() {
10305
10315
  if (hasLocalStorage()) {
10306
10316
  for (const scope of this.scopes.keys()) {
10307
- localStorage.removeItem(TOKEN_PREFIX + scope);
10317
+ localStorage.removeItem(this.storageKey(scope));
10308
10318
  }
10309
10319
  }
10310
10320
  this.scopes.clear();
@@ -11393,6 +11403,8 @@ class ArcFunction {
11393
11403
  if (!tokenInstance) {
11394
11404
  return false;
11395
11405
  }
11406
+ if (!protection.check)
11407
+ continue;
11396
11408
  const result = await protection.check(tokenInstance);
11397
11409
  if (result === false) {
11398
11410
  return false;
@@ -17839,11 +17851,18 @@ function notifyTokenChange2(scope) {
17839
17851
 
17840
17852
  class AuthAdapter2 {
17841
17853
  scopes = new Map;
17854
+ namespace;
17855
+ constructor(options) {
17856
+ this.namespace = options?.storageNamespace || undefined;
17857
+ }
17858
+ storageKey(scope) {
17859
+ return this.namespace ? `${TOKEN_PREFIX2}${this.namespace}:${scope}` : TOKEN_PREFIX2 + scope;
17860
+ }
17842
17861
  setToken(token, scope = "default") {
17843
17862
  if (!token) {
17844
17863
  this.scopes.delete(scope);
17845
17864
  if (hasLocalStorage2()) {
17846
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17865
+ localStorage.removeItem(this.storageKey(scope));
17847
17866
  notifyTokenChange2(scope);
17848
17867
  }
17849
17868
  return;
@@ -17853,7 +17872,7 @@ class AuthAdapter2 {
17853
17872
  if (parts.length !== 3) {
17854
17873
  this.scopes.delete(scope);
17855
17874
  if (hasLocalStorage2())
17856
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17875
+ localStorage.removeItem(this.storageKey(scope));
17857
17876
  return;
17858
17877
  }
17859
17878
  const payload = JSON.parse(atob(parts[1]));
@@ -17867,13 +17886,13 @@ class AuthAdapter2 {
17867
17886
  }
17868
17887
  });
17869
17888
  if (hasLocalStorage2()) {
17870
- localStorage.setItem(TOKEN_PREFIX2 + scope, token);
17889
+ localStorage.setItem(this.storageKey(scope), token);
17871
17890
  notifyTokenChange2(scope);
17872
17891
  }
17873
17892
  } catch {
17874
17893
  this.scopes.delete(scope);
17875
17894
  if (hasLocalStorage2())
17876
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17895
+ localStorage.removeItem(this.storageKey(scope));
17877
17896
  }
17878
17897
  }
17879
17898
  setDecoded(decoded, scope = "default") {
@@ -17882,10 +17901,13 @@ class AuthAdapter2 {
17882
17901
  loadPersisted() {
17883
17902
  if (!hasLocalStorage2())
17884
17903
  return;
17904
+ const prefix = this.namespace ? `${TOKEN_PREFIX2}${this.namespace}:` : TOKEN_PREFIX2;
17885
17905
  for (let i = 0;i < localStorage.length; i++) {
17886
17906
  const key = localStorage.key(i);
17887
- if (key?.startsWith(TOKEN_PREFIX2)) {
17888
- const scope = key.slice(TOKEN_PREFIX2.length);
17907
+ if (key?.startsWith(prefix)) {
17908
+ const scope = key.slice(prefix.length);
17909
+ if (!this.namespace && scope.includes(":"))
17910
+ continue;
17889
17911
  const raw = localStorage.getItem(key);
17890
17912
  if (raw) {
17891
17913
  try {
@@ -17939,7 +17961,7 @@ class AuthAdapter2 {
17939
17961
  clear() {
17940
17962
  if (hasLocalStorage2()) {
17941
17963
  for (const scope of this.scopes.keys()) {
17942
- localStorage.removeItem(TOKEN_PREFIX2 + scope);
17964
+ localStorage.removeItem(this.storageKey(scope));
17943
17965
  }
17944
17966
  }
17945
17967
  this.scopes.clear();
@@ -19028,6 +19050,8 @@ class ArcFunction2 {
19028
19050
  if (!tokenInstance) {
19029
19051
  return false;
19030
19052
  }
19053
+ if (!protection.check)
19054
+ continue;
19031
19055
  const result = await protection.check(tokenInstance);
19032
19056
  if (result === false) {
19033
19057
  return false;
@@ -36633,6 +36657,7 @@ async function platformDeploy(envArg, options = {}) {
36633
36657
  ensurePersistedSecret(ws.rootDir, name, "ARC_APP_SECRET", () => randomBytes(32).toString("base64url"));
36634
36658
  ensurePersistedSecret(ws.rootDir, name, "ARC_PAIRING_TOKEN", () => randomBytes(16).toString("base64url"));
36635
36659
  }
36660
+ cfg = loadDeployConfig(ws.rootDir);
36636
36661
  }
36637
36662
  if (cfg.observability?.enabled) {
36638
36663
  const key = cfg.observability.adminPasswordEnv ?? "ARC_OBSERVABILITY_PASSWORD";
@@ -38241,6 +38266,14 @@ async function createArcServer(config) {
38241
38266
  if (contentLength > maxRequestBodySize) {
38242
38267
  return Response.json({ error: "Payload too large" }, { status: 413, headers: corsHeaders2 });
38243
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
+ }
38244
38277
  const authHeader = req.headers.get("Authorization");
38245
38278
  let rawToken = authHeader?.replace("Bearer ", "") || null;
38246
38279
  if (!rawToken && config.allowTokenInQuery) {
@@ -38258,14 +38291,6 @@ async function createArcServer(config) {
38258
38291
  if (rawToken && !tokenPayload) {
38259
38292
  return Response.json({ error: "Unauthorized" }, { status: 401, headers: corsHeaders2 });
38260
38293
  }
38261
- if (url.pathname === "/ws" && req.headers.get("Upgrade") === "websocket") {
38262
- if (server2.upgrade(req, { data: { clientId: "" } }))
38263
- return;
38264
- return new Response("WebSocket upgrade failed", {
38265
- status: 500,
38266
- headers: corsHeaders2
38267
- });
38268
- }
38269
38294
  const reqCtx = { rawToken, tokenPayload, corsHeaders: corsHeaders2 };
38270
38295
  for (const handler of httpHandlers) {
38271
38296
  const response = await handler(req, url, reqCtx);
@@ -38379,7 +38404,13 @@ init_i18n();
38379
38404
  import { timingSafeEqual } from "crypto";
38380
38405
  import { existsSync as existsSync19, mkdirSync as mkdirSync15 } from "fs";
38381
38406
  import { join as join22 } from "path";
38382
- import { MAP_SCHEMA_VERSION } from "@arcote.tech/arc-map/types";
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
+ }
38383
38414
  async function resolveDbAdapterFactory(dbPath) {
38384
38415
  const databaseUrl = process.env.DATABASE_URL;
38385
38416
  if (databaseUrl && databaseUrl.length > 0) {
@@ -38648,6 +38679,7 @@ function parseCorsOrigins(raw) {
38648
38679
  const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
38649
38680
  return list.length > 0 ? list : undefined;
38650
38681
  }
38682
+ var DISCOVERY_SCHEMA_VERSION = 1;
38651
38683
  function safeEqual(a2, b4) {
38652
38684
  const ab = Buffer.from(a2);
38653
38685
  const bb = Buffer.from(b4);
@@ -38659,18 +38691,11 @@ function handleDiscovery(req, ctx, ws, manifest, federation, mapUrl) {
38659
38691
  if (!federation) {
38660
38692
  return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
38661
38693
  }
38662
- const presented = req.headers.get("X-Arc-Pairing-Token");
38663
- if (!presented || !safeEqual(presented, federation.registrationToken)) {
38664
- return new Response("Unauthorized", {
38665
- status: 401,
38666
- headers: ctx.corsHeaders
38667
- });
38668
- }
38669
38694
  const fed = manifest.federation;
38670
38695
  const platformUrl = process.env.ARC_PUBLIC_URL || new URL(req.url).origin;
38671
38696
  return Response.json({
38672
38697
  meta: {
38673
- schemaVersion: MAP_SCHEMA_VERSION,
38698
+ schemaVersion: DISCOVERY_SCHEMA_VERSION,
38674
38699
  name: ws.appName,
38675
38700
  version: ws.rootPkg?.version ?? undefined
38676
38701
  },
@@ -39085,11 +39110,11 @@ async function startPlatform(opts) {
39085
39110
  }
39086
39111
  async function startMapTool(ws, port, authToken, platformServer, platform3) {
39087
39112
  try {
39088
- const { startMapServer, MAP_SCHEMA_VERSION: MAP_SCHEMA_VERSION2 } = await import("@arcote.tech/arc-map");
39113
+ const { startMapServer, MAP_SCHEMA_VERSION } = await import("@arcote.tech/arc-map");
39089
39114
  const { getContext } = await import("@arcote.tech/platform");
39090
39115
  const globs = ws.packages.map((p3) => join23(p3.path, "src", "**", "*.{ts,tsx}"));
39091
39116
  const packageOf = (absFile) => ws.packages.find((p3) => absFile.startsWith(join23(p3.path, "src")))?.name;
39092
- const meta = readAppMeta(ws.rootDir, MAP_SCHEMA_VERSION2);
39117
+ const meta = readAppMeta(ws.rootDir, MAP_SCHEMA_VERSION);
39093
39118
  const map = await startMapServer({
39094
39119
  getContext: () => getContext(),
39095
39120
  scan: { globs, workspaceRoot: ws.rootDir, packageOf },
@@ -39111,18 +39136,12 @@ async function startMapTool(ws, port, authToken, platformServer, platform3) {
39111
39136
  function assertProdFederationSecrets(devMode, hasFederation) {
39112
39137
  if (devMode || !hasFederation)
39113
39138
  return;
39114
- const missing = [];
39115
- if (!process.env.ARC_APP_SECRET)
39116
- missing.push("ARC_APP_SECRET");
39117
- if (!process.env.ARC_PAIRING_TOKEN && !process.env.ARC_MAP_TOKEN) {
39118
- missing.push("ARC_PAIRING_TOKEN");
39119
- }
39120
- if (missing.length === 0)
39139
+ if (process.env.ARC_PAIRING_TOKEN || process.env.ARC_MAP_TOKEN)
39121
39140
  return;
39122
- err(`Federacja w produkcji wymaga zmiennych \u015Brodowiskowych: ${missing.join(", ")}.
39123
- ` + ` Bez nich sekret i token s\u0105 generowane w kontenerze i GIN\u0104 przy pierwszym
39124
- ` + ` recreate \u2014 hosty musia\u0142yby przeinstalowa\u0107 aplikacj\u0119, bez \u017Cadnego b\u0142\u0119du.
39125
- ` + ` 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).`);
39126
39145
  process.exit(1);
39127
39146
  }
39128
39147
  function resolveAppSecret(rootDir) {
@@ -39233,6 +39252,36 @@ async function platformDev(opts = {}) {
39233
39252
  });
39234
39253
  }
39235
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
+
39236
39285
  // src/commands/platform-start.ts
39237
39286
  async function platformStart() {
39238
39287
  const ws = resolveWorkspace();
@@ -39252,6 +39301,7 @@ platform3.command("dev").description("Start platform in dev mode (Bun server + V
39252
39301
  }));
39253
39302
  platform3.command("build").description("Build platform for production").option("--no-cache", "Force full rebuild").action((opts) => platformBuild({ noCache: opts.cache === false }));
39254
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);
39255
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));
39256
39306
  program2.parse(process.argv);
39257
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.0",
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.0",
16
- "@arcote.tech/arc-ds": "^0.8.0",
17
- "@arcote.tech/arc-react": "^0.8.0",
18
- "@arcote.tech/arc-host": "^0.8.0",
19
- "@arcote.tech/arc-adapter-db-sqlite": "^0.8.0",
20
- "@arcote.tech/arc-adapter-db-postgres": "^0.8.0",
21
- "@arcote.tech/arc-otel": "^0.8.0",
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.0",
35
- "@arcote.tech/arc-map": "^0.8.0",
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",
@@ -119,6 +119,14 @@ export async function platformDeploy(
119
119
  randomBytes(16).toString("base64url"),
120
120
  );
121
121
  }
122
+ // Przeładowanie jest KONIECZNE: `loadDeployConfig` wczytał sidecar i scalił
123
+ // go do `cfg.envs[].envVars` ZANIM powyższe dopisało do pliku, a compose
124
+ // renderuje `environment` właśnie z `envVars`. Bez tego kontener startuje
125
+ // bez sekretów i pada na fail-fascie — przy czym DRUGI deploy by przeszedł
126
+ // (sidecar ma już klucze), co daje błąd zależny od historii, najgorszy do
127
+ // zdiagnozowania. Inaczej niż hasło Postgresa, które trafia do compose
128
+ // przez `${ARC_PG_PASSWORD_*}` z `/opt/arc/.env`, nie przez `envVars`.
129
+ cfg = loadDeployConfig(ws.rootDir);
122
130
  }
123
131
 
124
132
  // Observability stack — one Grafana password per deployment (stack-wide,
@@ -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(
@@ -10,9 +10,6 @@ import {
10
10
  import { timingSafeEqual } from "crypto";
11
11
  import { existsSync, mkdirSync } from "fs";
12
12
  import { join } from "path";
13
- // Subpath `/types` jest bezzależnościowy — import z korzenia arc-map wciągnąłby
14
- // ts-morph do każdego startu platformy (startup.ts importuje mapę leniwie).
15
- import { MAP_SCHEMA_VERSION } from "@arcote.tech/arc-map/types";
16
13
  import { readTranslationsConfig } from "../i18n";
17
14
  import { err, ok, type BuildManifest, type WorkspaceInfo } from "./shared";
18
15
  import type { BuildManifestGroup, ModuleAccess } from "@arcote.tech/platform";
@@ -532,6 +529,19 @@ function parseCorsOrigins(raw: string | undefined): string[] | undefined {
532
529
  return list.length > 0 ? list : undefined;
533
530
  }
534
531
 
532
+ /**
533
+ * Wersja kontraktu discovery — MUSI odpowiadać `MAP_SCHEMA_VERSION`
534
+ * z `@arcote.tech/arc-map/src/types.ts`.
535
+ *
536
+ * Świadomie ZDUPLIKOWANA, nie zaimportowana: `arc-map` to narzędzie DEV, którego
537
+ * w obrazie produkcyjnym NIE MA (`bun install --production` pomija devDeps, a
538
+ * CLI trzyma go jako `--external`). Import — nawet z bezzależnościowego subpatha
539
+ * — zostawał w `host.js` i wywalał kontener w pętli restartów:
540
+ * `Cannot find module '@arcote.tech/arc-map/types'`. Bezzależnościowość PLIKU
541
+ * nie pomaga, gdy brakuje całego PAKIETU.
542
+ */
543
+ const DISCOVERY_SCHEMA_VERSION = 1;
544
+
535
545
  /** Porównanie sekretów w czasie stałym (bez wycieku długości prefiksu). */
536
546
  function safeEqual(a: string, b: string): boolean {
537
547
  const ab = Buffer.from(a);
@@ -563,14 +573,12 @@ function handleDiscovery(
563
573
  if (!federation) {
564
574
  return new Response("Not Found", { status: 404, headers: ctx.corsHeaders });
565
575
  }
566
- const presented = req.headers.get("X-Arc-Pairing-Token");
567
- if (!presented || !safeEqual(presented, federation.registrationToken)) {
568
- return new Response("Unauthorized", {
569
- status: 401,
570
- headers: ctx.corsHeaders,
571
- });
572
- }
573
-
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.
574
582
  const fed = manifest.federation;
575
583
  // Adres własny: za proxy `req.url` jest wewnętrzny, więc pozwalamy nadpisać
576
584
  // przez env. Konsument i tak zna nas z adresu, którego użył — to pole jest
@@ -581,7 +589,7 @@ function handleDiscovery(
581
589
  return Response.json(
582
590
  {
583
591
  meta: {
584
- schemaVersion: MAP_SCHEMA_VERSION,
592
+ schemaVersion: DISCOVERY_SCHEMA_VERSION,
585
593
  name: ws.appName,
586
594
  version: (ws.rootPkg?.version as string | undefined) ?? undefined,
587
595
  },
@@ -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
  }