@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-
|
|
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:
|
|
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.
|
|
8281
|
-
*
|
|
8282
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14411
|
-
|
|
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
|
-
|
|
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
|
}] });
|