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