@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 +94 -44
- package/package.json +10 -10
- package/src/commands/platform-deploy.ts +8 -0
- package/src/commands/platform-pairing-token.ts +53 -0
- package/src/index.ts +8 -0
- package/src/platform/server.ts +20 -12
- package/src/platform/startup.ts +15 -19
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
10253
|
-
const scope = key.slice(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
17888
|
-
const scope = key.slice(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
39123
|
-
` + ` Bez
|
|
39124
|
-
` + ` recreate \u2014
|
|
39125
|
-
` + ` 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).`);
|
|
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.
|
|
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",
|
|
@@ -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(
|
package/src/platform/server.ts
CHANGED
|
@@ -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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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:
|
|
592
|
+
schemaVersion: DISCOVERY_SCHEMA_VERSION,
|
|
585
593
|
name: ws.appName,
|
|
586
594
|
version: (ws.rootPkg?.version as string | undefined) ?? undefined,
|
|
587
595
|
},
|
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
|
}
|