@attraccess/plugin-wago 0.1.0-nightly.941.1 → 0.1.0-nightly.943.1
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
|
@@ -1414,7 +1414,7 @@ var WagoService = class {
|
|
|
1414
1414
|
/** Server-side commissioning revokes the enrollment it created without exposing credentials to a browser. */
|
|
1415
1415
|
async revokeEnrollmentById(id) {
|
|
1416
1416
|
const enrollment = await this.enrollments.findOneBy({ id });
|
|
1417
|
-
if (enrollment &&
|
|
1417
|
+
if (enrollment && !enrollment.consumedAt) await this.revokeEnrollment(enrollment);
|
|
1418
1418
|
}
|
|
1419
1419
|
async deleteEnrollmentById(id) {
|
|
1420
1420
|
await this.enrollments.delete(id);
|
|
@@ -1486,6 +1486,34 @@ var WagoService = class {
|
|
|
1486
1486
|
}
|
|
1487
1487
|
});
|
|
1488
1488
|
}
|
|
1489
|
+
/** Reverses the durable claim marker left behind when delivery is interrupted before completion is recorded. */
|
|
1490
|
+
async rollbackInterruptedClaim(hardwareId, mqttServerId, enrollmentId) {
|
|
1491
|
+
const controller = await this.controllers.findOneBy({ hardwareId });
|
|
1492
|
+
if (!controller || controller.trustState !== "claimed" || controller.mqttServerId !== mqttServerId || controller.enrollmentId !== enrollmentId)
|
|
1493
|
+
return;
|
|
1494
|
+
await this.withClaimLock(
|
|
1495
|
+
controller.id,
|
|
1496
|
+
() => this.withClaimConfigurationLock(async () => {
|
|
1497
|
+
const current = await this.controllers.findOneBy({ id: controller.id });
|
|
1498
|
+
if (!current || current.trustState !== "claimed" || current.mqttServerId !== mqttServerId || current.enrollmentId !== enrollmentId)
|
|
1499
|
+
return;
|
|
1500
|
+
const identity = `wago-controller-${current.hardwareId}`;
|
|
1501
|
+
const manual = await this.context.getMqttCredentialProvisioning().revoke({
|
|
1502
|
+
mqttServerId,
|
|
1503
|
+
identity,
|
|
1504
|
+
username: identity,
|
|
1505
|
+
vhost: "/"
|
|
1506
|
+
});
|
|
1507
|
+
if (manual)
|
|
1508
|
+
throw new import_common.ConflictException(`Manual credential revocation is required: ${manual.instructions.join(" ")}`);
|
|
1509
|
+
current.trustState = "untrusted";
|
|
1510
|
+
current.name = null;
|
|
1511
|
+
current.enrollmentId = null;
|
|
1512
|
+
current.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1513
|
+
await this.controllers.save(current);
|
|
1514
|
+
})
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1489
1517
|
async prepareClaim(id, name, verifier, mqttServerId) {
|
|
1490
1518
|
const controller = await this.controllers.findOneBy({ id });
|
|
1491
1519
|
if (!controller) throw new import_common.NotFoundException(`WAGO controller ${id} not found`);
|
|
@@ -2158,7 +2186,7 @@ var configuredRuntimeBundleSignature = process.env.WAGO_CC100_RUNTIME_BUNDLE_SIG
|
|
|
2158
2186
|
var configuredRuntimeSigningPublicKey = process.env.WAGO_CC100_RUNTIME_SIGNING_PUBLIC_KEY_PATH?.trim() ?? "";
|
|
2159
2187
|
var SIGNING_NAMESPACE = "attraccess-wago-runtime";
|
|
2160
2188
|
var SIGNING_IDENTITY = "attraccess-wago-runtime";
|
|
2161
|
-
var
|
|
2189
|
+
var VERIFIER_PREFIX = "encrypted:v1:";
|
|
2162
2190
|
var WagoCommissioningService = class {
|
|
2163
2191
|
constructor(context, wago) {
|
|
2164
2192
|
__publicField(this, "context", context);
|
|
@@ -2166,15 +2194,16 @@ var WagoCommissioningService = class {
|
|
|
2166
2194
|
__publicField(this, "sessions");
|
|
2167
2195
|
__publicField(this, "deliveryLocks", /* @__PURE__ */ new Map());
|
|
2168
2196
|
}
|
|
2169
|
-
onApplicationBootstrap() {
|
|
2197
|
+
async onApplicationBootstrap() {
|
|
2170
2198
|
this.sessions = this.context.getRepository(WagoCommissioningSession);
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2199
|
+
try {
|
|
2200
|
+
await this.recoverSessions();
|
|
2201
|
+
await this.reconcileCompletedSessions();
|
|
2202
|
+
this.wago.registerCommissioningDiscoveryHandler((controller) => this.claimDiscovered(controller));
|
|
2203
|
+
for (const controller of await this.wago.list()) await this.claimDiscovered(controller);
|
|
2204
|
+
} catch {
|
|
2205
|
+
this.context.logger?.warn("WAGO commissioning recovery failed; automatic discovery claim is disabled.");
|
|
2206
|
+
}
|
|
2178
2207
|
}
|
|
2179
2208
|
support() {
|
|
2180
2209
|
return {
|
|
@@ -2192,9 +2221,19 @@ var WagoCommissioningService = class {
|
|
|
2192
2221
|
throw new import_common2.ConflictException(
|
|
2193
2222
|
"no CC100 firmware baseline is configured; commissioning is disabled until an exact supported baseline is set"
|
|
2194
2223
|
);
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2224
|
+
let brokerAvailable;
|
|
2225
|
+
try {
|
|
2226
|
+
brokerAvailable = Boolean(await this.context.getMqttServerConfig(input.mqttServerId));
|
|
2227
|
+
} catch {
|
|
2228
|
+
throw new import_common2.ConflictException("Commissioning MQTT configuration could not be resolved.");
|
|
2229
|
+
}
|
|
2230
|
+
if (!brokerAvailable) throw new import_common2.NotFoundException("MQTT server not found");
|
|
2231
|
+
let hostKeyFingerprint;
|
|
2232
|
+
try {
|
|
2233
|
+
hostKeyFingerprint = await scanHostKey(input.targetHost);
|
|
2234
|
+
} catch {
|
|
2235
|
+
throw new import_common2.ConflictException("Commissioning SSH host-key scan failed.");
|
|
2236
|
+
}
|
|
2198
2237
|
const hardwareId = `cc100-${(0, import_node_crypto4.createHash)("sha256").update(hostKeyFingerprint).digest("hex").slice(0, 16)}`;
|
|
2199
2238
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2200
2239
|
const session = await this.sessions.save(
|
|
@@ -2209,7 +2248,7 @@ var WagoCommissioningService = class {
|
|
|
2209
2248
|
enrollmentExpiresAt: null,
|
|
2210
2249
|
enrollmentId: null,
|
|
2211
2250
|
// This verifier authenticates the first runtime announcement and is never exposed to an operator.
|
|
2212
|
-
pairingCode: (0, import_node_crypto4.randomBytes)(32).toString("base64url"),
|
|
2251
|
+
pairingCode: this.encryptVerifier((0, import_node_crypto4.randomBytes)(32).toString("base64url")),
|
|
2213
2252
|
codesysState: null,
|
|
2214
2253
|
progressPercent: 0,
|
|
2215
2254
|
progressStep: "Confirm controller identity",
|
|
@@ -2255,11 +2294,23 @@ var WagoCommissioningService = class {
|
|
|
2255
2294
|
if (!session) throw new import_common2.NotFoundException("commissioning session not found");
|
|
2256
2295
|
if (!["awaiting_delivery", "delivering", "awaiting_codesys_confirmation", "delivery_failed"].includes(session.state))
|
|
2257
2296
|
throw new import_common2.ConflictException("commissioning session cannot be delivered in its current state");
|
|
2297
|
+
if (input?.confirmInstall !== true)
|
|
2298
|
+
throw new import_common2.ConflictException("explicit installation confirmation is required for every delivery attempt");
|
|
2299
|
+
const credential = input.temporarySsh;
|
|
2300
|
+
if (!credential || typeof credential.username !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$/.test(credential.username) || typeof credential.password !== "string" || !credential.password.trim() || /[\r\n\0]/.test(credential.password))
|
|
2301
|
+
throw new import_common2.ConflictException("explicit valid SSH username and password are required for every delivery attempt");
|
|
2258
2302
|
if (!isRuntimeArtifactConfigured())
|
|
2259
2303
|
throw new import_common2.ConflictException("no immutable, signed CC100 runtime bundle is configured for commissioning");
|
|
2260
|
-
|
|
2304
|
+
let pairingCode;
|
|
2305
|
+
try {
|
|
2306
|
+
pairingCode = this.decryptVerifier(session);
|
|
2307
|
+
} catch {
|
|
2308
|
+
await this.invalidateVerifier(session);
|
|
2309
|
+
return this.toResponse(session);
|
|
2310
|
+
}
|
|
2261
2311
|
let enrollmentExpiresAt = null;
|
|
2262
2312
|
try {
|
|
2313
|
+
await this.revokeSessionEnrollment(session);
|
|
2263
2314
|
session.state = "delivering";
|
|
2264
2315
|
session.failureReason = null;
|
|
2265
2316
|
await this.updateProgress(
|
|
@@ -2330,12 +2381,12 @@ var WagoCommissioningService = class {
|
|
|
2330
2381
|
"Provisioning a restricted MQTT credential for the initial connection."
|
|
2331
2382
|
);
|
|
2332
2383
|
const enrollment = await this.wago.createEnrollment(session.hardwareId, session.mqttServerId);
|
|
2384
|
+
session.enrollmentId = enrollment.id;
|
|
2385
|
+
await this.save(session, "enrollment_created");
|
|
2333
2386
|
if (!enrollment.password)
|
|
2334
2387
|
throw new import_common2.ConflictException(
|
|
2335
2388
|
"automatic restricted MQTT credential provisioning is required for secure commissioning"
|
|
2336
2389
|
);
|
|
2337
|
-
if (!session.pairingCode) throw new import_common2.ConflictException("commissioning session has no automatic claim verifier");
|
|
2338
|
-
session.enrollmentId = enrollment.id;
|
|
2339
2390
|
enrollmentExpiresAt = enrollment.expiresAt;
|
|
2340
2391
|
await this.updateProgress(
|
|
2341
2392
|
session,
|
|
@@ -2353,7 +2404,7 @@ var WagoCommissioningService = class {
|
|
|
2353
2404
|
`WAGO_MQTT_USERNAME=${enrollment.username}`,
|
|
2354
2405
|
`WAGO_MQTT_PASSWORD=${enrollment.password}`,
|
|
2355
2406
|
`WAGO_ENROLLMENT_SECRET=${enrollment.claimSecret}`,
|
|
2356
|
-
`WAGO_PAIRING_CODE=${
|
|
2407
|
+
`WAGO_PAIRING_CODE=${pairingCode}`
|
|
2357
2408
|
].join("\n")
|
|
2358
2409
|
);
|
|
2359
2410
|
await this.updateProgress(
|
|
@@ -2378,18 +2429,16 @@ var WagoCommissioningService = class {
|
|
|
2378
2429
|
session.progressStep = "Waiting for controller connection";
|
|
2379
2430
|
session.progressDetail = "Runtime delivered. Waiting for the controller to connect and complete its automatic claim.";
|
|
2380
2431
|
return this.toResponse(await this.save(session, "bootstrap_delivered"));
|
|
2381
|
-
} catch
|
|
2432
|
+
} catch {
|
|
2382
2433
|
if (session.enrollmentId !== null) {
|
|
2383
2434
|
try {
|
|
2384
|
-
await this.
|
|
2385
|
-
} catch
|
|
2435
|
+
await this.revokeSessionEnrollment(session);
|
|
2436
|
+
} catch {
|
|
2386
2437
|
session.state = "delivery_failed";
|
|
2387
2438
|
session.enrollmentExpiresAt = null;
|
|
2388
2439
|
session.progressStep = "Delivery failed";
|
|
2389
2440
|
session.progressDetail = "Credential revocation requires attention before delivery can be retried.";
|
|
2390
|
-
session.failureReason =
|
|
2391
|
-
revocationError instanceof Error ? revocationError.message : String(revocationError)
|
|
2392
|
-
)}`;
|
|
2441
|
+
session.failureReason = "Secure delivery failed; bootstrap credential revocation requires attention.";
|
|
2393
2442
|
return this.toResponse(await this.save(session, "enrollment_revocation_failed"));
|
|
2394
2443
|
}
|
|
2395
2444
|
}
|
|
@@ -2397,7 +2446,7 @@ var WagoCommissioningService = class {
|
|
|
2397
2446
|
session.enrollmentExpiresAt = null;
|
|
2398
2447
|
session.progressStep = "Delivery failed";
|
|
2399
2448
|
session.progressDetail = "Review the error and retry automatic delivery when the controller is reachable.";
|
|
2400
|
-
session.failureReason =
|
|
2449
|
+
session.failureReason = "Secure delivery failed. Check controller access and the configured runtime bundle.";
|
|
2401
2450
|
return this.toResponse(await this.save(session, "delivery_failed"));
|
|
2402
2451
|
}
|
|
2403
2452
|
}
|
|
@@ -2405,8 +2454,9 @@ var WagoCommissioningService = class {
|
|
|
2405
2454
|
return this.withDeliveryLock(id, async () => {
|
|
2406
2455
|
const session = await this.sessions.findOneBy({ id });
|
|
2407
2456
|
if (!session) throw new import_common2.NotFoundException("commissioning session not found");
|
|
2408
|
-
|
|
2457
|
+
await this.revokeSessionEnrollment(session);
|
|
2409
2458
|
session.state = "revoked";
|
|
2459
|
+
session.pairingCode = null;
|
|
2410
2460
|
session.failureReason = null;
|
|
2411
2461
|
return this.toResponse(await this.save(session, "revoked"));
|
|
2412
2462
|
});
|
|
@@ -2416,8 +2466,12 @@ var WagoCommissioningService = class {
|
|
|
2416
2466
|
const session = await this.sessions.findOneBy({ id });
|
|
2417
2467
|
if (!session) throw new import_common2.NotFoundException("commissioning session not found");
|
|
2418
2468
|
if (session.enrollmentId !== null) {
|
|
2419
|
-
|
|
2420
|
-
|
|
2469
|
+
try {
|
|
2470
|
+
await this.wago.revokeEnrollmentById(session.enrollmentId);
|
|
2471
|
+
await this.wago.deleteEnrollmentById(session.enrollmentId);
|
|
2472
|
+
} catch {
|
|
2473
|
+
throw new import_common2.ConflictException("Commissioning enrollment removal failed.");
|
|
2474
|
+
}
|
|
2421
2475
|
}
|
|
2422
2476
|
await this.sessions.delete(id);
|
|
2423
2477
|
});
|
|
@@ -2439,6 +2493,13 @@ var WagoCommissioningService = class {
|
|
|
2439
2493
|
const current = await this.sessions.findOneBy({ id: session.id });
|
|
2440
2494
|
if (!current || current.state !== "awaiting_discovery" || current.hardwareId !== controller.hardwareId || current.mqttServerId !== controller.mqttServerId || current.enrollmentId !== controller.enrollmentId || !current.controllerName || !current.pairingCode)
|
|
2441
2495
|
return;
|
|
2496
|
+
let pairingCode;
|
|
2497
|
+
try {
|
|
2498
|
+
pairingCode = this.decryptVerifier(current);
|
|
2499
|
+
} catch {
|
|
2500
|
+
await this.invalidateVerifier(current);
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2442
2503
|
current.state = "awaiting_claim";
|
|
2443
2504
|
current.failureReason = null;
|
|
2444
2505
|
current.progressPercent = 100;
|
|
@@ -2446,21 +2507,21 @@ var WagoCommissioningService = class {
|
|
|
2446
2507
|
current.progressDetail = "Applying permanent credentials and the reserved controller name.";
|
|
2447
2508
|
await this.save(current, "automatic_claim_started");
|
|
2448
2509
|
try {
|
|
2449
|
-
await this.wago.claim(controller.id, current.controllerName,
|
|
2510
|
+
await this.wago.claim(controller.id, current.controllerName, pairingCode, current.mqttServerId);
|
|
2450
2511
|
current.state = "completed";
|
|
2451
2512
|
current.pairingCode = null;
|
|
2452
2513
|
current.failureReason = null;
|
|
2453
2514
|
current.progressStep = "Commissioning complete";
|
|
2454
2515
|
current.progressDetail = "The controller is claimed and ready to configure.";
|
|
2455
2516
|
await this.save(current, "automatic_claim_completed");
|
|
2456
|
-
} catch
|
|
2517
|
+
} catch {
|
|
2457
2518
|
current.state = "awaiting_discovery";
|
|
2458
|
-
current.failureReason =
|
|
2519
|
+
current.failureReason = "Automatic claim failed.";
|
|
2459
2520
|
await this.save(current, "automatic_claim_failed");
|
|
2460
2521
|
return;
|
|
2461
2522
|
}
|
|
2462
2523
|
await this.retireSupersededSessions(current.hardwareId, current.id).catch(
|
|
2463
|
-
(
|
|
2524
|
+
() => this.context.logger?.warn("Could not retire superseded WAGO commissioning sessions.")
|
|
2464
2525
|
);
|
|
2465
2526
|
});
|
|
2466
2527
|
}
|
|
@@ -2596,51 +2657,126 @@ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
|
|
|
2596
2657
|
progressStep: session.progressStep,
|
|
2597
2658
|
progressDetail: session.progressDetail,
|
|
2598
2659
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2599
|
-
}).catch((
|
|
2600
|
-
}
|
|
2601
|
-
async resumePendingDeliveries() {
|
|
2602
|
-
const sessions = await this.sessions.find({ where: [{ state: "awaiting_delivery" }, { state: "delivering" }] });
|
|
2603
|
-
for (const session of sessions) void this.deliverInBackground(session.id);
|
|
2660
|
+
}).catch(() => this.context.logger?.warn("Could not update WAGO runtime transfer progress."));
|
|
2604
2661
|
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2662
|
+
encryptVerifier(plaintext) {
|
|
2663
|
+
try {
|
|
2664
|
+
return VERIFIER_PREFIX + this.context.secrets.encrypt(plaintext);
|
|
2665
|
+
} catch {
|
|
2666
|
+
throw new import_common2.ConflictException("Commissioning verifier encryption failed.");
|
|
2667
|
+
}
|
|
2608
2668
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
if (current.enrollmentId !== null) await this.wago.revokeEnrollmentById(current.enrollmentId);
|
|
2619
|
-
current.state = "revoked";
|
|
2620
|
-
current.failureReason = null;
|
|
2621
|
-
current.progressStep = "Superseded by completed commissioning";
|
|
2622
|
-
current.progressDetail = "A newer commissioning session claimed this controller.";
|
|
2623
|
-
await this.save(current, "superseded_by_completed_session");
|
|
2624
|
-
})
|
|
2625
|
-
)
|
|
2626
|
-
);
|
|
2669
|
+
decryptVerifier(session) {
|
|
2670
|
+
try {
|
|
2671
|
+
if (!session.pairingCode?.startsWith(VERIFIER_PREFIX)) throw new Error();
|
|
2672
|
+
const plaintext = this.context.secrets.decrypt(session.pairingCode.slice(VERIFIER_PREFIX.length));
|
|
2673
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(plaintext)) throw new Error();
|
|
2674
|
+
return plaintext;
|
|
2675
|
+
} catch {
|
|
2676
|
+
throw new import_common2.ConflictException("Commissioning verifier is unavailable; create a new session.");
|
|
2677
|
+
}
|
|
2627
2678
|
}
|
|
2628
|
-
async
|
|
2679
|
+
async revokeSessionEnrollment(session) {
|
|
2680
|
+
if (session.enrollmentId == null) return;
|
|
2629
2681
|
try {
|
|
2630
|
-
await this.
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2682
|
+
await this.wago.revokeEnrollmentById(session.enrollmentId);
|
|
2683
|
+
await this.wago.deleteEnrollmentById(session.enrollmentId);
|
|
2684
|
+
} catch {
|
|
2685
|
+
throw new import_common2.ConflictException("Commissioning credential revocation requires attention.");
|
|
2686
|
+
}
|
|
2687
|
+
session.enrollmentId = null;
|
|
2688
|
+
session.enrollmentExpiresAt = null;
|
|
2689
|
+
await this.save(session, "enrollment_revoked");
|
|
2690
|
+
}
|
|
2691
|
+
async invalidateVerifier(session) {
|
|
2692
|
+
session.state = "revoked";
|
|
2693
|
+
session.pairingCode = null;
|
|
2694
|
+
session.progressStep = "Commissioning session revoked";
|
|
2695
|
+
session.progressDetail = "The saved verifier is unavailable. Create a new commissioning session.";
|
|
2696
|
+
session.failureReason = "Commissioning verifier is unavailable; credential revocation requires attention.";
|
|
2697
|
+
await this.save(session, "verifier_invalidated");
|
|
2698
|
+
await this.revokeSessionEnrollment(session);
|
|
2699
|
+
session.failureReason = null;
|
|
2700
|
+
await this.save(session, "invalid_verifier_revoked");
|
|
2701
|
+
}
|
|
2702
|
+
async recoverSessions() {
|
|
2703
|
+
let recoveryFailed = false;
|
|
2704
|
+
for (let skip = 0; ; skip += 100) {
|
|
2705
|
+
const page = await this.sessions.find({ order: { id: "ASC" }, take: 100, skip });
|
|
2706
|
+
for (const session of page) {
|
|
2707
|
+
try {
|
|
2708
|
+
if (session.state === "completed" && !session.pairingCode) continue;
|
|
2709
|
+
if (session.state === "revoked" && !session.pairingCode) {
|
|
2710
|
+
await this.revokeSessionEnrollment(session);
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
try {
|
|
2714
|
+
this.decryptVerifier(session);
|
|
2715
|
+
} catch {
|
|
2716
|
+
await this.invalidateVerifier(session);
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
if (session.state === "delivering" || session.state === "awaiting_claim") {
|
|
2720
|
+
if (session.state === "awaiting_claim")
|
|
2721
|
+
await this.wago.rollbackInterruptedClaim(session.hardwareId, session.mqttServerId, session.enrollmentId);
|
|
2722
|
+
session.state = "delivery_failed";
|
|
2723
|
+
session.progressStep = "Delivery interrupted";
|
|
2724
|
+
session.progressDetail = "Retry with explicit SSH credentials and installation confirmation.";
|
|
2725
|
+
session.failureReason = "Commissioning was interrupted.";
|
|
2726
|
+
await this.save(session, "delivery_interrupted");
|
|
2727
|
+
await this.revokeSessionEnrollment(session);
|
|
2728
|
+
}
|
|
2729
|
+
} catch {
|
|
2730
|
+
recoveryFailed = true;
|
|
2731
|
+
}
|
|
2642
2732
|
}
|
|
2733
|
+
if (page.length < 100) break;
|
|
2643
2734
|
}
|
|
2735
|
+
if (recoveryFailed) throw new import_common2.ConflictException("Commissioning recovery requires credential revocation.");
|
|
2736
|
+
}
|
|
2737
|
+
async reconcileCompletedSessions() {
|
|
2738
|
+
let reconciliationFailed = false;
|
|
2739
|
+
for (let skip = 0; ; skip += 100) {
|
|
2740
|
+
const page = await this.sessions.find({ where: { state: "completed" }, order: { id: "ASC" }, take: 100, skip });
|
|
2741
|
+
for (const session of page) {
|
|
2742
|
+
if (session.state !== "completed") continue;
|
|
2743
|
+
try {
|
|
2744
|
+
await this.retireSupersededSessions(session.hardwareId, session.id);
|
|
2745
|
+
} catch {
|
|
2746
|
+
reconciliationFailed = true;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
if (page.length < 100) break;
|
|
2750
|
+
}
|
|
2751
|
+
if (reconciliationFailed)
|
|
2752
|
+
throw new import_common2.ConflictException("Superseded commissioning credential cleanup requires attention.");
|
|
2753
|
+
}
|
|
2754
|
+
async retireSupersededSessions(hardwareId, completedSessionId) {
|
|
2755
|
+
let retirementFailed = false;
|
|
2756
|
+
for (let skip = 0; ; skip += 100) {
|
|
2757
|
+
const sessions = await this.sessions.find({ where: { hardwareId }, order: { id: "ASC" }, take: 100, skip });
|
|
2758
|
+
const results = await Promise.allSettled(
|
|
2759
|
+
sessions.filter(
|
|
2760
|
+
(session) => session.id !== completedSessionId && session.state !== "completed" && session.state !== "revoked"
|
|
2761
|
+
).map(
|
|
2762
|
+
(session) => this.withDeliveryLock(session.id, async () => {
|
|
2763
|
+
const current = await this.sessions.findOneBy({ id: session.id });
|
|
2764
|
+
if (!current || current.state === "completed" || current.state === "revoked") return;
|
|
2765
|
+
await this.revokeSessionEnrollment(current);
|
|
2766
|
+
current.state = "revoked";
|
|
2767
|
+
current.pairingCode = null;
|
|
2768
|
+
current.failureReason = null;
|
|
2769
|
+
current.progressStep = "Superseded by completed commissioning";
|
|
2770
|
+
current.progressDetail = "A newer commissioning session claimed this controller.";
|
|
2771
|
+
await this.save(current, "superseded_by_completed_session");
|
|
2772
|
+
})
|
|
2773
|
+
)
|
|
2774
|
+
);
|
|
2775
|
+
if (results.some((result) => result.status === "rejected")) retirementFailed = true;
|
|
2776
|
+
if (sessions.length < 100) break;
|
|
2777
|
+
}
|
|
2778
|
+
if (retirementFailed)
|
|
2779
|
+
throw new import_common2.ConflictException("Superseded commissioning credential cleanup requires attention.");
|
|
2644
2780
|
}
|
|
2645
2781
|
toResponse(session) {
|
|
2646
2782
|
const { pairingCode: _pairingCode, ...response } = session;
|
|
@@ -2707,16 +2843,18 @@ function runProcess(command, args, input, environment) {
|
|
|
2707
2843
|
return new Promise((resolve, reject) => {
|
|
2708
2844
|
const child = (0, import_node_child_process.spawn)(command, args, { env: { ...process.env, ...environment }, stdio: ["pipe", "pipe", "pipe"] });
|
|
2709
2845
|
let stdout = "";
|
|
2710
|
-
let stderr = "";
|
|
2711
2846
|
const timer = setTimeout(() => child.kill(), SSH_TIMEOUT_MS);
|
|
2712
2847
|
child.stdin.on("error", () => void 0);
|
|
2713
2848
|
child.stdout.on("data", (chunk) => stdout += chunk);
|
|
2714
|
-
child.stderr.on("data", (
|
|
2715
|
-
child.on("error",
|
|
2849
|
+
child.stderr.on("data", () => void 0);
|
|
2850
|
+
child.on("error", () => {
|
|
2851
|
+
clearTimeout(timer);
|
|
2852
|
+
reject(new Error("Commissioning subprocess failed."));
|
|
2853
|
+
});
|
|
2716
2854
|
child.on("close", (code) => {
|
|
2717
2855
|
clearTimeout(timer);
|
|
2718
2856
|
if (code === 0) resolve(stdout);
|
|
2719
|
-
else reject(new Error(
|
|
2857
|
+
else reject(new Error("Commissioning subprocess failed."));
|
|
2720
2858
|
});
|
|
2721
2859
|
child.stdin.end(input);
|
|
2722
2860
|
});
|
|
@@ -2726,7 +2864,6 @@ async function uploadFile(source, args, environment, onProgress) {
|
|
|
2726
2864
|
return new Promise((resolve, reject) => {
|
|
2727
2865
|
const child = (0, import_node_child_process.spawn)("ssh", args, { env: { ...process.env, ...environment }, stdio: ["pipe", "ignore", "pipe"] });
|
|
2728
2866
|
const stream = (0, import_node_fs.createReadStream)(source);
|
|
2729
|
-
let stderr = "";
|
|
2730
2867
|
let transferred = 0;
|
|
2731
2868
|
let lastPercent = -1;
|
|
2732
2869
|
let settled = false;
|
|
@@ -2736,13 +2873,13 @@ async function uploadFile(source, args, environment, onProgress) {
|
|
|
2736
2873
|
settled = true;
|
|
2737
2874
|
clearTimeout(timer);
|
|
2738
2875
|
stream.destroy();
|
|
2739
|
-
if (error) reject(
|
|
2876
|
+
if (error) reject(new Error("Commissioning upload failed."));
|
|
2740
2877
|
else resolve();
|
|
2741
2878
|
};
|
|
2742
2879
|
child.stdin.on("error", () => void 0);
|
|
2743
|
-
child.stderr.on("data", (
|
|
2880
|
+
child.stderr.on("data", () => void 0);
|
|
2744
2881
|
child.on("error", finish);
|
|
2745
|
-
child.on("close", (code) => finish(code === 0 ? void 0 : new Error(
|
|
2882
|
+
child.on("close", (code) => finish(code === 0 ? void 0 : new Error("Commissioning upload failed.")));
|
|
2746
2883
|
stream.on("error", (error) => {
|
|
2747
2884
|
child.kill();
|
|
2748
2885
|
finish(error);
|
|
@@ -2766,9 +2903,6 @@ function isPrivateAddress(host) {
|
|
|
2766
2903
|
if (octets.some((octet) => octet > 255)) return false;
|
|
2767
2904
|
return octets[0] === 10 || octets[0] === 192 && octets[1] === 168 || octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31;
|
|
2768
2905
|
}
|
|
2769
|
-
function redact(value) {
|
|
2770
|
-
return value.replace(/(?:password|secret|token)=?[^\s]*/gi, "[redacted]");
|
|
2771
|
-
}
|
|
2772
2906
|
function isImmutableImage(image) {
|
|
2773
2907
|
return /@sha256:[a-f0-9]{64}$/i.test(image);
|
|
2774
2908
|
}
|
|
@@ -2882,8 +3016,13 @@ var WagoControllerApi = class {
|
|
|
2882
3016
|
return this.commissioning.confirmHostKey(id, body.hostKeyFingerprint);
|
|
2883
3017
|
}
|
|
2884
3018
|
deliverCommissioningSession(id, body) {
|
|
3019
|
+
if (body?.confirmInstall !== true) throw new import_common3.BadRequestException("Explicit installation consent is required");
|
|
3020
|
+
if (typeof body.temporarySsh?.username !== "string" || !body.temporarySsh.username.trim() || typeof body.temporarySsh.password !== "string" || !body.temporarySsh.password) {
|
|
3021
|
+
throw new import_common3.BadRequestException("Temporary SSH username and password are required");
|
|
3022
|
+
}
|
|
2885
3023
|
return this.commissioning.deliver(id, {
|
|
2886
|
-
|
|
3024
|
+
confirmInstall: true,
|
|
3025
|
+
temporarySsh: { username: body.temporarySsh.username, password: body.temporarySsh.password }
|
|
2887
3026
|
});
|
|
2888
3027
|
}
|
|
2889
3028
|
revokeCommissioningSession(id) {
|
package/frontend/{__federation_expose_Plugin-BdPTyEH_.js → __federation_expose_Plugin-CVjUpNsq.js}
RENAMED
|
@@ -83,7 +83,7 @@ var confirmCommissioningHostKey = (id, hostKeyFingerprint) => api.request(`/comm
|
|
|
83
83
|
body: { hostKeyFingerprint }
|
|
84
84
|
});
|
|
85
85
|
var listCommissioningSessions = (limit = 100, offset = 0) => api.request(`/commissioning/sessions?limit=${limit}&offset=${offset}`);
|
|
86
|
-
var deliverCommissioningSession = (id, input
|
|
86
|
+
var deliverCommissioningSession = (id, input) => api.request(`/commissioning/sessions/${id}/deliver`, {
|
|
87
87
|
method: "POST",
|
|
88
88
|
body: input
|
|
89
89
|
});
|
|
@@ -183,7 +183,20 @@ function useConfirmCommissioningHostKeyMutation() {
|
|
|
183
183
|
function useDeliverCommissioningSessionMutation() {
|
|
184
184
|
const queryClient = useQueryClient();
|
|
185
185
|
return useMutation({
|
|
186
|
-
|
|
186
|
+
gcTime: 0,
|
|
187
|
+
retry: false,
|
|
188
|
+
networkMode: "always",
|
|
189
|
+
mutationFn: (variables) => {
|
|
190
|
+
const temporarySsh = { ...variables.temporarySsh };
|
|
191
|
+
const confirmInstall = variables.confirmInstall;
|
|
192
|
+
variables.temporarySsh.password = "";
|
|
193
|
+
variables.confirmInstall = false;
|
|
194
|
+
if (confirmInstall !== true) throw new Error("Explicit installation consent is required");
|
|
195
|
+
return deliverCommissioningSession(variables.id, {
|
|
196
|
+
confirmInstall,
|
|
197
|
+
temporarySsh
|
|
198
|
+
});
|
|
199
|
+
},
|
|
187
200
|
onSuccess: async () => {
|
|
188
201
|
await queryClient.invalidateQueries({ queryKey: queryKeys.controllers });
|
|
189
202
|
await queryClient.invalidateQueries({ queryKey: queryKeys.commissioningSessions });
|
|
@@ -324,7 +337,7 @@ function getErrorMessage$1(error) {
|
|
|
324
337
|
}
|
|
325
338
|
//#endregion
|
|
326
339
|
//#region frontend/src/ConfigurationEditor.tsx
|
|
327
|
-
var { Alert: Alert$4, Button: Button$5, Card, Checkbox, Form: Form$1, Input: Input$1, Label: Label$2, Modal: Modal$1, ModalBackdrop: ModalBackdrop$1, ModalBody: ModalBody$1, ModalContainer: ModalContainer$1, ModalDialog: ModalDialog$1, ModalFooter: ModalFooter$1, ModalHeader: ModalHeader$1, ModalHeading: ModalHeading$1, TextArea, TextField: TextField$1 } = await importShared("@heroui/react");
|
|
340
|
+
var { Alert: Alert$4, Button: Button$5, Card, Checkbox: Checkbox$1, Form: Form$1, Input: Input$1, Label: Label$2, Modal: Modal$1, ModalBackdrop: ModalBackdrop$1, ModalBody: ModalBody$1, ModalContainer: ModalContainer$1, ModalDialog: ModalDialog$1, ModalFooter: ModalFooter$1, ModalHeader: ModalHeader$1, ModalHeading: ModalHeading$1, TextArea, TextField: TextField$1 } = await importShared("@heroui/react");
|
|
328
341
|
var { useEffect: useEffect$3, useRef, useState: useState$3 } = await importShared("react");
|
|
329
342
|
var emptySnapshot = {
|
|
330
343
|
version: 1,
|
|
@@ -518,7 +531,7 @@ function ConfigurationEditor({ controllerId, onOpenChange }) {
|
|
|
518
531
|
className: "wg:text-sm wg:font-medium",
|
|
519
532
|
children: "Select preset changes to copy into this draft"
|
|
520
533
|
}),
|
|
521
|
-
presetPreview.diff.map((change) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox, {
|
|
534
|
+
presetPreview.diff.map((change) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox$1, {
|
|
522
535
|
isDisabled: isApplyingPreset,
|
|
523
536
|
isSelected: selectedPaths.includes(change.path),
|
|
524
537
|
onChange: (selected) => setSelectedPaths((paths) => selected ? [...paths, change.path] : paths.filter((path) => path !== change.path)),
|
|
@@ -800,7 +813,7 @@ function StandardDrawer({ isOpen, onOpenChange, children }) {
|
|
|
800
813
|
}
|
|
801
814
|
//#endregion
|
|
802
815
|
//#region frontend/src/CommissioningModal.tsx
|
|
803
|
-
var { Alert: Alert$3, Button: Button$3, DrawerBody: DrawerBody$1, DrawerFooter: DrawerFooter$1, DrawerHeader: DrawerHeader$1, Input, Label: Label$1, ListBox: ListBox$1, Select: Select$1, Spinner: Spinner$2, TextField } = await importShared("@heroui/react");
|
|
816
|
+
var { Alert: Alert$3, Button: Button$3, Checkbox, DrawerBody: DrawerBody$1, DrawerFooter: DrawerFooter$1, DrawerHeader: DrawerHeader$1, Input, Label: Label$1, ListBox: ListBox$1, Select: Select$1, Spinner: Spinner$2, TextField } = await importShared("@heroui/react");
|
|
804
817
|
var { AlertCircleIcon, CheckCircle2Icon, CpuIcon: CpuIcon$1 } = await importShared("lucide-react");
|
|
805
818
|
var { useEffect: useEffect$2, useState: useState$2 } = await importShared("react");
|
|
806
819
|
function CommissioningModal({ isOpen, session: resumedSession, onOpenChange }) {
|
|
@@ -817,14 +830,20 @@ function CommissioningModal({ isOpen, session: resumedSession, onOpenChange }) {
|
|
|
817
830
|
const [controllerIp, setControllerIp] = useState$2("");
|
|
818
831
|
const [mqttServerId, setMqttServerId] = useState$2(null);
|
|
819
832
|
const [hostKeyFingerprint, setHostKeyFingerprint] = useState$2("");
|
|
820
|
-
const [sshUsername, setSshUsername] = useState$2("
|
|
821
|
-
const [sshPassword, setSshPassword] = useState$2("
|
|
833
|
+
const [sshUsername, setSshUsername] = useState$2("");
|
|
834
|
+
const [sshPassword, setSshPassword] = useState$2("");
|
|
835
|
+
const [confirmInstall, setConfirmInstall] = useState$2(false);
|
|
822
836
|
const [isCancelConfirmationOpen, setCancelConfirmationOpen] = useState$2(false);
|
|
823
837
|
const mutationSession = deliverSessionMutation.data ?? resumedSession ?? createdSession;
|
|
824
838
|
const session = mutationSession ? commissioningSessionsQuery.data?.find((candidate) => candidate.id === mutationSession.id) ?? mutationSession : null;
|
|
825
839
|
const selectedMqttServerId = mqttServerId === null ? null : Number(mqttServerId);
|
|
826
840
|
const isLoading = createSessionMutation.isPending || confirmHostKeyMutation.isPending || deliverSessionMutation.isPending || removeSessionMutation.isPending;
|
|
827
|
-
const loadingStatus = createSessionMutation.isPending ? ["Preparing commissioning", "Scanning
|
|
841
|
+
const loadingStatus = createSessionMutation.isPending ? ["Preparing commissioning", "Scanning the SSH key for your review. A scan alone does not authenticate the controller."] : removeSessionMutation.isPending ? ["Canceling enrollment", "Revoking access and removing the enrollment records."] : confirmHostKeyMutation.isPending ? ["Confirming controller identity", "Saving the administrator-confirmed SSH host key."] : null;
|
|
842
|
+
useEffect$2(() => {
|
|
843
|
+
setSshPassword("");
|
|
844
|
+
setConfirmInstall(false);
|
|
845
|
+
setHostKeyFingerprint("");
|
|
846
|
+
}, [isOpen, resumedSession?.id]);
|
|
828
847
|
useEffect$2(() => {
|
|
829
848
|
if (!isOpen) return;
|
|
830
849
|
setMqttServerId(resumedSession?.mqttServerId.toString() ?? settingsQuery.data?.defaultMqttServerId?.toString() ?? null);
|
|
@@ -843,10 +862,12 @@ function CommissioningModal({ isOpen, session: resumedSession, onOpenChange }) {
|
|
|
843
862
|
setControllerIp("");
|
|
844
863
|
setMqttServerId(null);
|
|
845
864
|
setHostKeyFingerprint("");
|
|
846
|
-
setSshUsername("
|
|
847
|
-
setSshPassword("
|
|
865
|
+
setSshUsername("");
|
|
866
|
+
setSshPassword("");
|
|
867
|
+
setConfirmInstall(false);
|
|
848
868
|
setCancelConfirmationOpen(false);
|
|
849
869
|
createSessionMutation.reset();
|
|
870
|
+
confirmHostKeyMutation.reset();
|
|
850
871
|
deliverSessionMutation.reset();
|
|
851
872
|
removeSessionMutation.reset();
|
|
852
873
|
onOpenChange(false);
|
|
@@ -863,14 +884,17 @@ function CommissioningModal({ isOpen, session: resumedSession, onOpenChange }) {
|
|
|
863
884
|
} });
|
|
864
885
|
}
|
|
865
886
|
function deliverSession() {
|
|
866
|
-
if (!session) return;
|
|
887
|
+
if (!session || isLoading || !confirmInstall || !sshUsername.trim() || !sshPassword || !canInstall(session)) return;
|
|
867
888
|
deliverSessionMutation.mutate({
|
|
868
889
|
id: session.id,
|
|
890
|
+
confirmInstall: true,
|
|
869
891
|
temporarySsh: {
|
|
870
|
-
username: sshUsername,
|
|
892
|
+
username: sshUsername.trim(),
|
|
871
893
|
password: sshPassword
|
|
872
894
|
}
|
|
873
895
|
});
|
|
896
|
+
setSshPassword("");
|
|
897
|
+
setConfirmInstall(false);
|
|
874
898
|
}
|
|
875
899
|
function confirmHostKey() {
|
|
876
900
|
if (!session) return;
|
|
@@ -925,7 +949,9 @@ function CommissioningModal({ isOpen, session: resumedSession, onOpenChange }) {
|
|
|
925
949
|
sshUsername,
|
|
926
950
|
sshPassword,
|
|
927
951
|
onSshUsernameChange: setSshUsername,
|
|
928
|
-
onSshPasswordChange: setSshPassword
|
|
952
|
+
onSshPasswordChange: setSshPassword,
|
|
953
|
+
confirmInstall,
|
|
954
|
+
onConfirmInstallChange: setConfirmInstall
|
|
929
955
|
}),
|
|
930
956
|
session && activeStep === 3 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProgressStep, {
|
|
931
957
|
name: title,
|
|
@@ -956,22 +982,22 @@ function CommissioningModal({ isOpen, session: resumedSession, onOpenChange }) {
|
|
|
956
982
|
isPending: isLoading,
|
|
957
983
|
isDisabled: !controllerIp.trim() || selectedMqttServerId === null || mqttServersQuery.isPending || mqttServersQuery.isError,
|
|
958
984
|
onPress: createSession,
|
|
959
|
-
children: isLoading ? "Preparing commissioning" : "
|
|
985
|
+
children: isLoading ? "Preparing commissioning" : "Scan controller for review"
|
|
960
986
|
}),
|
|
961
987
|
session?.state === "awaiting_identity_confirmation" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
|
|
962
988
|
isPending: isLoading,
|
|
963
|
-
isDisabled: hostKeyFingerprint !== session.hostKeyFingerprint,
|
|
989
|
+
isDisabled: !hostKeyFingerprint || hostKeyFingerprint !== session.hostKeyFingerprint,
|
|
964
990
|
onPress: confirmHostKey,
|
|
965
991
|
children: isLoading ? "Confirming identity" : "Confirm host key"
|
|
966
992
|
}),
|
|
967
|
-
session &&
|
|
993
|
+
session && canInstall(session) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
|
|
968
994
|
isPending: isLoading,
|
|
969
|
-
isDisabled: !sshUsername.trim() || !sshPassword,
|
|
995
|
+
isDisabled: isLoading || !confirmInstall || !sshUsername.trim() || !sshPassword,
|
|
970
996
|
onPress: deliverSession,
|
|
971
|
-
children: isLoading ? "Starting
|
|
997
|
+
children: isLoading ? "Starting installation" : session.state === "delivery_failed" ? "Retry installation" : "Install runtime"
|
|
972
998
|
}),
|
|
973
999
|
session && session.state !== "completed" && session.state !== "revoked" && (isCancelConfirmationOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
|
|
974
|
-
|
|
1000
|
+
variant: "danger",
|
|
975
1001
|
isPending: isLoading,
|
|
976
1002
|
onPress: () => removeSessionMutation.mutate(session.id, { onSuccess: close }),
|
|
977
1003
|
children: isLoading ? "Canceling enrollment" : "Confirm cancellation"
|
|
@@ -989,7 +1015,7 @@ function DevicePassport({ className, name, step }) {
|
|
|
989
1015
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
|
|
990
1016
|
className: `wg:min-w-0 wg:rounded-large wg:bg-default-100 wg:p-5 ${className ?? ""}`,
|
|
991
1017
|
children: [
|
|
992
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CpuIcon$1, { className: "wg:h-10 wg:w-10 wg:text-
|
|
1018
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CpuIcon$1, { className: "wg:h-10 wg:w-10 wg:text-primary" }),
|
|
993
1019
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
994
1020
|
className: "wg:mt-4 wg:text-xs wg:font-semibold wg:uppercase wg:tracking-wider wg:text-muted",
|
|
995
1021
|
children: "CC100 device passport"
|
|
@@ -1003,15 +1029,15 @@ function DevicePassport({ className, name, step }) {
|
|
|
1003
1029
|
children: [
|
|
1004
1030
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PassportRow, {
|
|
1005
1031
|
label: "Identity",
|
|
1006
|
-
value: step >= 2 ? "
|
|
1032
|
+
value: step >= 2 ? "Review required" : "Not scanned"
|
|
1007
1033
|
}),
|
|
1008
1034
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PassportRow, {
|
|
1009
1035
|
label: "Runtime",
|
|
1010
|
-
value: step >=
|
|
1036
|
+
value: step >= 2 ? "See session status" : "Not installed"
|
|
1011
1037
|
}),
|
|
1012
1038
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PassportRow, {
|
|
1013
1039
|
label: "Claim",
|
|
1014
|
-
value: step >= 3 ? "
|
|
1040
|
+
value: step >= 3 ? "See session status" : "Pending"
|
|
1015
1041
|
})
|
|
1016
1042
|
]
|
|
1017
1043
|
}),
|
|
@@ -1022,7 +1048,7 @@ function DevicePassport({ className, name, step }) {
|
|
|
1022
1048
|
1,
|
|
1023
1049
|
2,
|
|
1024
1050
|
3
|
|
1025
|
-
].map((index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `wg:h-1.5 wg:flex-1 wg:rounded-full ${index <= step ? "wg:bg-
|
|
1051
|
+
].map((index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `wg:h-1.5 wg:flex-1 wg:rounded-full ${index <= step ? "wg:bg-primary" : "wg:bg-default-300"}` }, index))
|
|
1026
1052
|
})
|
|
1027
1053
|
]
|
|
1028
1054
|
});
|
|
@@ -1039,9 +1065,9 @@ function PassportRow({ label, value }) {
|
|
|
1039
1065
|
function StepHeading({ step }) {
|
|
1040
1066
|
const content = [
|
|
1041
1067
|
["Give this controller a useful name", "This name is reserved now and applied automatically when the controller comes online."],
|
|
1042
|
-
["Connect
|
|
1043
|
-
["
|
|
1044
|
-
["Commissioning
|
|
1068
|
+
["Connect the controller", "Enter its private address and choose an MQTT server. Review the scanned identity before installation."],
|
|
1069
|
+
["Review and approve installation", "Confirm the controller identity, enter temporary SSH credentials, and approve the changes for this attempt."],
|
|
1070
|
+
["Commissioning status", "A saved session does not authorize a new installation. After an interruption, review the status before retrying."]
|
|
1045
1071
|
][step];
|
|
1046
1072
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
1047
1073
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
@@ -1065,7 +1091,7 @@ function StepHeading({ step }) {
|
|
|
1065
1091
|
function OperationStatus({ title, description }) {
|
|
1066
1092
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1067
1093
|
"aria-live": "polite",
|
|
1068
|
-
className: "wg:rounded-large wg:border wg:border-
|
|
1094
|
+
className: "wg:rounded-large wg:border wg:border-primary/30 wg:bg-primary/5 wg:p-3",
|
|
1069
1095
|
children: [
|
|
1070
1096
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1071
1097
|
className: "wg:flex wg:items-center wg:gap-2",
|
|
@@ -1083,7 +1109,7 @@ function OperationStatus({ title, description }) {
|
|
|
1083
1109
|
}),
|
|
1084
1110
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1085
1111
|
className: "wg:mt-3 wg:h-1 wg:overflow-hidden wg:rounded-full wg:bg-default-200",
|
|
1086
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "wg:h-full wg:w-2/5 wg:animate-pulse wg:rounded-full wg:bg-
|
|
1112
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "wg:h-full wg:w-2/5 wg:animate-pulse wg:rounded-full wg:bg-primary" })
|
|
1087
1113
|
})
|
|
1088
1114
|
]
|
|
1089
1115
|
});
|
|
@@ -1103,93 +1129,139 @@ function NameStep({ name, onNameChange }) {
|
|
|
1103
1129
|
function ConnectionStep({ controllerIp, mqttServerId, mqttServersQuery, onControllerIpChange, onMqttServerIdChange }) {
|
|
1104
1130
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1105
1131
|
className: "wg:space-y-4",
|
|
1106
|
-
children: [
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1132
|
+
children: [
|
|
1133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3, {
|
|
1134
|
+
status: "accent",
|
|
1135
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Title, { children: "Prepare the CC100 (751-9301), firmware 31" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Description, { children: "Have the controller powered using its specified supply and wait for it to finish starting. Connect its Ethernet cable to the intended local network. Ask your network administrator for its assigned IP address and ensure the Attraccess server can reach it. Keep power and networking connected during installation. If the model or firmware is uncertain, check the device label and your maintenance records with the responsible administrator before proceeding." })] })]
|
|
1136
|
+
}),
|
|
1137
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField, {
|
|
1138
|
+
isRequired: true,
|
|
1139
|
+
name: "controller-ip",
|
|
1140
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$1, { children: "Controller IP address" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
1141
|
+
value: controllerIp,
|
|
1142
|
+
placeholder: "192.168.1.42",
|
|
1143
|
+
onChange: (event) => onControllerIpChange(event.target.value)
|
|
1144
|
+
})]
|
|
1145
|
+
}),
|
|
1146
|
+
mqttServersQuery.isPending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1147
|
+
className: "wg:flex wg:justify-center wg:p-2",
|
|
1148
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner$2, {
|
|
1149
|
+
color: "accent",
|
|
1150
|
+
size: "sm"
|
|
1151
|
+
})
|
|
1152
|
+
}) : mqttServersQuery.isError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorAlert$1, { error: mqttServersQuery.error }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
|
|
1153
|
+
className: "wg:w-full",
|
|
1154
|
+
name: "mqttServerId",
|
|
1155
|
+
placeholder: "Select an MQTT server",
|
|
1156
|
+
value: mqttServerId,
|
|
1157
|
+
onChange: onMqttServerIdChange,
|
|
1158
|
+
children: [
|
|
1159
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$1, { children: "MQTT server" }),
|
|
1160
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1.Trigger, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select$1.Value, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select$1.Indicator, {})] }),
|
|
1161
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select$1.Popover, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListBox$1, {
|
|
1162
|
+
renderEmptyState: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1163
|
+
className: "wg:block wg:p-3 wg:text-sm wg:text-muted",
|
|
1164
|
+
children: "No MQTT servers configured."
|
|
1165
|
+
}),
|
|
1166
|
+
children: (mqttServersQuery.data ?? []).map((server) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ListBox$1.Item, {
|
|
1167
|
+
id: server.id.toString(),
|
|
1168
|
+
textValue: server.name,
|
|
1169
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1170
|
+
className: "wg:min-w-0 wg:truncate",
|
|
1171
|
+
children: server.name
|
|
1172
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListBox$1.ItemIndicator, {})]
|
|
1173
|
+
}, server.id))
|
|
1174
|
+
}) })
|
|
1175
|
+
]
|
|
1119
1176
|
})
|
|
1120
|
-
|
|
1121
|
-
className: "wg:w-full",
|
|
1122
|
-
name: "mqttServerId",
|
|
1123
|
-
placeholder: "Select an MQTT server",
|
|
1124
|
-
value: mqttServerId,
|
|
1125
|
-
onChange: onMqttServerIdChange,
|
|
1126
|
-
children: [
|
|
1127
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$1, { children: "MQTT server" }),
|
|
1128
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1.Trigger, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select$1.Value, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select$1.Indicator, {})] }),
|
|
1129
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select$1.Popover, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListBox$1, {
|
|
1130
|
-
renderEmptyState: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1131
|
-
className: "wg:block wg:p-3 wg:text-sm wg:text-muted",
|
|
1132
|
-
children: "No MQTT servers configured."
|
|
1133
|
-
}),
|
|
1134
|
-
children: (mqttServersQuery.data ?? []).map((server) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ListBox$1.Item, {
|
|
1135
|
-
id: server.id.toString(),
|
|
1136
|
-
textValue: server.name,
|
|
1137
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1138
|
-
className: "wg:min-w-0 wg:truncate",
|
|
1139
|
-
children: server.name
|
|
1140
|
-
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ListBox$1.ItemIndicator, {})]
|
|
1141
|
-
}, server.id))
|
|
1142
|
-
}) })
|
|
1143
|
-
]
|
|
1144
|
-
})]
|
|
1177
|
+
]
|
|
1145
1178
|
});
|
|
1146
1179
|
}
|
|
1147
1180
|
function HostKeyConfirmationStep({ fingerprint, expectedFingerprint, onFingerprintChange }) {
|
|
1148
1181
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1149
1182
|
className: "wg:space-y-4",
|
|
1150
|
-
children: [
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1183
|
+
children: [
|
|
1184
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3, {
|
|
1185
|
+
status: "warning",
|
|
1186
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Title, { children: "Review the controller SSH key" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Description, { children: "Compare the scanned fingerprint with a trusted inventory record or a fingerprint obtained independently through a trusted administrator. Copying the scan back here is not independent authentication. If no trusted fingerprint is available, confirm the physical controller and its cabling on an isolated network you control; this reduces exposure but does not independently authenticate the SSH key. Stop if the device or network is uncertain. Confirming the key does not start installation." })] })]
|
|
1187
|
+
}),
|
|
1188
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
1189
|
+
className: "wg:break-all wg:text-sm",
|
|
1190
|
+
children: ["Scanned fingerprint: ", expectedFingerprint]
|
|
1191
|
+
}),
|
|
1192
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField, {
|
|
1193
|
+
isRequired: true,
|
|
1194
|
+
name: "host-key-fingerprint",
|
|
1195
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$1, { children: "Reviewed SSH host-key fingerprint" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
1196
|
+
value: fingerprint,
|
|
1197
|
+
onChange: (event) => onFingerprintChange(event.target.value)
|
|
1198
|
+
})]
|
|
1199
|
+
})
|
|
1200
|
+
]
|
|
1162
1201
|
});
|
|
1163
1202
|
}
|
|
1164
|
-
function
|
|
1203
|
+
function canInstall(session) {
|
|
1204
|
+
return [
|
|
1205
|
+
"awaiting_delivery",
|
|
1206
|
+
"delivery_failed",
|
|
1207
|
+
"awaiting_codesys_confirmation"
|
|
1208
|
+
].includes(session.state);
|
|
1209
|
+
}
|
|
1210
|
+
function DeliveryStep({ isDelivering, session, sshUsername, sshPassword, onSshUsernameChange, onSshPasswordChange, confirmInstall, onConfirmInstallChange }) {
|
|
1165
1211
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1166
1212
|
className: "wg:space-y-4",
|
|
1167
1213
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommissioningStatusPanel, {
|
|
1168
1214
|
isActive: isDelivering || session.state === "delivering",
|
|
1169
1215
|
session
|
|
1170
|
-
}),
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1216
|
+
}), canInstall(session) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1217
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3, {
|
|
1218
|
+
status: "warning",
|
|
1219
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Indicator, {}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert$3.Title, { children: "Review this installation attempt" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3.Description, { children: [
|
|
1220
|
+
"Installing on ",
|
|
1221
|
+
session.targetHost,
|
|
1222
|
+
" can interrupt CODESYS and replaces an existing Attraccess runtime container. Make sure connected equipment can safely tolerate the interruption and replacement. Installation does not certify the controller as hardened or the connected equipment as ready for use."
|
|
1223
|
+
] })] })]
|
|
1224
|
+
}),
|
|
1225
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1226
|
+
className: "wg:grid wg:gap-4 wg:sm:grid-cols-2",
|
|
1227
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField, {
|
|
1228
|
+
isRequired: true,
|
|
1229
|
+
isDisabled: isDelivering,
|
|
1230
|
+
name: "ssh-username",
|
|
1231
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$1, { children: "Temporary SSH username" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
1232
|
+
autoComplete: "off",
|
|
1233
|
+
value: sshUsername,
|
|
1234
|
+
onChange: (event) => onSshUsernameChange(event.target.value)
|
|
1235
|
+
})]
|
|
1236
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField, {
|
|
1237
|
+
isRequired: true,
|
|
1238
|
+
isDisabled: isDelivering,
|
|
1239
|
+
name: "ssh-password",
|
|
1240
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label$1, { children: "Temporary SSH password" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
1241
|
+
autoComplete: "off",
|
|
1242
|
+
type: "password",
|
|
1243
|
+
value: sshPassword,
|
|
1244
|
+
onChange: (event) => onSshPasswordChange(event.target.value)
|
|
1245
|
+
})]
|
|
1178
1246
|
})]
|
|
1179
|
-
}),
|
|
1247
|
+
}),
|
|
1248
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox, {
|
|
1180
1249
|
isRequired: true,
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1250
|
+
isDisabled: isDelivering,
|
|
1251
|
+
isSelected: confirmInstall,
|
|
1252
|
+
onChange: onConfirmInstallChange,
|
|
1253
|
+
name: "confirm-install",
|
|
1254
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Checkbox.Content, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox.Control, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox.Indicator, {}) }), "I approve CODESYS interruption and replacement of the existing Attraccess runtime container for this installation attempt."] })
|
|
1255
|
+
}),
|
|
1256
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
1257
|
+
className: "wg:text-sm wg:text-muted",
|
|
1258
|
+
children: "Enter the controller credentials explicitly; no default credentials are used. The password and approval are cleared after submitting or closing. Every retry needs a new approval and password. Restarting the server does not start or resume an installation."
|
|
1259
|
+
})
|
|
1260
|
+
] })]
|
|
1189
1261
|
});
|
|
1190
1262
|
}
|
|
1191
1263
|
function ProgressStep({ name, session }) {
|
|
1192
|
-
const complete = session.state === "completed";
|
|
1264
|
+
const complete = session.state === "completed" || session.state === "revoked";
|
|
1193
1265
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1194
1266
|
className: "wg:space-y-4",
|
|
1195
1267
|
children: [
|
|
@@ -1209,7 +1281,7 @@ function ProgressStep({ name, session }) {
|
|
|
1209
1281
|
children: "Safe to close"
|
|
1210
1282
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
1211
1283
|
className: "wg:mt-1 wg:text-muted",
|
|
1212
|
-
children: "This session is saved in the CC100 devices table.
|
|
1284
|
+
children: "This session is saved in the CC100 devices table. Closing this window does not cancel an installation already submitted. If installation is interrupted, reopening the session or restarting the server does not authorize another attempt."
|
|
1213
1285
|
})]
|
|
1214
1286
|
}),
|
|
1215
1287
|
session.failureReason && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert$3, {
|
|
@@ -1223,11 +1295,11 @@ function CommissioningStatusPanel({ isActive, session }) {
|
|
|
1223
1295
|
const percent = session.progressPercent ?? 0;
|
|
1224
1296
|
const isQueued = session.state === "awaiting_delivery";
|
|
1225
1297
|
const hasFailure = !isActive && Boolean(session.failureReason);
|
|
1226
|
-
const title = isQueued ? "
|
|
1227
|
-
const detail = isQueued ? "
|
|
1298
|
+
const title = isQueued ? "Installation approval required" : session.progressStep ?? (isActive ? "Preparing commissioning" : commissioningLabel(session.state));
|
|
1299
|
+
const detail = isQueued ? "Review the installation, enter temporary SSH credentials, and approve this attempt. Saved sessions do not start or resume installation automatically." : session.progressDetail ?? "Waiting for the next commissioning operation.";
|
|
1228
1300
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1229
1301
|
"aria-live": "polite",
|
|
1230
|
-
className: "wg:rounded-large wg:border wg:border-
|
|
1302
|
+
className: "wg:rounded-large wg:border wg:border-primary/30 wg:bg-primary/5 wg:p-4",
|
|
1231
1303
|
children: [
|
|
1232
1304
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1233
1305
|
className: "wg:flex wg:items-start wg:gap-3",
|
|
@@ -1254,7 +1326,7 @@ function CommissioningStatusPanel({ isActive, session }) {
|
|
|
1254
1326
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1255
1327
|
className: "wg:mt-3 wg:h-1.5 wg:overflow-hidden wg:rounded-full wg:bg-default-200",
|
|
1256
1328
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1257
|
-
className: `wg:h-full wg:rounded-full wg:transition-[width] wg:duration-500 ${hasFailure ? "wg:bg-danger" : "wg:bg-
|
|
1329
|
+
className: `wg:h-full wg:rounded-full wg:transition-[width] wg:duration-500 ${hasFailure ? "wg:bg-danger" : "wg:bg-primary"}`,
|
|
1258
1330
|
style: { width: `${percent}%` }
|
|
1259
1331
|
})
|
|
1260
1332
|
})
|
package/frontend/remoteEntry.js
CHANGED
|
@@ -9,7 +9,7 @@ var exportSet = /* @__PURE__ */ new Set([
|
|
|
9
9
|
]);
|
|
10
10
|
var moduleMap = { "./plugin": () => {
|
|
11
11
|
dynamicLoadingCss(["style.css"], false, "./plugin");
|
|
12
|
-
return __federation_import("./__federation_expose_Plugin-
|
|
12
|
+
return __federation_import("./__federation_expose_Plugin-CVjUpNsq.js").then((module) => Object.keys(module).every((item) => exportSet.has(item)) ? () => module.default : () => module);
|
|
13
13
|
} };
|
|
14
14
|
var seen = {};
|
|
15
15
|
var dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
|
package/frontend/style.css
CHANGED
|
@@ -259,17 +259,7 @@
|
|
|
259
259
|
border-top-width: 1px;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
.wg\:
|
|
263
|
-
border-color: var(--accent);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
267
|
-
.wg\:border-accent\/30 {
|
|
268
|
-
border-color: color-mix(in oklab, var(--accent) 30%, transparent);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.wg\:bg-accent, .wg\:bg-accent\/5 {
|
|
262
|
+
.wg\:bg-accent\/5 {
|
|
273
263
|
background-color: var(--accent);
|
|
274
264
|
}
|
|
275
265
|
|
|
@@ -371,6 +361,10 @@
|
|
|
371
361
|
letter-spacing: var(--wg-tracking-wider);
|
|
372
362
|
}
|
|
373
363
|
|
|
364
|
+
.wg\:break-all {
|
|
365
|
+
word-break: break-all;
|
|
366
|
+
}
|
|
367
|
+
|
|
374
368
|
.wg\:text-accent-soft-foreground {
|
|
375
369
|
color: var(--accent-soft-foreground);
|
|
376
370
|
}
|