@camstack/addon-tailscale-ingress 0.1.3 → 0.1.5

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.
@@ -4621,7 +4621,7 @@ function _instanceof(cls, params = {}) {
4621
4621
  return inst;
4622
4622
  }
4623
4623
  //#endregion
4624
- //#region ../types/dist/index-YnRVILXN.mjs
4624
+ //#region ../types/dist/index-CWhQOnm9.mjs
4625
4625
  var MODEL_FORMATS = [
4626
4626
  "onnx",
4627
4627
  "coreml",
@@ -7288,14 +7288,35 @@ var StatusSchema = object({
7288
7288
  embeddedRunning: boolean()
7289
7289
  });
7290
7290
  method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
7291
+ var LinkStateSchema = _enum([
7292
+ "unlinked",
7293
+ "linked",
7294
+ "error"
7295
+ ]);
7296
+ var ExportSetupFieldSchema = object({
7297
+ label: string(),
7298
+ value: string(),
7299
+ /** Mask the value by default + render a reveal toggle (client id, secrets). */
7300
+ secret: boolean().optional()
7301
+ });
7302
+ var ExportSetupSchema = object({
7303
+ /** A string to render as a scannable QR — HAP `X-HM://…` URI, a pairing URL, etc. Omitted when there's nothing to scan. */
7304
+ qr: string().optional(),
7305
+ /** Label/value rows shown with a copy button (HAP setup code, OAuth URLs, client id, linked-account count, …). */
7306
+ fields: array(ExportSetupFieldSchema).readonly().optional(),
7307
+ /** Free-form operator instructions rendered above the fields. */
7308
+ note: string().optional()
7309
+ });
7291
7310
  var DeviceExportStatusSchema = object({
7292
- linkState: _enum([
7293
- "unlinked",
7294
- "linked",
7295
- "error"
7296
- ]),
7311
+ linkState: LinkStateSchema,
7297
7312
  exposedDeviceCount: number(),
7298
- error: string().optional()
7313
+ error: string().optional(),
7314
+ /**
7315
+ * Optional pairing/account info the panel renders in a generic
7316
+ * "Setup" section. Addon-agnostic — the addon id identifies the
7317
+ * export target, never an `ecosystem` key here.
7318
+ */
7319
+ setup: ExportSetupSchema.optional()
7299
7320
  });
7300
7321
  var DeviceKindSchema = string();
7301
7322
  var ExposedDeviceSchema = object({
@@ -8209,42 +8230,6 @@ method(object({
8209
8230
  username: string(),
8210
8231
  password: string()
8211
8232
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
8212
- var AuthProviderInfoSchema = object({
8213
- /** Stable id matching the addon id (used for `getLoginUrl({addonId,…})`). */
8214
- addonId: string(),
8215
- /**
8216
- * Per-instance id when one addon registers multiple "logical"
8217
- * providers (e.g. OIDC with Google + Microsoft + custom). The login
8218
- * URL becomes `/addon/${addonId}/${instanceId}/start` — handler reads
8219
- * `:instanceId` from the route. Empty/unset means the addon is a
8220
- * single-instance provider; the URL is `/addon/${addonId}/start`.
8221
- */
8222
- instanceId: string().optional(),
8223
- /** Display label shown on the login button + admin row. */
8224
- displayName: string(),
8225
- /** Optional iconography hint (lucide-react icon name OR emoji). */
8226
- icon: string().optional(),
8227
- /** When true, the provider exposes a redirect-based login flow
8228
- * (`getLoginUrl` returns a URL the browser navigates to). */
8229
- hasRedirectFlow: boolean(),
8230
- /** When true, the provider exposes a credential-form login flow
8231
- * (`validateCredentials` accepts username + password). */
8232
- hasCredentialFlow: boolean(),
8233
- /** Provider kind, drives admin-UI hint dispatch (oidc / saml / totp / …). */
8234
- kind: string().optional(),
8235
- /** Operator-facing status string (e.g. "Connected to https://login.acme.com"). */
8236
- status: string().optional(),
8237
- /** When false, the provider is registered but disabled by config; the
8238
- * UI surfaces it as inactive without enumerating it for login. */
8239
- enabled: boolean()
8240
- });
8241
- method(_void(), array(AuthProviderInfoSchema).readonly()), method(object({
8242
- addonId: string(),
8243
- enabled: boolean()
8244
- }), object({ success: literal(true) }), {
8245
- kind: "mutation",
8246
- auth: "admin"
8247
- });
8248
8233
  var NetworkEndpointSchema = object({
8249
8234
  url: string(),
8250
8235
  hostname: string(),
@@ -8273,7 +8258,6 @@ var networkAccessCapability = {
8273
8258
  name: "network-access",
8274
8259
  scope: "system",
8275
8260
  mode: "collection",
8276
- internal: true,
8277
8261
  providerKind: "ingress",
8278
8262
  methods: {
8279
8263
  start: method(_void(), NetworkEndpointSchema, { kind: "mutation" }),
@@ -8281,40 +8265,13 @@ var networkAccessCapability = {
8281
8265
  getEndpoint: method(_void(), NetworkEndpointSchema.nullable()),
8282
8266
  getStatus: method(_void(), NetworkAccessStatusSchema),
8283
8267
  /**
8284
- * Enumerate every active ingress entry. Default implementation (when
8285
- * the provider omits this method) is derived from `getEndpoint()` —
8286
- * see the remote-access orchestrator for the fallback path.
8268
+ * Enumerate every active ingress entry. Providers that expose only a
8269
+ * single endpoint may omit this method; callers fall back to
8270
+ * `getEndpoint()` in that case.
8287
8271
  */
8288
8272
  listEndpoints: method(_void(), array(NetworkEndpointEntrySchema).readonly())
8289
8273
  }
8290
8274
  };
8291
- var RemoteAccessEndpointSchema = object({
8292
- url: string(),
8293
- hostname: string(),
8294
- port: number(),
8295
- protocol: _enum(["http", "https"])
8296
- });
8297
- var RemoteAccessProviderInfoSchema = object({
8298
- /** Stable id matching the addon id. */
8299
- addonId: string(),
8300
- /** Display label shown on the admin row — sourced from the addon manifest. */
8301
- displayName: string(),
8302
- /** When false, the provider is registered but disabled. */
8303
- enabled: boolean(),
8304
- /** True when the underlying tunnel/connection is up. */
8305
- connected: boolean(),
8306
- /** Public-facing endpoint, when connected. Null otherwise. */
8307
- endpoint: RemoteAccessEndpointSchema.nullable(),
8308
- /** Last error message (when connected=false), if available. */
8309
- error: string().optional()
8310
- });
8311
- method(_void(), array(RemoteAccessProviderInfoSchema).readonly()), method(object({ addonId: string() }), RemoteAccessEndpointSchema, {
8312
- kind: "mutation",
8313
- auth: "admin"
8314
- }), method(object({ addonId: string() }), object({ success: literal(true) }), {
8315
- kind: "mutation",
8316
- auth: "admin"
8317
- });
8318
8275
  var TurnServerSchema = object({
8319
8276
  /** Single URL or list of URLs (e.g. "turn:turn.example.com:3478?transport=udp"). */
8320
8277
  urls: union([string(), array(string())]),
@@ -8322,33 +8279,6 @@ var TurnServerSchema = object({
8322
8279
  credential: string().optional()
8323
8280
  });
8324
8281
  method(_void(), array(TurnServerSchema).readonly());
8325
- var TurnProviderInfoSchema = object({
8326
- /** Stable id matching the addon id. */
8327
- addonId: string(),
8328
- /** Display label shown on the admin row — sourced from the addon manifest. */
8329
- displayName: string(),
8330
- /** When false, the provider is registered but disabled. */
8331
- enabled: boolean(),
8332
- /** Number of servers this provider is currently exposing. */
8333
- serverCount: number(),
8334
- /**
8335
- * Flat list of every TURN/STUN URL this provider currently exposes.
8336
- * One row per URL (multi-URL ICE server entries are flattened). The
8337
- * admin UI shows this in a compact per-provider list so operators
8338
- * can verify what's actually being negotiated without having to dig
8339
- * into the combined `getAllServers` output.
8340
- */
8341
- urls: array(string()).readonly(),
8342
- /** Last fetch error (when serverCount=0 due to API failure), if any. */
8343
- error: string().optional()
8344
- });
8345
- method(_void(), array(TurnProviderInfoSchema).readonly()), method(_void(), array(TurnServerSchema).readonly()), method(object({
8346
- addonId: string(),
8347
- enabled: boolean()
8348
- }), object({ success: literal(true) }), {
8349
- kind: "mutation",
8350
- auth: "admin"
8351
- });
8352
8282
  var SnapshotImageSchema = object({
8353
8283
  base64: string(),
8354
8284
  contentType: string()
@@ -9442,7 +9372,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
9442
9372
  * tunnel always emits `https://` regardless. */
9443
9373
  scheme: _enum(["http", "https"]).optional()
9444
9374
  }), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
9445
- var MeshEndpointSchema$1 = object({
9375
+ var MeshEndpointSchema = object({
9446
9376
  /** Stable identifier within the provider (e.g. `mesh-ipv4`, `magicdns`, `funnel`). */
9447
9377
  id: string(),
9448
9378
  /** Operator-facing label (e.g. "Mesh IPv4", "MagicDNS"). */
@@ -9519,7 +9449,7 @@ var MeshStatusSchema = object({
9519
9449
  /** Number of peers visible to this host (excluding self). */
9520
9450
  peerCount: number(),
9521
9451
  /** Every endpoint this provider exposes for the current host. */
9522
- endpoints: array(MeshEndpointSchema$1).readonly(),
9452
+ endpoints: array(MeshEndpointSchema).readonly(),
9523
9453
  /** Last error from the daemon, when not joined. */
9524
9454
  error: string().optional(),
9525
9455
  /**
@@ -9551,7 +9481,24 @@ var MeshStatusSchema = object({
9551
9481
  * doesn't rotate keys for the bound host. Operator-facing surface
9552
9482
  * for "your access expires on …" banners.
9553
9483
  */
9554
- keyExpiry: number().nullable()
9484
+ keyExpiry: number().nullable(),
9485
+ /**
9486
+ * When the provider runs its OWN mesh daemon (e.g. the Tailscale
9487
+ * client addon in `onboard` mode spawns a private `tailscaled`),
9488
+ * this carries the local control-socket path. Companion addons that
9489
+ * must drive the SAME daemon — chiefly `tailscale-ingress` for
9490
+ * Serve/Funnel — read it to point their CLI at the right socket
9491
+ * instead of the system default. Empty when the provider uses the
9492
+ * host's system daemon (or doesn't have the concept).
9493
+ */
9494
+ daemonSocket: string().optional(),
9495
+ /**
9496
+ * Path to the mesh CLI binary the provider downloaded for onboard
9497
+ * mode. Companion addons reuse it so they don't need a system
9498
+ * install when the operator chose a fully self-contained mesh.
9499
+ * Empty in host mode.
9500
+ */
9501
+ daemonCliPath: string().optional()
9555
9502
  });
9556
9503
  method(_void(), MeshStatusSchema), method(object({
9557
9504
  /** Provider-specific auth key. For Tailscale this is the
@@ -9577,51 +9524,6 @@ authKey: string().optional() }), object({
9577
9524
  /** Human-readable error when `ok: false`. */
9578
9525
  error: string().optional()
9579
9526
  }), { kind: "mutation" });
9580
- var MeshEndpointSchema = object({
9581
- id: string(),
9582
- label: string(),
9583
- scope: _enum(["mesh", "public"]),
9584
- url: string(),
9585
- hostname: string(),
9586
- port: number(),
9587
- protocol: _enum(["http", "https"])
9588
- });
9589
- var MeshProviderInfoSchema = object({
9590
- /** Stable id matching the addon id. */
9591
- addonId: string(),
9592
- /** Display label shown on the admin row — sourced from the addon manifest. */
9593
- displayName: string(),
9594
- /** True when the host is joined to this provider's mesh. */
9595
- joined: boolean(),
9596
- /** Local mesh IP (empty when not joined). */
9597
- meshIp: string(),
9598
- /** MagicDNS / mesh hostname (empty when not configured). */
9599
- magicDnsHostname: string(),
9600
- /** Peer count (excluding self). */
9601
- peerCount: number(),
9602
- /** Active endpoints (mesh IP + MagicDNS + optional public Funnel). */
9603
- endpoints: array(MeshEndpointSchema).readonly(),
9604
- /** Last error reported by the provider. */
9605
- error: string().optional(),
9606
- /** Tenant / tailnet / network display name. Empty pre-join. */
9607
- tenantName: string(),
9608
- /** Mesh DNS suffix (e.g. tailXXXX.ts.net). Empty when not configured. */
9609
- magicDnsSuffix: string(),
9610
- /** Authenticated user / account login. Null for token-only providers. */
9611
- userLogin: string().nullable(),
9612
- /** Provider control-plane URL. */
9613
- controlPlaneUrl: string(),
9614
- /** Machine-key expiry (epoch ms). Null when keys don't rotate. */
9615
- keyExpiry: number().nullable()
9616
- });
9617
- method(_void(), array(MeshProviderInfoSchema).readonly()), method(object({
9618
- addonId: string(),
9619
- authKey: string().min(8),
9620
- hostname: string().optional()
9621
- }), object({ joined: literal(true) }), { kind: "mutation" }), method(object({ addonId: string() }), object({ success: literal(true) }), { kind: "mutation" }), method(object({
9622
- addonId: string(),
9623
- hostname: string().optional()
9624
- }), object({ loginUrl: string() }), { kind: "mutation" }), method(object({ addonId: string() }), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(object({ addonId: string() }), object({ peers: array(MeshPeerSchema).readonly() }));
9625
9527
  var MethodAccessSchema = _enum([
9626
9528
  "view",
9627
9529
  "create",
@@ -10233,6 +10135,21 @@ var AddonAutoUpdateSchema = ChannelWithInheritSchema;
10233
10135
  var RestartAddonResultSchema = unknown();
10234
10136
  var InstallPackageResultSchema = unknown();
10235
10137
  var ReloadPackagesResultSchema = unknown();
10138
+ var UpdateFrameworkPackageResultSchema = object({
10139
+ packageName: string(),
10140
+ fromVersion: string(),
10141
+ toVersion: string(),
10142
+ /** Ms-epoch the server scheduled its self-restart. */
10143
+ restartingAt: number()
10144
+ });
10145
+ var FrameworkPackageStatusSchema = object({
10146
+ packageName: string(),
10147
+ currentVersion: string(),
10148
+ latestVersion: string().nullable(),
10149
+ hasUpdate: boolean(),
10150
+ /** Optional manifest description for the row tooltip. */
10151
+ description: string().optional()
10152
+ });
10236
10153
  var LogStreamEntrySchema = object({
10237
10154
  timestamp: string(),
10238
10155
  level: string(),
@@ -10264,21 +10181,50 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
10264
10181
  }), method(_void(), ReloadPackagesResultSchema, {
10265
10182
  kind: "mutation",
10266
10183
  auth: "admin"
10267
- }), method(object({ query: string().optional() }), array(SearchResultSchema)), method(_void(), array(PackageUpdateSchema).readonly(), { auth: "admin" }), method(object({
10184
+ }), method(object({ query: string().optional() }), array(SearchResultSchema)), method(object({ nodeId: string().optional() }), array(PackageUpdateSchema).readonly(), { auth: "admin" }), method(object({
10268
10185
  name: string().min(1),
10269
- version: string().optional()
10186
+ version: string().optional(),
10187
+ nodeId: string().optional()
10270
10188
  }), unknown(), {
10271
10189
  kind: "mutation",
10272
10190
  auth: "admin"
10273
10191
  }), method(object({ name: string().min(1) }), object({ rolledBackTo: string().nullable() }), {
10274
10192
  kind: "mutation",
10275
10193
  auth: "admin"
10276
- }), method(_void(), unknown(), {
10194
+ }), method(object({ nodeId: string().optional() }), unknown(), {
10277
10195
  kind: "mutation",
10278
10196
  auth: "admin"
10279
10197
  }), method(object({ confirm: literal(true) }), unknown(), {
10280
10198
  kind: "mutation",
10281
10199
  auth: "admin"
10200
+ }), method(_void(), object({
10201
+ kind: _enum([
10202
+ "framework-update",
10203
+ "manual",
10204
+ "system"
10205
+ ]),
10206
+ packageName: string().optional(),
10207
+ fromVersion: string().optional(),
10208
+ toVersion: string().optional(),
10209
+ requestedBy: string().optional(),
10210
+ requestedAt: number()
10211
+ }).nullable(), { auth: "admin" }), method(_void(), array(FrameworkPackageStatusSchema).readonly(), { auth: "admin" }), method(object({ capName: string().min(1) }), array(object({
10212
+ addonId: string(),
10213
+ mode: _enum(["singleton", "collection"]),
10214
+ isActive: boolean()
10215
+ })).readonly()), method(object({
10216
+ capName: string().min(1),
10217
+ addonId: string().min(1),
10218
+ enabled: boolean()
10219
+ }), object({ success: literal(true) }), {
10220
+ kind: "mutation",
10221
+ auth: "admin"
10222
+ }), method(object({
10223
+ packageName: string().min(1),
10224
+ version: string().optional()
10225
+ }), UpdateFrameworkPackageResultSchema, {
10226
+ kind: "mutation",
10227
+ auth: "admin"
10282
10228
  }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
10283
10229
  kind: "mutation",
10284
10230
  auth: "admin"
@@ -10310,6 +10256,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
10310
10256
  EventCategory2["SystemBoot"] = "system.boot";
10311
10257
  EventCategory2["SystemAddonsReady"] = "system.addons-ready";
10312
10258
  EventCategory2["SystemRestarting"] = "system.restarting";
10259
+ EventCategory2["SystemRestartCompleted"] = "system.restart-completed";
10313
10260
  EventCategory2["SystemReadyState"] = "system.ready-state";
10314
10261
  EventCategory2["AddonStarted"] = "addon.started";
10315
10262
  EventCategory2["AddonStopped"] = "addon.stopped";
@@ -10832,6 +10779,12 @@ Object.freeze({
10832
10779
  addonId: null,
10833
10780
  access: "view"
10834
10781
  },
10782
+ "addons.getLastRestart": {
10783
+ capName: "addons",
10784
+ capScope: "system",
10785
+ addonId: null,
10786
+ access: "view"
10787
+ },
10835
10788
  "addons.getLogs": {
10836
10789
  capName: "addons",
10837
10790
  capScope: "system",
@@ -10868,6 +10821,18 @@ Object.freeze({
10868
10821
  addonId: null,
10869
10822
  access: "view"
10870
10823
  },
10824
+ "addons.listCapabilityProviders": {
10825
+ capName: "addons",
10826
+ capScope: "system",
10827
+ addonId: null,
10828
+ access: "view"
10829
+ },
10830
+ "addons.listFrameworkPackages": {
10831
+ capName: "addons",
10832
+ capScope: "system",
10833
+ addonId: null,
10834
+ access: "view"
10835
+ },
10871
10836
  "addons.listPackages": {
10872
10837
  capName: "addons",
10873
10838
  capScope: "system",
@@ -10940,12 +10905,24 @@ Object.freeze({
10940
10905
  addonId: null,
10941
10906
  access: "create"
10942
10907
  },
10908
+ "addons.setCapabilityProviderEnabled": {
10909
+ capName: "addons",
10910
+ capScope: "system",
10911
+ addonId: null,
10912
+ access: "create"
10913
+ },
10943
10914
  "addons.uninstallPackage": {
10944
10915
  capName: "addons",
10945
10916
  capScope: "system",
10946
10917
  addonId: null,
10947
10918
  access: "delete"
10948
10919
  },
10920
+ "addons.updateFrameworkPackage": {
10921
+ capName: "addons",
10922
+ capScope: "system",
10923
+ addonId: null,
10924
+ access: "create"
10925
+ },
10949
10926
  "addons.updatePackage": {
10950
10927
  capName: "addons",
10951
10928
  capScope: "system",
@@ -11186,18 +11163,6 @@ Object.freeze({
11186
11163
  addonId: null,
11187
11164
  access: "view"
11188
11165
  },
11189
- "authentication.listProviders": {
11190
- capName: "authentication",
11191
- capScope: "system",
11192
- addonId: null,
11193
- access: "view"
11194
- },
11195
- "authentication.setProviderEnabled": {
11196
- capName: "authentication",
11197
- capScope: "system",
11198
- addonId: null,
11199
- access: "create"
11200
- },
11201
11166
  "authProvider.getLoginUrl": {
11202
11167
  capName: "auth-provider",
11203
11168
  capScope: "system",
@@ -12044,42 +12009,6 @@ Object.freeze({
12044
12009
  addonId: null,
12045
12010
  access: "create"
12046
12011
  },
12047
- "meshOrchestrator.joinProvider": {
12048
- capName: "mesh-orchestrator",
12049
- capScope: "system",
12050
- addonId: null,
12051
- access: "create"
12052
- },
12053
- "meshOrchestrator.leaveProvider": {
12054
- capName: "mesh-orchestrator",
12055
- capScope: "system",
12056
- addonId: null,
12057
- access: "create"
12058
- },
12059
- "meshOrchestrator.listProviderPeers": {
12060
- capName: "mesh-orchestrator",
12061
- capScope: "system",
12062
- addonId: null,
12063
- access: "view"
12064
- },
12065
- "meshOrchestrator.listProviders": {
12066
- capName: "mesh-orchestrator",
12067
- capScope: "system",
12068
- addonId: null,
12069
- access: "view"
12070
- },
12071
- "meshOrchestrator.logoutProvider": {
12072
- capName: "mesh-orchestrator",
12073
- capScope: "system",
12074
- addonId: null,
12075
- access: "create"
12076
- },
12077
- "meshOrchestrator.startLoginProvider": {
12078
- capName: "mesh-orchestrator",
12079
- capScope: "system",
12080
- addonId: null,
12081
- access: "create"
12082
- },
12083
12012
  "metricsProvider.collectSnapshot": {
12084
12013
  capName: "metrics-provider",
12085
12014
  capScope: "system",
@@ -13106,24 +13035,6 @@ Object.freeze({
13106
13035
  addonId: null,
13107
13036
  access: "create"
13108
13037
  },
13109
- "remoteAccess.listProviders": {
13110
- capName: "remote-access",
13111
- capScope: "system",
13112
- addonId: null,
13113
- access: "view"
13114
- },
13115
- "remoteAccess.startProvider": {
13116
- capName: "remote-access",
13117
- capScope: "system",
13118
- addonId: null,
13119
- access: "create"
13120
- },
13121
- "remoteAccess.stopProvider": {
13122
- capName: "remote-access",
13123
- capScope: "system",
13124
- addonId: null,
13125
- access: "create"
13126
- },
13127
13038
  "restreamer.getExposedResources": {
13128
13039
  capName: "restreamer",
13129
13040
  capScope: "system",
@@ -13682,24 +13593,6 @@ Object.freeze({
13682
13593
  addonId: null,
13683
13594
  access: "view"
13684
13595
  },
13685
- "turnOrchestrator.getAllServers": {
13686
- capName: "turn-orchestrator",
13687
- capScope: "system",
13688
- addonId: null,
13689
- access: "view"
13690
- },
13691
- "turnOrchestrator.listProviders": {
13692
- capName: "turn-orchestrator",
13693
- capScope: "system",
13694
- addonId: null,
13695
- access: "view"
13696
- },
13697
- "turnOrchestrator.setProviderEnabled": {
13698
- capName: "turn-orchestrator",
13699
- capScope: "system",
13700
- addonId: null,
13701
- access: "create"
13702
- },
13703
13596
  "turnProvider.getTurnServers": {
13704
13597
  capName: "turn-provider",
13705
13598
  capScope: "system",
@@ -14053,32 +13946,98 @@ var TailscaleCliError = class extends Error {
14053
13946
  this.name = "TailscaleCliError";
14054
13947
  }
14055
13948
  };
13949
+ var NOOP_LOGGER = {
13950
+ info: () => void 0,
13951
+ warn: () => void 0,
13952
+ error: () => void 0,
13953
+ debug: () => void 0,
13954
+ child: () => NOOP_LOGGER,
13955
+ withTags: () => NOOP_LOGGER
13956
+ };
14056
13957
  var TailscaleCli = class {
14057
13958
  resolvedBin = null;
13959
+ logger;
13960
+ binPathOverride;
13961
+ socketPath;
13962
+ constructor(opts = {}) {
13963
+ if (typeof opts.info === "function" && !("logger" in opts)) {
13964
+ this.logger = opts;
13965
+ this.binPathOverride = void 0;
13966
+ this.socketPath = void 0;
13967
+ } else {
13968
+ const o = opts;
13969
+ this.logger = o.logger ?? NOOP_LOGGER;
13970
+ this.binPathOverride = o.binPath;
13971
+ this.socketPath = o.socketPath;
13972
+ }
13973
+ }
13974
+ /** Prepend `--socket=<path>` when driving an onboard daemon. */
13975
+ withSocket(args) {
13976
+ return this.socketPath ? [`--socket=${this.socketPath}`, ...args] : [...args];
13977
+ }
14058
13978
  /** Locate the `tailscale` binary once and cache the result. */
14059
13979
  async resolveBin() {
14060
13980
  if (this.resolvedBin) return this.resolvedBin;
13981
+ if (this.binPathOverride) {
13982
+ this.resolvedBin = this.binPathOverride;
13983
+ this.logger.info("CLI binary (onboard override)", { meta: {
13984
+ bin: this.binPathOverride,
13985
+ socketPath: this.socketPath
13986
+ } });
13987
+ return this.binPathOverride;
13988
+ }
13989
+ const tried = [];
14061
13990
  for (const candidate of TAILSCALE_CANDIDATES) try {
14062
13991
  await execFileP(candidate, ["version"], { timeout: 3e3 });
14063
13992
  this.resolvedBin = candidate;
13993
+ this.logger.info("CLI binary resolved", { meta: {
13994
+ bin: candidate,
13995
+ candidatesTried: tried
13996
+ } });
14064
13997
  return candidate;
14065
- } catch {}
13998
+ } catch {
13999
+ tried.push(candidate);
14000
+ }
14066
14001
  throw new TailscaleCliError("tailscale binary not found — install Tailscale from https://tailscale.com/download");
14067
14002
  }
14068
14003
  async version() {
14069
- const { stdout } = await execFileP(await this.resolveBin(), ["version"], { timeout: 5e3 });
14004
+ const { stdout } = await execFileP(await this.resolveBin(), this.withSocket(["version"]), { timeout: 5e3 });
14070
14005
  return stdout.trim().split("\n")[0] ?? "";
14071
14006
  }
14072
14007
  async status() {
14073
14008
  const bin = await this.resolveBin();
14074
14009
  try {
14075
- const { stdout } = await execFileP(bin, ["status", "--json"], { timeout: 1e4 });
14010
+ const { stdout } = await execFileP(bin, this.withSocket(["status", "--json"]), { timeout: 1e4 });
14076
14011
  return JSON.parse(stdout);
14077
14012
  } catch (err) {
14078
14013
  const e = err;
14079
14014
  throw new TailscaleCliError(`tailscale status failed: ${e.message}`, e.stderr ?? "");
14080
14015
  }
14081
14016
  }
14017
+ /**
14018
+ * Whether the tailnet ACL policy grants THIS host the Funnel
14019
+ * attribute. Reads `Self.CapMap` / `Self.Capabilities` from
14020
+ * `tailscale status --json` — a pre-flight check so the addon can
14021
+ * fail fast with an actionable error BEFORE spending 15s on a
14022
+ * `tailscale funnel` call that the daemon would reject anyway.
14023
+ *
14024
+ * The funnel grant surfaces as a bare `funnel` key in the modern
14025
+ * CapMap; the legacy `Capabilities` string array carries either
14026
+ * `funnel` or a `.../cap/funnel` URL form.
14027
+ */
14028
+ async funnelCapable() {
14029
+ const s = await this.status();
14030
+ const capMap = s.Self?.CapMap ?? {};
14031
+ if (Object.prototype.hasOwnProperty.call(capMap, "funnel")) return true;
14032
+ const legacy = s.Self?.Capabilities ?? [];
14033
+ const capable = legacy.some((c) => c === "funnel" || c.endsWith("/cap/funnel"));
14034
+ this.logger.info("funnelCapable check", { meta: {
14035
+ capable,
14036
+ capMapKeys: Object.keys(capMap),
14037
+ legacyCount: legacy.length
14038
+ } });
14039
+ return capable;
14040
+ }
14082
14041
  /** Bring the daemon up with an auth key. Idempotent — calling
14083
14042
  * while already joined returns immediately. */
14084
14043
  async up(input) {
@@ -14091,7 +14050,7 @@ var TailscaleCli = class {
14091
14050
  ];
14092
14051
  if (input.hostname) args.push(`--hostname=${input.hostname}`);
14093
14052
  try {
14094
- await execFileP(bin, args, { timeout: 6e4 });
14053
+ await execFileP(bin, this.withSocket(args), { timeout: 6e4 });
14095
14054
  } catch (err) {
14096
14055
  const e = err;
14097
14056
  throw new TailscaleCliError(`tailscale up failed: ${e.message}`, e.stderr ?? "");
@@ -14102,7 +14061,7 @@ var TailscaleCli = class {
14102
14061
  async down() {
14103
14062
  const bin = await this.resolveBin();
14104
14063
  try {
14105
- await execFileP(bin, ["down"], { timeout: 15e3 });
14064
+ await execFileP(bin, this.withSocket(["down"]), { timeout: 15e3 });
14106
14065
  } catch (err) {
14107
14066
  const e = err;
14108
14067
  throw new TailscaleCliError(`tailscale down failed: ${e.message}`, e.stderr ?? "");
@@ -14118,11 +14077,19 @@ var TailscaleCli = class {
14118
14077
  async serve(input) {
14119
14078
  const bin = await this.resolveBin();
14120
14079
  const args = this.buildIngressArgs("serve", input);
14080
+ this.logger.info("serve: invoking CLI", { meta: {
14081
+ bin,
14082
+ args,
14083
+ ...input
14084
+ } });
14121
14085
  try {
14122
- await execFileP(bin, args, { timeout: 15e3 });
14086
+ const { stdout, stderr } = await execFileP(bin, this.withSocket(args), { timeout: 15e3 });
14087
+ this.logger.info("serve: CLI returned", { meta: {
14088
+ stdout: trimForLog(stdout),
14089
+ stderr: trimForLog(stderr)
14090
+ } });
14123
14091
  } catch (err) {
14124
- const e = err;
14125
- throw new TailscaleCliError(`tailscale serve failed: ${e.message}`, e.stderr ?? "");
14092
+ throw this.wrapCliError("tailscale serve", err, bin, args);
14126
14093
  }
14127
14094
  }
14128
14095
  /** `tailscale funnel` — exposes a local port to the open internet
@@ -14131,14 +14098,52 @@ var TailscaleCli = class {
14131
14098
  async funnel(input) {
14132
14099
  const bin = await this.resolveBin();
14133
14100
  const args = this.buildIngressArgs("funnel", input);
14101
+ this.logger.info("funnel: invoking CLI", { meta: {
14102
+ bin,
14103
+ args,
14104
+ ...input
14105
+ } });
14134
14106
  try {
14135
- await execFileP(bin, args, { timeout: 15e3 });
14107
+ const { stdout, stderr } = await execFileP(bin, this.withSocket(args), { timeout: 15e3 });
14108
+ this.logger.info("funnel: CLI returned", { meta: {
14109
+ stdout: trimForLog(stdout),
14110
+ stderr: trimForLog(stderr)
14111
+ } });
14136
14112
  } catch (err) {
14137
- const e = err;
14138
- throw new TailscaleCliError(`tailscale funnel failed: ${e.message}`, e.stderr ?? "");
14113
+ throw this.wrapCliError("tailscale funnel", err, bin, args);
14139
14114
  }
14140
14115
  }
14141
14116
  /**
14117
+ * Build a TailscaleCliError that propagates BOTH stdout and stderr
14118
+ * up to the caller. The tailscale CLI is inconsistent about which
14119
+ * stream it uses for human-readable errors — `funnel` (and at least
14120
+ * some `serve` paths) writes the actionable hint to STDOUT when
14121
+ * the host lacks the ACL grant (e.g. "Funnel is not enabled on
14122
+ * your tailnet. To enable, visit: https://login.tailscale.com/...").
14123
+ * Reading only stderr loses that hint and the operator sees a bare
14124
+ * "Command failed: …" with no recovery path.
14125
+ *
14126
+ * Special-cases the "Funnel is not enabled" outcome: extracts the
14127
+ * enable-URL the CLI prints and surfaces a concise, single-line
14128
+ * actionable error instead of dumping the raw multi-line blob.
14129
+ */
14130
+ wrapCliError(verb, err, bin, args) {
14131
+ const e = err;
14132
+ const stderr = (e.stderr ?? "").trim();
14133
+ const stdout = (e.stdout ?? "").trim();
14134
+ this.logger.error(`${verb}: CLI failed`, { meta: {
14135
+ bin,
14136
+ args,
14137
+ message: e.message,
14138
+ stderr: trimForLog(stderr),
14139
+ stdout: trimForLog(stdout)
14140
+ } });
14141
+ const combined = `${stdout}\n${stderr}`;
14142
+ if (/funnel is not enabled/i.test(combined)) return new TailscaleCliError(`Funnel is not enabled on your tailnet. Enable it for this host at: ${combined.match(/https:\/\/login\.tailscale\.com\/\S+/)?.[0] ?? "https://login.tailscale.com/admin/settings/funnel"}`, stderr.length > 0 ? stderr : stdout);
14143
+ const detail = stderr.length > 0 ? `--- stderr ---\n${stderr}` : stdout.length > 0 ? `--- stdout ---\n${stdout}` : "";
14144
+ return new TailscaleCliError(`${verb} failed: ${e.message.trim()}${detail ? `\n${detail}` : ""}`, stderr.length > 0 ? stderr : stdout);
14145
+ }
14146
+ /**
14142
14147
  * Single source of truth for the serve/funnel arg shape. The CLI
14143
14148
  * accepts the same flag layout for both verbs, so we share the
14144
14149
  * builder. `--set-path` is only emitted when the operator picks a
@@ -14154,10 +14159,17 @@ var TailscaleCli = class {
14154
14159
  }
14155
14160
  const out = [verb, "--bg"];
14156
14161
  if (path && path !== "/") out.push(`--set-path=${path}`);
14157
- out.push(`http://127.0.0.1:${input.port}`);
14162
+ const scheme = input.upstreamScheme ?? "https+insecure";
14163
+ out.push(`${scheme}://127.0.0.1:${input.port}`);
14158
14164
  return out;
14159
14165
  }
14160
14166
  };
14167
+ /** Truncate long stdout/stderr blobs so the log entry stays readable. */
14168
+ function trimForLog(s, max = 800) {
14169
+ const trimmed = s.trim();
14170
+ if (trimmed.length <= max) return trimmed;
14171
+ return `${trimmed.slice(0, max)}…[+${trimmed.length - max} bytes]`;
14172
+ }
14161
14173
  //#endregion
14162
14174
  //#region src/tailscale-ingress.addon.ts
14163
14175
  /**
@@ -14185,10 +14197,11 @@ var TailscaleCli = class {
14185
14197
  var DEFAULT_CONFIG = {
14186
14198
  mode: "serve",
14187
14199
  sourcePort: 4443,
14188
- targetPath: ""
14200
+ targetPath: "",
14201
+ upstreamScheme: "https+insecure"
14189
14202
  };
14190
14203
  var TailscaleIngressAddon = class extends BaseAddon {
14191
- cli = new TailscaleCli();
14204
+ cli;
14192
14205
  /**
14193
14206
  * Snapshot of the running ingress params so stop()/disposer can issue
14194
14207
  * the matching `off` call even if `this.config` mutates mid-flight.
@@ -14201,6 +14214,7 @@ var TailscaleIngressAddon = class extends BaseAddon {
14201
14214
  super({ ...DEFAULT_CONFIG });
14202
14215
  }
14203
14216
  async onInitialize() {
14217
+ this.cli = new TailscaleCli(this.ctx.logger.child("tailscale-cli"));
14204
14218
  try {
14205
14219
  await this.cli.version();
14206
14220
  } catch (err) {
@@ -14226,17 +14240,27 @@ var TailscaleIngressAddon = class extends BaseAddon {
14226
14240
  topic: "tailscale-ingress",
14227
14241
  phase: "verify-client"
14228
14242
  } });
14229
- await this.requireClientJoined();
14243
+ const meshStatus = await this.requireClientJoined();
14244
+ this.syncCliToDaemon(meshStatus);
14245
+ if (this.config.mode === "funnel") {
14246
+ log.info("start: phase=verify-funnel-grant", { tags: {
14247
+ topic: "tailscale-ingress",
14248
+ phase: "verify-funnel-grant"
14249
+ } });
14250
+ if (!await this.cli.funnelCapable()) throw new Error("tailscale-ingress: Funnel is not enabled for this host in the tailnet ACL policy. Enable it at https://login.tailscale.com/admin/settings/funnel (or add a `funnel` nodeAttr for this machine), then retry. Serve mode works without this grant.");
14251
+ }
14230
14252
  const next = {
14231
14253
  mode: this.config.mode,
14232
14254
  sourcePort: this.config.sourcePort,
14233
- targetPath: this.config.targetPath
14255
+ targetPath: this.config.targetPath,
14256
+ upstreamScheme: this.config.upstreamScheme
14234
14257
  };
14235
14258
  log.info("start: phase=apply-rule", {
14236
14259
  meta: {
14237
14260
  mode: next.mode,
14238
14261
  sourcePort: next.sourcePort,
14239
- targetPath: next.targetPath
14262
+ targetPath: next.targetPath,
14263
+ upstreamScheme: next.upstreamScheme
14240
14264
  },
14241
14265
  tags: {
14242
14266
  topic: "tailscale-ingress",
@@ -14376,10 +14400,27 @@ var TailscaleIngressAddon = class extends BaseAddon {
14376
14400
  const opts = {
14377
14401
  port: rule.sourcePort,
14378
14402
  enabled,
14403
+ upstreamScheme: rule.upstreamScheme,
14379
14404
  ...rule.targetPath && rule.targetPath !== "/" ? { targetPath: rule.targetPath } : {}
14380
14405
  };
14406
+ this.ctx.logger.info(`applyRule: ${rule.mode}`, {
14407
+ meta: {
14408
+ ...opts,
14409
+ mode: rule.mode
14410
+ },
14411
+ tags: {
14412
+ topic: "tailscale-ingress",
14413
+ phase: "apply-rule",
14414
+ verb: rule.mode
14415
+ }
14416
+ });
14381
14417
  if (rule.mode === "funnel") await this.cli.funnel(opts);
14382
14418
  else await this.cli.serve(opts);
14419
+ this.ctx.logger.info(`applyRule: ${rule.mode} done`, { tags: {
14420
+ topic: "tailscale-ingress",
14421
+ phase: "apply-rule-done",
14422
+ verb: rule.mode
14423
+ } });
14383
14424
  }
14384
14425
  /**
14385
14426
  * Build the canonical endpoint from the active ingress + the
@@ -14409,15 +14450,65 @@ var TailscaleIngressAddon = class extends BaseAddon {
14409
14450
  /**
14410
14451
  * Wait for `mesh-network` (the tailscale-client provider) to be
14411
14452
  * mounted AND report joined. Throws an actionable error otherwise.
14453
+ *
14454
+ * IMPORTANT: we route the check through the API surface
14455
+ * (`api.meshNetwork.getStatus.query({ addonId: 'tailscale-client' })`),
14456
+ * NOT through `ctx.capabilities.getCollectionEntries`. The latter
14457
+ * only sees providers registered in THIS process's local
14458
+ * CapabilityRegistry — and tailscale-client runs in a separate
14459
+ * `hub/tailscale-client` group runner. The API surface is
14460
+ * cross-process via Moleculer, so it can find the provider wherever
14461
+ * it lives.
14462
+ *
14463
+ * `mesh-network` is a `collection` cap (Tailscale + Headscale +
14464
+ * ZeroTier could all implement it in parallel), so we pin to
14465
+ * `tailscale-client` via the `addonId` filter — without it the API
14466
+ * would route to whichever provider registered first and we'd risk
14467
+ * emitting `tailscale serve` against e.g. a Headscale daemon that
14468
+ * doesn't know that verb.
14412
14469
  */
14413
14470
  async requireClientJoined() {
14414
- if (!(this.ctx.capabilities?.getCollectionEntries("mesh-network") ?? []).find(([id]) => id === "tailscale-client")) throw new Error(`tailscale-ingress: tailscale-client provider not registered — install + start the @camstack/addon-tailscale-client addon first.`);
14415
- if (!(await this.fetchMeshStatus()).joined) throw new Error("tailscale-ingress: tailnet not joined — open the Mesh Networks page and click \"Connect to Tailscale\" first.");
14471
+ let status;
14472
+ try {
14473
+ status = await this.fetchMeshStatus({ addonId: "tailscale-client" });
14474
+ } catch (err) {
14475
+ throw new Error("tailscale-ingress: tailscale-client provider not registered — install + start the @camstack/addon-tailscale-client addon first.", { cause: err });
14476
+ }
14477
+ if (!status.joined) throw new Error("tailscale-ingress: tailnet not joined — open the Mesh Networks page and click \"Connect to Tailscale\" first.");
14478
+ return status;
14416
14479
  }
14417
- async fetchMeshStatus() {
14480
+ /**
14481
+ * Rebuild `this.cli` to drive the tailscale-client's daemon. When
14482
+ * the client runs in `onboard` mode it spawned a private
14483
+ * `tailscaled`; the mesh status then carries `daemonSocket` +
14484
+ * `daemonCliPath` and we MUST route serve/funnel through that
14485
+ * socket — the system daemon is a different node with a different
14486
+ * mesh identity. Host mode leaves both unset → default CLI.
14487
+ */
14488
+ syncCliToDaemon(status) {
14489
+ const cliLogger = this.ctx.logger.child("tailscale-cli");
14490
+ if (status.daemonSocket) {
14491
+ this.ctx.logger.info("start: routing CLI to onboard daemon", {
14492
+ meta: {
14493
+ daemonSocket: status.daemonSocket,
14494
+ daemonCliPath: status.daemonCliPath
14495
+ },
14496
+ tags: {
14497
+ topic: "tailscale-ingress",
14498
+ phase: "daemon-route"
14499
+ }
14500
+ });
14501
+ this.cli = new TailscaleCli({
14502
+ logger: cliLogger,
14503
+ socketPath: status.daemonSocket,
14504
+ ...status.daemonCliPath ? { binPath: status.daemonCliPath } : {}
14505
+ });
14506
+ } else this.cli = new TailscaleCli({ logger: cliLogger });
14507
+ }
14508
+ async fetchMeshStatus(filter) {
14418
14509
  const api = this.ctx.api;
14419
14510
  if (!api.meshNetwork?.getStatus?.query) throw new Error("tailscale-ingress: mesh-network cap not exposed on the api surface");
14420
- return await api.meshNetwork.getStatus.query();
14511
+ return await api.meshNetwork.getStatus.query(filter);
14421
14512
  }
14422
14513
  globalSettingsSchema() {
14423
14514
  return this.schema({ sections: [{
@@ -14457,6 +14548,27 @@ var TailscaleIngressAddon = class extends BaseAddon {
14457
14548
  description: "Public-side mount path. Leave empty for root.",
14458
14549
  default: DEFAULT_CONFIG.targetPath,
14459
14550
  placeholder: "/"
14551
+ }),
14552
+ this.field({
14553
+ type: "select",
14554
+ key: "upstreamScheme",
14555
+ label: "Upstream scheme",
14556
+ description: "How Tailscale reaches the local hub. The hub listens HTTPS with a self-signed cert by default — keep \"https+insecure\". Use \"http\" ONLY if the hub runs with tls.enabled: false (a plain \"http\" rule against the TLS listener returns 502).",
14557
+ default: DEFAULT_CONFIG.upstreamScheme,
14558
+ options: [
14559
+ {
14560
+ value: "https+insecure",
14561
+ label: "HTTPS, self-signed (default)"
14562
+ },
14563
+ {
14564
+ value: "https",
14565
+ label: "HTTPS, valid cert"
14566
+ },
14567
+ {
14568
+ value: "http",
14569
+ label: "HTTP (only if hub TLS disabled)"
14570
+ }
14571
+ ]
14460
14572
  })
14461
14573
  ]
14462
14574
  }] });