@camstack/addon-terminal 0.1.28 → 0.1.30

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.
Files changed (3) hide show
  1. package/dist/addon.js +229 -23
  2. package/dist/addon.mjs +229 -23
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -31,7 +31,7 @@ let node_module = require("node:module");
31
31
  let sharp = require("sharp");
32
32
  sharp = __toESM(sharp);
33
33
  let node_http = require("node:http");
34
- //#region ../types/dist/event-category-XfKNtfCc.mjs
34
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
35
35
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
36
  EventCategory["SystemBoot"] = "system.boot";
37
37
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -47,6 +47,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
47
47
  */
48
48
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
49
49
  /**
50
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
51
+ * Emitted only when the material on disk actually changed, so an
52
+ * operator who trusted the old certificate by hand is told rather than
53
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
54
+ *
55
+ * Rule: docs/decisions/adr-0227-*.md
56
+ */
57
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
58
+ /**
50
59
  * A newer addon or server-root package version was found by the
51
60
  * authoritative registry check. Emitted once when any observed
52
61
  * `latestVersion` changes (or a package/node first appears behind);
@@ -20023,7 +20032,12 @@ var TerminalSessionInfoSchema = object({
20023
20032
  var TerminalProfileInfoSchema = object({
20024
20033
  profileId: string(),
20025
20034
  label: string(),
20026
- description: string().optional()
20035
+ description: string().optional(),
20036
+ /** Spawn defaults the instance form copies on create. */
20037
+ executable: string().optional(),
20038
+ args: array(string()).readonly().optional(),
20039
+ cwd: string().optional(),
20040
+ environment: array(string()).readonly().optional()
20027
20041
  });
20028
20042
  /**
20029
20043
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20036,7 +20050,11 @@ var TerminalInstanceInfoSchema = object({
20036
20050
  profileId: string(),
20037
20051
  profileLabel: string(),
20038
20052
  name: string(),
20039
- enabled: boolean()
20053
+ enabled: boolean(),
20054
+ executable: string(),
20055
+ args: array(string()).readonly(),
20056
+ cwd: string(),
20057
+ environment: array(string()).readonly()
20040
20058
  });
20041
20059
  var TerminalLegacyCameraSchema = object({
20042
20060
  stableId: string(),
@@ -20082,7 +20100,22 @@ var terminalSessionCapability = {
20082
20100
  createInstance: method(object({
20083
20101
  targetNodeId: string().min(1),
20084
20102
  profileId: string().min(1),
20085
- name: string().trim().min(1).max(160).optional()
20103
+ name: string().trim().min(1).max(160).optional(),
20104
+ executable: string().max(1024).optional(),
20105
+ args: array(string().max(2048)).max(64).optional(),
20106
+ cwd: string().max(1024).optional(),
20107
+ environment: array(string().max(4096)).max(64).optional()
20108
+ }), TerminalInstanceInfoSchema, {
20109
+ kind: "mutation",
20110
+ auth: "admin"
20111
+ }),
20112
+ updateInstance: method(object({
20113
+ instanceId: string().min(1),
20114
+ name: string().trim().min(1).max(160).optional(),
20115
+ executable: string().max(1024).optional(),
20116
+ args: array(string().max(2048)).max(64).optional(),
20117
+ cwd: string().max(1024).optional(),
20118
+ environment: array(string().max(4096)).max(64).optional()
20086
20119
  }), TerminalInstanceInfoSchema, {
20087
20120
  kind: "mutation",
20088
20121
  auth: "admin"
@@ -20117,7 +20150,11 @@ var terminalSessionCapability = {
20117
20150
  openSession: method(object({
20118
20151
  profileId: string(),
20119
20152
  cols: number().int().positive(),
20120
- rows: number().int().positive()
20153
+ rows: number().int().positive(),
20154
+ executable: string().max(1024).optional(),
20155
+ args: array(string().max(2048)).max(64).optional(),
20156
+ cwd: string().max(1024).optional(),
20157
+ environment: array(string().max(4096)).max(64).optional()
20121
20158
  }), TerminalSessionInfoSchema, {
20122
20159
  kind: "mutation",
20123
20160
  auth: "admin"
@@ -24172,6 +24209,20 @@ var AllowedAddressesSchema = object({
24172
24209
  * Network Addresses admin page and persisted by the addon.
24173
24210
  */
24174
24211
  addresses: array(string()).readonly() });
24212
+ var TlsStatusSchema = object({
24213
+ mode: _enum([
24214
+ "generated",
24215
+ "uploaded",
24216
+ "disabled"
24217
+ ]),
24218
+ leafFingerprintSha256: string().nullable(),
24219
+ caFingerprintSha256: string().nullable(),
24220
+ validTo: string().nullable(),
24221
+ sans: array(string()),
24222
+ caCertPem: string().nullable(),
24223
+ reissueError: string().nullable(),
24224
+ restartRequired: boolean()
24225
+ });
24175
24226
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24176
24227
  /**
24177
24228
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -24191,7 +24242,20 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
24191
24242
  * to avoid mixed-content blocks in the browser. The public
24192
24243
  * tunnel always emits `https://` regardless. */
24193
24244
  scheme: _enum(["http", "https"]).optional()
