@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-
|
|
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:
|
|
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.
|
|
8285
|
-
*
|
|
8286
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14415
|
-
|
|
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
|
-
|
|
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
|
}] });
|