24194
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24245
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
24246
+ kind: "mutation",
24247
+ auth: "admin"
24248
+ }), method(object({
24249
+ certPem: string().min(1),
24250
+ keyPem: string().min(1),
24251
+ caPem: string().optional()
24252
+ }), TlsStatusSchema, {
24253
+ kind: "mutation",
24254
+ auth: "admin"
24255
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
24256
+ kind: "mutation",
24257
+ auth: "admin"
24258
+ });
24195
24259
  var LockControlStatusSchema = object({
24196
24260
  /** Lifecycle state of the lock. `jammed` means the motor reported
24197
24261
  * failure to reach the target — operator intervention required. */
@@ -32216,6 +32280,12 @@ Object.freeze({
32216
32280
  addonId: null,
32217
32281
  access: "create"
32218
32282
  },
32283
+ "localNetwork.downloadCa": {
32284
+ capName: "local-network",
32285
+ capScope: "system",
32286
+ addonId: null,
32287
+ access: "view"
32288
+ },
32219
32289
  "localNetwork.getAllowedAddresses": {
32220
32290
  capName: "local-network",
32221
32291
  capScope: "system",
@@ -32240,18 +32310,36 @@ Object.freeze({
32240
32310
  addonId: null,
32241
32311
  access: "view"
32242
32312
  },
32313
+ "localNetwork.getTlsStatus": {
32314
+ capName: "local-network",
32315
+ capScope: "system",
32316
+ addonId: null,
32317
+ access: "view"
32318
+ },
32243
32319
  "localNetwork.list": {
32244
32320
  capName: "local-network",
32245
32321
  capScope: "system",
32246
32322
  addonId: null,
32247
32323
  access: "view"
32248
32324
  },
32325
+ "localNetwork.regenerateCertificate": {
32326
+ capName: "local-network",
32327
+ capScope: "system",
32328
+ addonId: null,
32329
+ access: "create"
32330
+ },
32249
32331
  "localNetwork.resetAllowlistToBestMatch": {
32250
32332
  capName: "local-network",
32251
32333
  capScope: "system",
32252
32334
  addonId: null,
32253
32335
  access: "delete"
32254
32336
  },
32337
+ "localNetwork.revertToGeneratedCertificate": {
32338
+ capName: "local-network",
32339
+ capScope: "system",
32340
+ addonId: null,
32341
+ access: "create"
32342
+ },
32255
32343
  "localNetwork.setAllowedAddresses": {
32256
32344
  capName: "local-network",
32257
32345
  capScope: "system",
@@ -32264,6 +32352,12 @@ Object.freeze({
32264
32352
  addonId: null,
32265
32353
  access: "create"
32266
32354
  },
32355
+ "localNetwork.uploadCertificate": {
32356
+ capName: "local-network",
32357
+ capScope: "system",
32358
+ addonId: null,
32359
+ access: "create"
32360
+ },
32267
32361
  "lockControl.lock": {
32268
32362
  capName: "lock-control",
32269
32363
  capScope: "device",
@@ -35144,6 +35238,12 @@ Object.freeze({
35144
35238
  addonId: null,
35145
35239
  access: "create"
35146
35240
  },
35241
+ "terminalSession.updateInstance": {
35242
+ capName: "terminal-session",
35243
+ capScope: "system",
35244
+ addonId: null,
35245
+ access: "create"
35246
+ },
35147
35247
  "terminalSession.writeInput": {
35148
35248
  capName: "terminal-session",
35149
35249
  capScope: "system",
@@ -37631,6 +37731,35 @@ Object.freeze(Object.fromEntries([{
37631
37731
  }]
37632
37732
  }].map((s) => [s.stepId, s.defaultModelId])));
37633
37733
  string().min(1);
37734
+ var CLUSTER_STEP_SETTING_FIELDS = [{
37735
+ stepId: "face-embedding",
37736
+ key: "minLandmarkFaceSize",
37737
+ label: "Min face size for recognition (detection px)",
37738
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
37739
+ type: "slider",
37740
+ min: 0,
37741
+ max: 64,
37742
+ step: 2,
37743
+ default: 24
37744
+ }];
37745
+ function clusterStepSettingKey(stepId, fieldKey) {
37746
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
37747
+ }
37748
+ var ClusterSettingNumberSchema = number().finite();
37749
+ function readClusterStepSettings(config) {
37750
+ const out = {};
37751
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
37752
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
37753
+ const value = parsed.success ? parsed.data : field.default;
37754
+ const existing = out[field.stepId] ?? {};
37755
+ out[field.stepId] = {
37756
+ ...existing,
37757
+ [field.key]: value
37758
+ };
37759
+ }
37760
+ return out;
37761
+ }
37762
+ readClusterStepSettings({});
37634
37763
  object({
37635
37764
  /**
37636
37765
  * Fraction of the box's own size added on EACH side before cutting.
@@ -43338,7 +43467,8 @@ var TerminalCameraRelay = class {
43338
43467
  const opening = this.api.openSession(state.nodeId, {
43339
43468
  profileId: state.profileId,
43340
43469
  cols: 120,
43341
- rows: 40
43470
+ rows: 40,
43471
+ instanceId: state.instanceId
43342
43472
  });
43343
43473
  state.openPromise = opening;
43344
43474
  try {
@@ -43725,8 +43855,25 @@ var TerminalInstanceSchema = object({
43725
43855
  profileId: string().min(1).max(64),
43726
43856
  profileLabel: string().min(1).max(120),
43727
43857
  name: string().min(1).max(160),
43728
- enabled: boolean()
43858
+ enabled: boolean(),
43859
+ executable: string().max(1024).default(""),
43860
+ args: array(string().max(2048)).max(64).default([]),
43861
+ cwd: string().max(1024).default(""),
43862
+ environment: array(string().max(4096)).max(64).default([])
43729
43863
  });
43864
+ function environmentEntriesToRecord(entries) {
43865
+ const out = {};
43866
+ for (const entry of entries) {
43867
+ const eq = entry.indexOf("=");
43868
+ if (eq <= 0) continue;
43869
+ out[entry.slice(0, eq)] = entry.slice(eq + 1);
43870
+ }
43871
+ return out;
43872
+ }
43873
+ function environmentRecordToEntries(env) {
43874
+ if (env === void 0) return [];
43875
+ return Object.entries(env).map(([name, value]) => `${name}=${value}`);
43876
+ }
43730
43877
  /**
43731
43878
  * Config is operator-writable, so malformed or duplicate rows are ignored
43732
43879
  * rather than allowed to make declaration reconciliation destructive.
@@ -43990,7 +44137,11 @@ var TerminalSessionManager = class {
43990
44137
  return this.profiles.map((p) => ({
43991
44138
  profileId: p.profileId,
43992
44139
  label: p.label,
43993
- ...p.description !== void 0 ? { description: p.description } : {}
44140
+ ...p.description !== void 0 ? { description: p.description } : {},
44141
+ executable: p.file,
44142
+ args: [...p.args],
44143
+ cwd: p.cwd ?? "",
44144
+ environment: environmentRecordToEntries(p.env)
43994
44145
  }));
43995
44146
  }
43996
44147
  setInstanceControl(control) {
@@ -44002,6 +44153,9 @@ var TerminalSessionManager = class {
44002
44153
  async createInstance(input) {
44003
44154
  return this.requireInstanceControl().createInstance(input);
44004
44155
  }
44156
+ async updateInstance(input) {
44157
+ return this.requireInstanceControl().updateInstance(input);
44158
+ }
44005
44159
  async deleteInstance(input) {
44006
44160
  await this.requireInstanceControl().deleteInstance(input);
44007
44161
  }
@@ -44024,24 +44178,29 @@ var TerminalSessionManager = class {
44024
44178
  const cols = clampDimension(input.cols, MAX_COLS);
44025
44179
  const rows = clampDimension(input.rows, MAX_ROWS);
44026
44180
  const sessionId = this.mintId();
44181
+ const file = input.executable?.trim() || profile.file;
44182
+ const args = input.args !== void 0 ? [...input.args] : [...profile.args];
44183
+ const cwd = input.cwd?.trim() || profile.cwd || this.opts.cwd;
44184
+ const env = {
44185
+ ...this.opts.env,
44186
+ ...profile.env,
44187
+ ...environmentEntriesToRecord(input.environment ?? [])
44188
+ };
44027
44189
  const executable = [{
44028
- file: profile.file,
44029
- args: profile.args
44030
- }, ...profile.fallbacks ?? []].map((candidate) => ({
44190
+ file,
44191
+ args
44192
+ }, ...input.executable?.trim() ? [] : profile.fallbacks ?? []].map((candidate) => ({
44031
44193
  ...candidate,
44032
44194
  resolvedFile: this.resolveBinary(candidate.file)
44033
44195
  })).find((candidate) => candidate.resolvedFile !== null);
44034
- if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${profile.file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44196
+ if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44035
44197
  const pty = this.opts.spawn({
44036
44198
  file: executable.resolvedFile,
44037
44199
  args: executable.args,
44038
44200
  cols,
44039
44201
  rows,
44040
- ...profile.cwd !== void 0 || this.opts.cwd !== void 0 ? { cwd: profile.cwd ?? this.opts.cwd } : {},
44041
- ...profile.env !== void 0 || this.opts.env !== void 0 ? { env: {
44042
- ...this.opts.env,
44043
- ...profile.env
44044
- } } : {}
44202
+ ...cwd !== void 0 && cwd !== "" ? { cwd } : {},
44203
+ ...Object.keys(env).length > 0 ? { env } : {}
44045
44204
  });
44046
44205
  const screen = this.opts.screenFactory(cols, rows);
44047
44206
  const info = {
@@ -44139,7 +44298,7 @@ var TerminalSessionManager = class {
44139
44298
  const oldestSeq = session.output[0]?.seq ?? session.nextSeq;
44140
44299
  if (input.afterSeq === 0 || input.afterSeq < oldestSeq - 1 || input.afterSeq > cursor) {
44141
44300
  await session.screen.flush();
44142
- const exit = [...session.output].reverse().find((event) => event.kind === "exit");
44301
+ const exit = session.output.toReversed().find((event) => event.kind === "exit");
44143
44302
  return {
44144
44303
  cursor,
44145
44304
  reset: true,
@@ -44383,7 +44542,21 @@ var TerminalAddon = class extends BaseAddon {
44383
44542
  const localNodeId = declarationOwnerNodeId(this.ctx.kernel.localNodeId);
44384
44543
  const cameraRelay = new TerminalCameraRelay({
44385
44544
  listProfiles: (nodeId) => nodeId === localNodeId ? manager.listProfiles() : this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
44386
- openSession: (nodeId, input) => nodeId === localNodeId ? manager.openSession(input) : this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
44545
+ openSession: (nodeId, input) => {
44546
+ const instance = input.instanceId ? this.terminalInstances().find((row) => row.id === input.instanceId) : void 0;
44547
+ const payload = {
44548
+ profileId: input.profileId,
44549
+ cols: input.cols,
44550
+ rows: input.rows,
44551
+ ...instance ? {
44552
+ ...instance.executable ? { executable: instance.executable } : {},
44553
+ ...instance.args.length > 0 ? { args: [...instance.args] } : {},
44554
+ ...instance.cwd ? { cwd: instance.cwd } : {},
44555
+ ...instance.environment.length > 0 ? { environment: [...instance.environment] } : {}
44556
+ } : {}
44557
+ };
44558
+ return nodeId === localNodeId ? manager.openSession(payload) : this.ctx.api.terminalSession.openSession.mutate(payload, nodePin(nodeId));
44559
+ },
44387
44560
  pullOutput: (nodeId, input) => nodeId === localNodeId ? manager.pullOutput(input) : this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
44388
44561
  closeSession: async (nodeId, sessionId) => {
44389
44562
  if (nodeId === localNodeId) await manager.close({ sessionId });
@@ -44588,6 +44761,7 @@ var TerminalAddon = class extends BaseAddon {
44588
44761
  return {
44589
44762
  listInstances: async () => this.terminalInstances().map((instance) => this.instanceInfo(instance)),
44590
44763
  createInstance: async (input) => this.createTerminalInstance(input),
44764
+ updateInstance: async (input) => this.updateTerminalInstance(input),
44591
44765
  deleteInstance: async ({ instanceId }) => this.deleteTerminalInstance(instanceId),
44592
44766
  setInstanceEnabled: async ({ instanceId, enabled }) => this.setTerminalInstanceEnabled(instanceId, enabled),
44593
44767
  listLegacyCameras: async () => this.listLegacyTerminalCameras(),
@@ -44607,7 +44781,11 @@ var TerminalAddon = class extends BaseAddon {
44607
44781
  profileId: instance.profileId,
44608
44782
  profileLabel: instance.profileLabel,
44609
44783
  name: instance.name,
44610
- enabled: instance.enabled
44784
+ enabled: instance.enabled,
44785
+ executable: instance.executable,
44786
+ args: instance.args,
44787
+ cwd: instance.cwd,
44788
+ environment: instance.environment
44611
44789
  };
44612
44790
  }
44613
44791
  replaceTerminalCameraTombstones(stableIds) {
@@ -44635,11 +44813,35 @@ var TerminalAddon = class extends BaseAddon {
44635
44813
  profileId: profile.profileId,
44636
44814
  profileLabel: profile.label,
44637
44815
  name,
44638
- enabled: true
44816
+ enabled: true,
44817
+ executable: input.executable?.trim() || profile.executable || "",
44818
+ args: input.args ? [...input.args] : [...profile.args ?? []],
44819
+ cwd: input.cwd?.trim() || profile.cwd || "",
44820
+ environment: input.environment ? [...input.environment] : [...profile.environment ?? []]
44639
44821
  };
44640
44822
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44641
44823
  return this.instanceInfo(instance);
44642
44824
  }
44825
+ async updateTerminalInstance(input) {
44826
+ const updated = await this.instanceMutationQueue.run(() => this.updateTerminalInstanceUnlocked(input));
44827
+ await this.cameraRelay?.closeInstance(updated.instanceId);
44828
+ await this.reconcileTerminalCameras();
44829
+ return updated;
44830
+ }
44831
+ async updateTerminalInstanceUnlocked(input) {
44832
+ const instance = this.terminalInstances().find((candidate) => candidate.id === input.instanceId);
44833
+ if (!instance) throw new Error(`No such Terminal instance: ${input.instanceId}`);
44834
+ const updated = {
44835
+ ...instance,
44836
+ ...input.name !== void 0 ? { name: input.name.trim() } : {},
44837
+ ...input.executable !== void 0 ? { executable: input.executable.trim() } : {},
44838
+ ...input.args !== void 0 ? { args: [...input.args] } : {},
44839
+ ...input.cwd !== void 0 ? { cwd: input.cwd.trim() } : {},
44840
+ ...input.environment !== void 0 ? { environment: [...input.environment] } : {}
44841
+ };
44842
+ await this.updateGlobalSettings({ terminalInstances: this.config.terminalInstances.map((candidate) => candidate.id === input.instanceId ? updated : candidate) });
44843
+ return this.instanceInfo(updated);
44844
+ }
44643
44845
  async deleteTerminalInstance(instanceId) {
44644
44846
  await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instanceId));
44645
44847
  await this.reconcileTerminalCameras();
@@ -44699,7 +44901,11 @@ var TerminalAddon = class extends BaseAddon {
44699
44901
  profileId: "monitor",
44700
44902
  profileLabel: legacy.profileLabel,
44701
44903
  name: requestedName?.trim() || legacy.name,
44702
- enabled: true
44904
+ enabled: true,
44905
+ executable: "",
44906
+ args: [],
44907
+ cwd: "",
44908
+ environment: []
44703
44909
  };
44704
44910
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44705
44911
  return this.instanceInfo(instance);
package/dist/addon.mjs CHANGED
@@ -8,7 +8,7 @@ import { createServer } from "node:http";
8
8
  //#region \0rolldown/runtime.js
9
9
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
10
  //#endregion
11
- //#region ../types/dist/event-category-XfKNtfCc.mjs
11
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
12
12
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
13
13
  EventCategory["SystemBoot"] = "system.boot";
14
14
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -24,6 +24,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
24
24
  */
25
25
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
26
26
  /**
27
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
28
+ * Emitted only when the material on disk actually changed, so an
29
+ * operator who trusted the old certificate by hand is told rather than
30
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
31
+ *
32
+ * Rule: docs/decisions/adr-0227-*.md
33
+ */
34
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
35
+ /**
27
36
  * A newer addon or server-root package version was found by the
28
37
  * authoritative registry check. Emitted once when any observed
29
38
  * `latestVersion` changes (or a package/node first appears behind);
@@ -20000,7 +20009,12 @@ var TerminalSessionInfoSchema = object({
20000
20009
  var TerminalProfileInfoSchema = object({
20001
20010
  profileId: string(),
20002
20011
  label: string(),
20003
- description: string().optional()
20012
+ description: string().optional(),
20013
+ /** Spawn defaults the instance form copies on create. */
20014
+ executable: string().optional(),
20015
+ args: array(string()).readonly().optional(),
20016
+ cwd: string().optional(),
20017
+ environment: array(string()).readonly().optional()
20004
20018
  });
20005
20019
  /**
20006
20020
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -20013,7 +20027,11 @@ var TerminalInstanceInfoSchema = object({
20013
20027
  profileId: string(),
20014
20028
  profileLabel: string(),
20015
20029
  name: string(),
20016
- enabled: boolean()
20030
+ enabled: boolean(),
20031
+ executable: string(),
20032
+ args: array(string()).readonly(),
20033
+ cwd: string(),
20034
+ environment: array(string()).readonly()
20017
20035
  });
20018
20036
  var TerminalLegacyCameraSchema = object({
20019
20037
  stableId: string(),
@@ -20059,7 +20077,22 @@ var terminalSessionCapability = {
20059
20077
  createInstance: method(object({
20060
20078
  targetNodeId: string().min(1),
20061
20079
  profileId: string().min(1),
20062
- name: string().trim().min(1).max(160).optional()
20080
+ name: string().trim().min(1).max(160).optional(),
20081
+ executable: string().max(1024).optional(),
20082
+ args: array(string().max(2048)).max(64).optional(),
20083
+ cwd: string().max(1024).optional(),
20084
+ environment: array(string().max(4096)).max(64).optional()
20085
+ }), TerminalInstanceInfoSchema, {
20086
+ kind: "mutation",
20087
+ auth: "admin"
20088
+ }),
20089
+ updateInstance: method(object({
20090
+ instanceId: string().min(1),
20091
+ name: string().trim().min(1).max(160).optional(),
20092
+ executable: string().max(1024).optional(),
20093
+ args: array(string().max(2048)).max(64).optional(),
20094
+ cwd: string().max(1024).optional(),
20095
+ environment: array(string().max(4096)).max(64).optional()
20063
20096
  }), TerminalInstanceInfoSchema, {
20064
20097
  kind: "mutation",
20065
20098
  auth: "admin"
@@ -20094,7 +20127,11 @@ var terminalSessionCapability = {
20094
20127
  openSession: method(object({
20095
20128
  profileId: string(),
20096
20129
  cols: number().int().positive(),
20097
- rows: number().int().positive()
20130
+ rows: number().int().positive(),
20131
+ executable: string().max(1024).optional(),
20132
+ args: array(string().max(2048)).max(64).optional(),
20133
+ cwd: string().max(1024).optional(),
20134
+ environment: array(string().max(4096)).max(64).optional()
20098
20135
  }), TerminalSessionInfoSchema, {
20099
20136
  kind: "mutation",
20100
20137
  auth: "admin"
@@ -24149,6 +24186,20 @@ var AllowedAddressesSchema = object({
24149
24186
  * Network Addresses admin page and persisted by the addon.
24150
24187
  */
24151
24188
  addresses: array(string()).readonly() });
24189
+ var TlsStatusSchema = object({
24190
+ mode: _enum([
24191
+ "generated",
24192
+ "uploaded",
24193
+ "disabled"
24194
+ ]),
24195
+ leafFingerprintSha256: string().nullable(),
24196
+ caFingerprintSha256: string().nullable(),
24197
+ validTo: string().nullable(),
24198
+ sans: array(string()),
24199
+ caCertPem: string().nullable(),
24200
+ reissueError: string().nullable(),
24201
+ restartRequired: boolean()
24202
+ });
24152
24203
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
24153
24204
  /**
24154
24205
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -24168,7 +24219,20 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
24168
24219
  * to avoid mixed-content blocks in the browser. The public
24169
24220
  * tunnel always emits `https://` regardless. */
24170
24221
  scheme: _enum(["http", "https"]).optional()
24171
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
24222
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
24223
+ kind: "mutation",
24224
+ auth: "admin"
24225
+ }), method(object({
24226
+ certPem: string().min(1),
24227
+ keyPem: string().min(1),
24228
+ caPem: string().optional()
24229
+ }), TlsStatusSchema, {
24230
+ kind: "mutation",
24231
+ auth: "admin"
24232
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
24233
+ kind: "mutation",
24234
+ auth: "admin"
24235
+ });
24172
24236
  var LockControlStatusSchema = object({
24173
24237
  /** Lifecycle state of the lock. `jammed` means the motor reported
24174
24238
  * failure to reach the target — operator intervention required. */
@@ -32193,6 +32257,12 @@ Object.freeze({
32193
32257
  addonId: null,
32194
32258
  access: "create"
32195
32259
  },
32260
+ "localNetwork.downloadCa": {
32261
+ capName: "local-network",
32262
+ capScope: "system",
32263
+ addonId: null,
32264
+ access: "view"
32265
+ },
32196
32266
  "localNetwork.getAllowedAddresses": {
32197
32267
  capName: "local-network",
32198
32268
  capScope: "system",
@@ -32217,18 +32287,36 @@ Object.freeze({
32217
32287
  addonId: null,
32218
32288
  access: "view"
32219
32289
  },
32290
+ "localNetwork.getTlsStatus": {
32291
+ capName: "local-network",
32292
+ capScope: "system",
32293
+ addonId: null,
32294
+ access: "view"
32295
+ },
32220
32296
  "localNetwork.list": {
32221
32297
  capName: "local-network",
32222
32298
  capScope: "system",
32223
32299
  addonId: null,
32224
32300
  access: "view"
32225
32301
  },
32302
+ "localNetwork.regenerateCertificate": {
32303
+ capName: "local-network",
32304
+ capScope: "system",
32305
+ addonId: null,
32306
+ access: "create"
32307
+ },
32226
32308
  "localNetwork.resetAllowlistToBestMatch": {
32227
32309
  capName: "local-network",
32228
32310
  capScope: "system",
32229
32311
  addonId: null,
32230
32312
  access: "delete"
32231
32313
  },
32314
+ "localNetwork.revertToGeneratedCertificate": {
32315
+ capName: "local-network",
32316
+ capScope: "system",
32317
+ addonId: null,
32318
+ access: "create"
32319
+ },
32232
32320
  "localNetwork.setAllowedAddresses": {
32233
32321
  capName: "local-network",
32234
32322
  capScope: "system",
@@ -32241,6 +32329,12 @@ Object.freeze({
32241
32329
  addonId: null,
32242
32330
  access: "create"
32243
32331
  },
32332
+ "localNetwork.uploadCertificate": {
32333
+ capName: "local-network",
32334
+ capScope: "system",
32335
+ addonId: null,
32336
+ access: "create"
32337
+ },
32244
32338
  "lockControl.lock": {
32245
32339
  capName: "lock-control",
32246
32340
  capScope: "device",
@@ -35121,6 +35215,12 @@ Object.freeze({
35121
35215
  addonId: null,
35122
35216
  access: "create"
35123
35217
  },
35218
+ "terminalSession.updateInstance": {
35219
+ capName: "terminal-session",
35220
+ capScope: "system",
35221
+ addonId: null,
35222
+ access: "create"
35223
+ },
35124
35224
  "terminalSession.writeInput": {
35125
35225
  capName: "terminal-session",
35126
35226
  capScope: "system",
@@ -37608,6 +37708,35 @@ Object.freeze(Object.fromEntries([{
37608
37708
  }]
37609
37709
  }].map((s) => [s.stepId, s.defaultModelId])));
37610
37710
  string().min(1);
37711
+ var CLUSTER_STEP_SETTING_FIELDS = [{
37712
+ stepId: "face-embedding",
37713
+ key: "minLandmarkFaceSize",
37714
+ label: "Min face size for recognition (detection px)",
37715
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
37716
+ type: "slider",
37717
+ min: 0,
37718
+ max: 64,
37719
+ step: 2,
37720
+ default: 24
37721
+ }];
37722
+ function clusterStepSettingKey(stepId, fieldKey) {
37723
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
37724
+ }
37725
+ var ClusterSettingNumberSchema = number().finite();
37726
+ function readClusterStepSettings(config) {
37727
+ const out = {};
37728
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
37729
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
37730
+ const value = parsed.success ? parsed.data : field.default;
37731
+ const existing = out[field.stepId] ?? {};
37732
+ out[field.stepId] = {
37733
+ ...existing,
37734
+ [field.key]: value
37735
+ };
37736
+ }
37737
+ return out;
37738
+ }
37739
+ readClusterStepSettings({});
37611
37740
  object({
37612
37741
  /**
37613
37742
  * Fraction of the box's own size added on EACH side before cutting.
@@ -43315,7 +43444,8 @@ var TerminalCameraRelay = class {
43315
43444
  const opening = this.api.openSession(state.nodeId, {
43316
43445
  profileId: state.profileId,
43317
43446
  cols: 120,
43318
- rows: 40
43447
+ rows: 40,
43448
+ instanceId: state.instanceId
43319
43449
  });
43320
43450
  state.openPromise = opening;
43321
43451
  try {
@@ -43702,8 +43832,25 @@ var TerminalInstanceSchema = object({
43702
43832
  profileId: string().min(1).max(64),
43703
43833
  profileLabel: string().min(1).max(120),
43704
43834
  name: string().min(1).max(160),
43705
- enabled: boolean()
43835
+ enabled: boolean(),
43836
+ executable: string().max(1024).default(""),
43837
+ args: array(string().max(2048)).max(64).default([]),
43838
+ cwd: string().max(1024).default(""),
43839
+ environment: array(string().max(4096)).max(64).default([])
43706
43840
  });
43841
+ function environmentEntriesToRecord(entries) {
43842
+ const out = {};
43843
+ for (const entry of entries) {
43844
+ const eq = entry.indexOf("=");
43845
+ if (eq <= 0) continue;
43846
+ out[entry.slice(0, eq)] = entry.slice(eq + 1);
43847
+ }
43848
+ return out;
43849
+ }
43850
+ function environmentRecordToEntries(env) {
43851
+ if (env === void 0) return [];
43852
+ return Object.entries(env).map(([name, value]) => `${name}=${value}`);
43853
+ }
43707
43854
  /**
43708
43855
  * Config is operator-writable, so malformed or duplicate rows are ignored
43709
43856
  * rather than allowed to make declaration reconciliation destructive.
@@ -43967,7 +44114,11 @@ var TerminalSessionManager = class {
43967
44114
  return this.profiles.map((p) => ({
43968
44115
  profileId: p.profileId,
43969
44116
  label: p.label,
43970
- ...p.description !== void 0 ? { description: p.description } : {}
44117
+ ...p.description !== void 0 ? { description: p.description } : {},
44118
+ executable: p.file,
44119
+ args: [...p.args],
44120
+ cwd: p.cwd ?? "",
44121
+ environment: environmentRecordToEntries(p.env)
43971
44122
  }));
43972
44123
  }
43973
44124
  setInstanceControl(control) {
@@ -43979,6 +44130,9 @@ var TerminalSessionManager = class {
43979
44130
  async createInstance(input) {
43980
44131
  return this.requireInstanceControl().createInstance(input);
43981
44132
  }
44133
+ async updateInstance(input) {
44134
+ return this.requireInstanceControl().updateInstance(input);
44135
+ }
43982
44136
  async deleteInstance(input) {
43983
44137
  await this.requireInstanceControl().deleteInstance(input);
43984
44138
  }
@@ -44001,24 +44155,29 @@ var TerminalSessionManager = class {
44001
44155
  const cols = clampDimension(input.cols, MAX_COLS);
44002
44156
  const rows = clampDimension(input.rows, MAX_ROWS);
44003
44157
  const sessionId = this.mintId();
44158
+ const file = input.executable?.trim() || profile.file;
44159
+ const args = input.args !== void 0 ? [...input.args] : [...profile.args];
44160
+ const cwd = input.cwd?.trim() || profile.cwd || this.opts.cwd;
44161
+ const env = {
44162
+ ...this.opts.env,
44163
+ ...profile.env,
44164
+ ...environmentEntriesToRecord(input.environment ?? [])
44165
+ };
44004
44166
  const executable = [{
44005
- file: profile.file,
44006
- args: profile.args
44007
- }, ...profile.fallbacks ?? []].map((candidate) => ({
44167
+ file,
44168
+ args
44169
+ }, ...input.executable?.trim() ? [] : profile.fallbacks ?? []].map((candidate) => ({
44008
44170
  ...candidate,
44009
44171
  resolvedFile: this.resolveBinary(candidate.file)
44010
44172
  })).find((candidate) => candidate.resolvedFile !== null);
44011
- if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${profile.file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44173
+ if (executable === void 0) throw new Error(`Terminal profile '${profile.profileId}' cannot start: executable '${file}' was not found on PATH. Install it in the server image or set its path in Terminal settings.`);
44012
44174
  const pty = this.opts.spawn({
44013
44175
  file: executable.resolvedFile,
44014
44176
  args: executable.args,
44015
44177
  cols,
44016
44178
  rows,
44017
- ...profile.cwd !== void 0 || this.opts.cwd !== void 0 ? { cwd: profile.cwd ?? this.opts.cwd } : {},
44018
- ...profile.env !== void 0 || this.opts.env !== void 0 ? { env: {
44019
- ...this.opts.env,
44020
- ...profile.env
44021
- } } : {}
44179
+ ...cwd !== void 0 && cwd !== "" ? { cwd } : {},
44180
+ ...Object.keys(env).length > 0 ? { env } : {}
44022
44181
  });
44023
44182
  const screen = this.opts.screenFactory(cols, rows);
44024
44183
  const info = {
@@ -44116,7 +44275,7 @@ var TerminalSessionManager = class {
44116
44275
  const oldestSeq = session.output[0]?.seq ?? session.nextSeq;
44117
44276
  if (input.afterSeq === 0 || input.afterSeq < oldestSeq - 1 || input.afterSeq > cursor) {
44118
44277
  await session.screen.flush();
44119
- const exit = [...session.output].reverse().find((event) => event.kind === "exit");
44278
+ const exit = session.output.toReversed().find((event) => event.kind === "exit");
44120
44279
  return {
44121
44280
  cursor,
44122
44281
  reset: true,
@@ -44360,7 +44519,21 @@ var TerminalAddon = class extends BaseAddon {
44360
44519
  const localNodeId = declarationOwnerNodeId(this.ctx.kernel.localNodeId);
44361
44520
  const cameraRelay = new TerminalCameraRelay({
44362
44521
  listProfiles: (nodeId) => nodeId === localNodeId ? manager.listProfiles() : this.ctx.api.terminalSession.listProfiles.query(void 0, nodePin(nodeId)),
44363
- openSession: (nodeId, input) => nodeId === localNodeId ? manager.openSession(input) : this.ctx.api.terminalSession.openSession.mutate(input, nodePin(nodeId)),
44522
+ openSession: (nodeId, input) => {
44523
+ const instance = input.instanceId ? this.terminalInstances().find((row) => row.id === input.instanceId) : void 0;
44524
+ const payload = {
44525
+ profileId: input.profileId,
44526
+ cols: input.cols,
44527
+ rows: input.rows,
44528
+ ...instance ? {
44529
+ ...instance.executable ? { executable: instance.executable } : {},
44530
+ ...instance.args.length > 0 ? { args: [...instance.args] } : {},
44531
+ ...instance.cwd ? { cwd: instance.cwd } : {},
44532
+ ...instance.environment.length > 0 ? { environment: [...instance.environment] } : {}
44533
+ } : {}
44534
+ };
44535
+ return nodeId === localNodeId ? manager.openSession(payload) : this.ctx.api.terminalSession.openSession.mutate(payload, nodePin(nodeId));
44536
+ },
44364
44537
  pullOutput: (nodeId, input) => nodeId === localNodeId ? manager.pullOutput(input) : this.ctx.api.terminalSession.pullOutput.mutate(input, nodePin(nodeId)),
44365
44538
  closeSession: async (nodeId, sessionId) => {
44366
44539
  if (nodeId === localNodeId) await manager.close({ sessionId });
@@ -44565,6 +44738,7 @@ var TerminalAddon = class extends BaseAddon {
44565
44738
  return {
44566
44739
  listInstances: async () => this.terminalInstances().map((instance) => this.instanceInfo(instance)),
44567
44740
  createInstance: async (input) => this.createTerminalInstance(input),
44741
+ updateInstance: async (input) => this.updateTerminalInstance(input),
44568
44742
  deleteInstance: async ({ instanceId }) => this.deleteTerminalInstance(instanceId),
44569
44743
  setInstanceEnabled: async ({ instanceId, enabled }) => this.setTerminalInstanceEnabled(instanceId, enabled),
44570
44744
  listLegacyCameras: async () => this.listLegacyTerminalCameras(),
@@ -44584,7 +44758,11 @@ var TerminalAddon = class extends BaseAddon {
44584
44758
  profileId: instance.profileId,
44585
44759
  profileLabel: instance.profileLabel,
44586
44760
  name: instance.name,
44587
- enabled: instance.enabled
44761
+ enabled: instance.enabled,
44762
+ executable: instance.executable,
44763
+ args: instance.args,
44764
+ cwd: instance.cwd,
44765
+ environment: instance.environment
44588
44766
  };
44589
44767
  }
44590
44768
  replaceTerminalCameraTombstones(stableIds) {
@@ -44612,11 +44790,35 @@ var TerminalAddon = class extends BaseAddon {
44612
44790
  profileId: profile.profileId,
44613
44791
  profileLabel: profile.label,
44614
44792
  name,
44615
- enabled: true
44793
+ enabled: true,
44794
+ executable: input.executable?.trim() || profile.executable || "",
44795
+ args: input.args ? [...input.args] : [...profile.args ?? []],
44796
+ cwd: input.cwd?.trim() || profile.cwd || "",
44797
+ environment: input.environment ? [...input.environment] : [...profile.environment ?? []]
44616
44798
  };
44617
44799
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44618
44800
  return this.instanceInfo(instance);
44619
44801
  }
44802
+ async updateTerminalInstance(input) {
44803
+ const updated = await this.instanceMutationQueue.run(() => this.updateTerminalInstanceUnlocked(input));
44804
+ await this.cameraRelay?.closeInstance(updated.instanceId);
44805
+ await this.reconcileTerminalCameras();
44806
+ return updated;
44807
+ }
44808
+ async updateTerminalInstanceUnlocked(input) {
44809
+ const instance = this.terminalInstances().find((candidate) => candidate.id === input.instanceId);
44810
+ if (!instance) throw new Error(`No such Terminal instance: ${input.instanceId}`);
44811
+ const updated = {
44812
+ ...instance,
44813
+ ...input.name !== void 0 ? { name: input.name.trim() } : {},
44814
+ ...input.executable !== void 0 ? { executable: input.executable.trim() } : {},
44815
+ ...input.args !== void 0 ? { args: [...input.args] } : {},
44816
+ ...input.cwd !== void 0 ? { cwd: input.cwd.trim() } : {},
44817
+ ...input.environment !== void 0 ? { environment: [...input.environment] } : {}
44818
+ };
44819
+ await this.updateGlobalSettings({ terminalInstances: this.config.terminalInstances.map((candidate) => candidate.id === input.instanceId ? updated : candidate) });
44820
+ return this.instanceInfo(updated);
44821
+ }
44620
44822
  async deleteTerminalInstance(instanceId) {
44621
44823
  await this.instanceMutationQueue.run(() => this.deleteTerminalInstanceUnlocked(instanceId));
44622
44824
  await this.reconcileTerminalCameras();
@@ -44676,7 +44878,11 @@ var TerminalAddon = class extends BaseAddon {
44676
44878
  profileId: "monitor",
44677
44879
  profileLabel: legacy.profileLabel,
44678
44880
  name: requestedName?.trim() || legacy.name,
44679
- enabled: true
44881
+ enabled: true,
44882
+ executable: "",
44883
+ args: [],
44884
+ cwd: "",
44885
+ environment: []
44680
44886
  };
44681
44887
  await this.updateGlobalSettings({ terminalInstances: [...this.config.terminalInstances, instance] });
44682
44888
  return this.instanceInfo(instance);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",