@camstack/addon-tailscale-ingress 0.1.3 → 0.1.4
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-Ce7RZWP4.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({
|
|
@@ -9547,7 +9568,24 @@ var MeshStatusSchema = object({
|
|
|
9547
9568
|
* doesn't rotate keys for the bound host. Operator-facing surface
|
|
9548
9569
|
* for "your access expires on …" banners.
|
|
9549
9570
|
*/
|
|
9550
|
-
keyExpiry: number().nullable()
|
|
9571
|
+
keyExpiry: number().nullable(),
|
|
9572
|
+
/**
|
|
9573
|
+
* When the provider runs its OWN mesh daemon (e.g. the Tailscale
|
|
9574
|
+
* client addon in `onboard` mode spawns a private `tailscaled`),
|
|
9575
|
+
* this carries the local control-socket path. Companion addons that
|
|
9576
|
+
* must drive the SAME daemon — chiefly `tailscale-ingress` for
|
|
9577
|
+
* Serve/Funnel — read it to point their CLI at the right socket
|
|
9578
|
+
* instead of the system default. Empty when the provider uses the
|
|
9579
|
+
* host's system daemon (or doesn't have the concept).
|
|
9580
|
+
*/
|
|
9581
|
+
daemonSocket: string().optional(),
|
|
9582
|
+
/**
|
|
9583
|
+
* Path to the mesh CLI binary the provider downloaded for onboard
|
|
9584
|
+
* mode. Companion addons reuse it so they don't need a system
|
|
9585
|
+
* install when the operator chose a fully self-contained mesh.
|
|
9586
|
+
* Empty in host mode.
|
|
9587
|
+
*/
|
|
9588
|
+
daemonCliPath: string().optional()
|
|
9551
9589
|
});
|
|
9552
9590
|
method(_void(), MeshStatusSchema), method(object({
|
|
9553
9591
|
/** Provider-specific auth key. For Tailscale this is the
|
|
@@ -10229,6 +10267,21 @@ var AddonAutoUpdateSchema = ChannelWithInheritSchema;
|
|
|
10229
10267
|
var RestartAddonResultSchema = unknown();
|
|
10230
10268
|
var InstallPackageResultSchema = unknown();
|
|
10231
10269
|
var ReloadPackagesResultSchema = unknown();
|
|
10270
|
+
var UpdateFrameworkPackageResultSchema = object({
|
|
10271
|
+
packageName: string(),
|
|
10272
|
+
fromVersion: string(),
|
|
10273
|
+
toVersion: string(),
|
|
10274
|
+
/** Ms-epoch the server scheduled its self-restart. */
|
|
10275
|
+
restartingAt: number()
|
|
10276
|
+
});
|
|
10277
|
+
var FrameworkPackageStatusSchema = object({
|
|
10278
|
+
packageName: string(),
|
|
10279
|
+
currentVersion: string(),
|
|
10280
|
+
latestVersion: string().nullable(),
|
|
10281
|
+
hasUpdate: boolean(),
|
|
10282
|
+
/** Optional manifest description for the row tooltip. */
|
|
10283
|
+
description: string().optional()
|
|
10284
|
+
});
|
|
10232
10285
|
var LogStreamEntrySchema = object({
|
|
10233
10286
|
timestamp: string(),
|
|
10234
10287
|
level: string(),
|
|
@@ -10260,21 +10313,43 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
|
|
|
10260
10313
|
}), method(_void(), ReloadPackagesResultSchema, {
|
|
10261
10314
|
kind: "mutation",
|
|
10262
10315
|
auth: "admin"
|
|
10263
|
-
}), method(object({ query: string().optional() }), array(SearchResultSchema)), method(
|
|
10316
|
+
}), method(object({ query: string().optional() }), array(SearchResultSchema)), method(object({ nodeId: string().optional() }), array(PackageUpdateSchema).readonly(), { auth: "admin" }), method(object({
|
|
10264
10317
|
name: string().min(1),
|
|
10265
|
-
version: string().optional()
|
|
10318
|
+
version: string().optional(),
|
|
10319
|
+
nodeId: string().optional()
|
|
10266
10320
|
}), unknown(), {
|
|
10267
10321
|
kind: "mutation",
|
|
10268
10322
|
auth: "admin"
|
|
10269
10323
|
}), method(object({ name: string().min(1) }), object({ rolledBackTo: string().nullable() }), {
|
|
10270
10324
|
kind: "mutation",
|
|
10271
10325
|
auth: "admin"
|
|
10272
|
-
}), method(
|
|
10326
|
+
}), method(object({ nodeId: string().optional() }), unknown(), {
|
|
10273
10327
|
kind: "mutation",
|
|
10274
10328
|
auth: "admin"
|
|
10275
10329
|
}), method(object({ confirm: literal(true) }), unknown(), {
|
|
10276
10330
|
kind: "mutation",
|
|
10277
10331
|
auth: "admin"
|
|
10332
|
+
}), method(_void(), object({
|
|
10333
|
+
kind: _enum([
|
|
10334
|
+
"framework-update",
|
|
10335
|
+
"manual",
|
|
10336
|
+
"system"
|
|
10337
|
+
]),
|
|
10338
|
+
packageName: string().optional(),
|
|
10339
|
+
fromVersion: string().optional(),
|
|
10340
|
+
toVersion: string().optional(),
|
|
10341
|
+
requestedBy: string().optional(),
|
|
10342
|
+
requestedAt: number()
|
|
10343
|
+
}).nullable(), { auth: "admin" }), method(_void(), array(FrameworkPackageStatusSchema).readonly(), { auth: "admin" }), method(object({ capName: string().min(1) }), array(object({
|
|
10344
|
+
addonId: string(),
|
|
10345
|
+
mode: _enum(["singleton", "collection"]),
|
|
10346
|
+
isActive: boolean()
|
|
10347
|
+
})).readonly()), method(object({
|
|
10348
|
+
packageName: string().min(1),
|
|
10349
|
+
version: string().optional()
|
|
10350
|
+
}), UpdateFrameworkPackageResultSchema, {
|
|
10351
|
+
kind: "mutation",
|
|
10352
|
+
auth: "admin"
|
|
10278
10353
|
}), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
|
|
10279
10354
|
kind: "mutation",
|
|
10280
10355
|
auth: "admin"
|
|
@@ -10306,6 +10381,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
10306
10381
|
EventCategory2["SystemBoot"] = "system.boot";
|
|
10307
10382
|
EventCategory2["SystemAddonsReady"] = "system.addons-ready";
|
|
10308
10383
|
EventCategory2["SystemRestarting"] = "system.restarting";
|
|
10384
|
+
EventCategory2["SystemRestartCompleted"] = "system.restart-completed";
|
|
10309
10385
|
EventCategory2["SystemReadyState"] = "system.ready-state";
|
|
10310
10386
|
EventCategory2["AddonStarted"] = "addon.started";
|
|
10311
10387
|
EventCategory2["AddonStopped"] = "addon.stopped";
|
|
@@ -10828,6 +10904,12 @@ Object.freeze({
|
|
|
10828
10904
|
addonId: null,
|
|
10829
10905
|
access: "view"
|
|
10830
10906
|
},
|
|
10907
|
+
"addons.getLastRestart": {
|
|
10908
|
+
capName: "addons",
|
|
10909
|
+
capScope: "system",
|
|
10910
|
+
addonId: null,
|
|
10911
|
+
access: "view"
|
|
10912
|
+
},
|
|
10831
10913
|
"addons.getLogs": {
|
|
10832
10914
|
capName: "addons",
|
|
10833
10915
|
capScope: "system",
|
|
@@ -10864,6 +10946,18 @@ Object.freeze({
|
|
|
10864
10946
|
addonId: null,
|
|
10865
10947
|
access: "view"
|
|
10866
10948
|
},
|
|
10949
|
+
"addons.listCapabilityProviders": {
|
|
10950
|
+
capName: "addons",
|
|
10951
|
+
capScope: "system",
|
|
10952
|
+
addonId: null,
|
|
10953
|
+
access: "view"
|
|
10954
|
+
},
|
|
10955
|
+
"addons.listFrameworkPackages": {
|
|
10956
|
+
capName: "addons",
|
|
10957
|
+
capScope: "system",
|
|
10958
|
+
addonId: null,
|
|
10959
|
+
access: "view"
|
|
10960
|
+
},
|
|
10867
10961
|
"addons.listPackages": {
|
|
10868
10962
|
capName: "addons",
|
|
10869
10963
|
capScope: "system",
|
|
@@ -10942,6 +11036,12 @@ Object.freeze({
|
|
|
10942
11036
|
addonId: null,
|
|
10943
11037
|
access: "delete"
|
|
10944
11038
|
},
|
|
11039
|
+
"addons.updateFrameworkPackage": {
|
|
11040
|
+
capName: "addons",
|
|
11041
|
+
capScope: "system",
|
|
11042
|
+
addonId: null,
|
|
11043
|
+
access: "create"
|
|
11044
|
+
},
|
|
10945
11045
|
"addons.updatePackage": {
|
|
10946
11046
|
capName: "addons",
|
|
10947
11047
|
capScope: "system",
|
|
@@ -14049,32 +14149,98 @@ var TailscaleCliError = class extends Error {
|
|
|
14049
14149
|
this.name = "TailscaleCliError";
|
|
14050
14150
|
}
|
|
14051
14151
|
};
|
|
14152
|
+
var NOOP_LOGGER = {
|
|
14153
|
+
info: () => void 0,
|
|
14154
|
+
warn: () => void 0,
|
|
14155
|
+
error: () => void 0,
|
|
14156
|
+
debug: () => void 0,
|
|
14157
|
+
child: () => NOOP_LOGGER,
|
|
14158
|
+
withTags: () => NOOP_LOGGER
|
|
14159
|
+
};
|
|
14052
14160
|
var TailscaleCli = class {
|
|
14053
14161
|
resolvedBin = null;
|
|
14162
|
+
logger;
|
|
14163
|
+
binPathOverride;
|
|
14164
|
+
socketPath;
|
|
14165
|
+
constructor(opts = {}) {
|
|
14166
|
+
if (typeof opts.info === "function" && !("logger" in opts)) {
|
|
14167
|
+
this.logger = opts;
|
|
14168
|
+
this.binPathOverride = void 0;
|
|
14169
|
+
this.socketPath = void 0;
|
|
14170
|
+
} else {
|
|
14171
|
+
const o = opts;
|
|
14172
|
+
this.logger = o.logger ?? NOOP_LOGGER;
|
|
14173
|
+
this.binPathOverride = o.binPath;
|
|
14174
|
+
this.socketPath = o.socketPath;
|
|
14175
|
+
}
|
|
14176
|
+
}
|
|
14177
|
+
/** Prepend `--socket=<path>` when driving an onboard daemon. */
|
|
14178
|
+
withSocket(args) {
|
|
14179
|
+
return this.socketPath ? [`--socket=${this.socketPath}`, ...args] : [...args];
|
|
14180
|
+
}
|
|
14054
14181
|
/** Locate the `tailscale` binary once and cache the result. */
|
|
14055
14182
|
async resolveBin() {
|
|
14056
14183
|
if (this.resolvedBin) return this.resolvedBin;
|
|
14184
|
+
if (this.binPathOverride) {
|
|
14185
|
+
this.resolvedBin = this.binPathOverride;
|
|
14186
|
+
this.logger.info("CLI binary (onboard override)", { meta: {
|
|
14187
|
+
bin: this.binPathOverride,
|
|
14188
|
+
socketPath: this.socketPath
|
|
14189
|
+
} });
|
|
14190
|
+
return this.binPathOverride;
|
|
14191
|
+
}
|
|
14192
|
+
const tried = [];
|
|
14057
14193
|
for (const candidate of TAILSCALE_CANDIDATES) try {
|
|
14058
14194
|
await execFileP(candidate, ["version"], { timeout: 3e3 });
|
|
14059
14195
|
this.resolvedBin = candidate;
|
|
14196
|
+
this.logger.info("CLI binary resolved", { meta: {
|
|
14197
|
+
bin: candidate,
|
|
14198
|
+
candidatesTried: tried
|
|
14199
|
+
} });
|
|
14060
14200
|
return candidate;
|
|
14061
|
-
} catch {
|
|
14201
|
+
} catch {
|
|
14202
|
+
tried.push(candidate);
|
|
14203
|
+
}
|
|
14062
14204
|
throw new TailscaleCliError("tailscale binary not found — install Tailscale from https://tailscale.com/download");
|
|
14063
14205
|
}
|
|
14064
14206
|
async version() {
|
|
14065
|
-
const { stdout } = await execFileP(await this.resolveBin(), ["version"], { timeout: 5e3 });
|
|
14207
|
+
const { stdout } = await execFileP(await this.resolveBin(), this.withSocket(["version"]), { timeout: 5e3 });
|
|
14066
14208
|
return stdout.trim().split("\n")[0] ?? "";
|
|
14067
14209
|
}
|
|
14068
14210
|
async status() {
|
|
14069
14211
|
const bin = await this.resolveBin();
|
|
14070
14212
|
try {
|
|
14071
|
-
const { stdout } = await execFileP(bin, ["status", "--json"], { timeout: 1e4 });
|
|
14213
|
+
const { stdout } = await execFileP(bin, this.withSocket(["status", "--json"]), { timeout: 1e4 });
|
|
14072
14214
|
return JSON.parse(stdout);
|
|
14073
14215
|
} catch (err) {
|
|
14074
14216
|
const e = err;
|
|
14075
14217
|
throw new TailscaleCliError(`tailscale status failed: ${e.message}`, e.stderr ?? "");
|
|
14076
14218
|
}
|
|
14077
14219
|
}
|
|
14220
|
+
/**
|
|
14221
|
+
* Whether the tailnet ACL policy grants THIS host the Funnel
|
|
14222
|
+
* attribute. Reads `Self.CapMap` / `Self.Capabilities` from
|
|
14223
|
+
* `tailscale status --json` — a pre-flight check so the addon can
|
|
14224
|
+
* fail fast with an actionable error BEFORE spending 15s on a
|
|
14225
|
+
* `tailscale funnel` call that the daemon would reject anyway.
|
|
14226
|
+
*
|
|
14227
|
+
* The funnel grant surfaces as a bare `funnel` key in the modern
|
|
14228
|
+
* CapMap; the legacy `Capabilities` string array carries either
|
|
14229
|
+
* `funnel` or a `.../cap/funnel` URL form.
|
|
14230
|
+
*/
|
|
14231
|
+
async funnelCapable() {
|
|
14232
|
+
const s = await this.status();
|
|
14233
|
+
const capMap = s.Self?.CapMap ?? {};
|
|
14234
|
+
if (Object.prototype.hasOwnProperty.call(capMap, "funnel")) return true;
|
|
14235
|
+
const legacy = s.Self?.Capabilities ?? [];
|
|
14236
|
+
const capable = legacy.some((c) => c === "funnel" || c.endsWith("/cap/funnel"));
|
|
14237
|
+
this.logger.info("funnelCapable check", { meta: {
|
|
14238
|
+
capable,
|
|
14239
|
+
capMapKeys: Object.keys(capMap),
|
|
14240
|
+
legacyCount: legacy.length
|
|
14241
|
+
} });
|
|
14242
|
+
return capable;
|
|
14243
|
+
}
|
|
14078
14244
|
/** Bring the daemon up with an auth key. Idempotent — calling
|
|
14079
14245
|
* while already joined returns immediately. */
|
|
14080
14246
|
async up(input) {
|
|
@@ -14087,7 +14253,7 @@ var TailscaleCli = class {
|
|
|
14087
14253
|
];
|
|
14088
14254
|
if (input.hostname) args.push(`--hostname=${input.hostname}`);
|
|
14089
14255
|
try {
|
|
14090
|
-
await execFileP(bin, args, { timeout: 6e4 });
|
|
14256
|
+
await execFileP(bin, this.withSocket(args), { timeout: 6e4 });
|
|
14091
14257
|
} catch (err) {
|
|
14092
14258
|
const e = err;
|
|
14093
14259
|
throw new TailscaleCliError(`tailscale up failed: ${e.message}`, e.stderr ?? "");
|
|
@@ -14098,7 +14264,7 @@ var TailscaleCli = class {
|
|
|
14098
14264
|
async down() {
|
|
14099
14265
|
const bin = await this.resolveBin();
|
|
14100
14266
|
try {
|
|
14101
|
-
await execFileP(bin, ["down"], { timeout: 15e3 });
|
|
14267
|
+
await execFileP(bin, this.withSocket(["down"]), { timeout: 15e3 });
|
|
14102
14268
|
} catch (err) {
|
|
14103
14269
|
const e = err;
|
|
14104
14270
|
throw new TailscaleCliError(`tailscale down failed: ${e.message}`, e.stderr ?? "");
|
|
@@ -14114,11 +14280,19 @@ var TailscaleCli = class {
|
|
|
14114
14280
|
async serve(input) {
|
|
14115
14281
|
const bin = await this.resolveBin();
|
|
14116
14282
|
const args = this.buildIngressArgs("serve", input);
|
|
14283
|
+
this.logger.info("serve: invoking CLI", { meta: {
|
|
14284
|
+
bin,
|
|
14285
|
+
args,
|
|
14286
|
+
...input
|
|
14287
|
+
} });
|
|
14117
14288
|
try {
|
|
14118
|
-
await execFileP(bin, args, { timeout: 15e3 });
|
|
14289
|
+
const { stdout, stderr } = await execFileP(bin, this.withSocket(args), { timeout: 15e3 });
|
|
14290
|
+
this.logger.info("serve: CLI returned", { meta: {
|
|
14291
|
+
stdout: trimForLog(stdout),
|
|
14292
|
+
stderr: trimForLog(stderr)
|
|
14293
|
+
} });
|
|
14119
14294
|
} catch (err) {
|
|
14120
|
-
|
|
14121
|
-
throw new TailscaleCliError(`tailscale serve failed: ${e.message}`, e.stderr ?? "");
|
|
14295
|
+
throw this.wrapCliError("tailscale serve", err, bin, args);
|
|
14122
14296
|
}
|
|
14123
14297
|
}
|
|
14124
14298
|
/** `tailscale funnel` — exposes a local port to the open internet
|
|
@@ -14127,14 +14301,52 @@ var TailscaleCli = class {
|
|
|
14127
14301
|
async funnel(input) {
|
|
14128
14302
|
const bin = await this.resolveBin();
|
|
14129
14303
|
const args = this.buildIngressArgs("funnel", input);
|
|
14304
|
+
this.logger.info("funnel: invoking CLI", { meta: {
|
|
14305
|
+
bin,
|
|
14306
|
+
args,
|
|
14307
|
+
...input
|
|
14308
|
+
} });
|
|
14130
14309
|
try {
|
|
14131
|
-
await execFileP(bin, args, { timeout: 15e3 });
|
|
14310
|
+
const { stdout, stderr } = await execFileP(bin, this.withSocket(args), { timeout: 15e3 });
|
|
14311
|
+
this.logger.info("funnel: CLI returned", { meta: {
|
|
14312
|
+
stdout: trimForLog(stdout),
|
|
14313
|
+
stderr: trimForLog(stderr)
|
|
14314
|
+
} });
|
|
14132
14315
|
} catch (err) {
|
|
14133
|
-
|
|
14134
|
-
throw new TailscaleCliError(`tailscale funnel failed: ${e.message}`, e.stderr ?? "");
|
|
14316
|
+
throw this.wrapCliError("tailscale funnel", err, bin, args);
|
|
14135
14317
|
}
|
|
14136
14318
|
}
|
|
14137
14319
|
/**
|
|
14320
|
+
* Build a TailscaleCliError that propagates BOTH stdout and stderr
|
|
14321
|
+
* up to the caller. The tailscale CLI is inconsistent about which
|
|
14322
|
+
* stream it uses for human-readable errors — `funnel` (and at least
|
|
14323
|
+
* some `serve` paths) writes the actionable hint to STDOUT when
|
|
14324
|
+
* the host lacks the ACL grant (e.g. "Funnel is not enabled on
|
|
14325
|
+
* your tailnet. To enable, visit: https://login.tailscale.com/...").
|
|
14326
|
+
* Reading only stderr loses that hint and the operator sees a bare
|
|
14327
|
+
* "Command failed: …" with no recovery path.
|
|
14328
|
+
*
|
|
14329
|
+
* Special-cases the "Funnel is not enabled" outcome: extracts the
|
|
14330
|
+
* enable-URL the CLI prints and surfaces a concise, single-line
|
|
14331
|
+
* actionable error instead of dumping the raw multi-line blob.
|
|
14332
|
+
*/
|
|
14333
|
+
wrapCliError(verb, err, bin, args) {
|
|
14334
|
+
const e = err;
|
|
14335
|
+
const stderr = (e.stderr ?? "").trim();
|
|
14336
|
+
const stdout = (e.stdout ?? "").trim();
|
|
14337
|
+
this.logger.error(`${verb}: CLI failed`, { meta: {
|
|
14338
|
+
bin,
|
|
14339
|
+
args,
|
|
14340
|
+
message: e.message,
|
|
14341
|
+
stderr: trimForLog(stderr),
|
|
14342
|
+
stdout: trimForLog(stdout)
|
|
14343
|
+
} });
|
|
14344
|
+
const combined = `${stdout}\n${stderr}`;
|
|
14345
|
+
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);
|
|
14346
|
+
const detail = stderr.length > 0 ? `--- stderr ---\n${stderr}` : stdout.length > 0 ? `--- stdout ---\n${stdout}` : "";
|
|
14347
|
+
return new TailscaleCliError(`${verb} failed: ${e.message.trim()}${detail ? `\n${detail}` : ""}`, stderr.length > 0 ? stderr : stdout);
|
|
14348
|
+
}
|
|
14349
|
+
/**
|
|
14138
14350
|
* Single source of truth for the serve/funnel arg shape. The CLI
|
|
14139
14351
|
* accepts the same flag layout for both verbs, so we share the
|
|
14140
14352
|
* builder. `--set-path` is only emitted when the operator picks a
|
|
@@ -14150,10 +14362,17 @@ var TailscaleCli = class {
|
|
|
14150
14362
|
}
|
|
14151
14363
|
const out = [verb, "--bg"];
|
|
14152
14364
|
if (path && path !== "/") out.push(`--set-path=${path}`);
|
|
14153
|
-
|
|
14365
|
+
const scheme = input.upstreamScheme ?? "https+insecure";
|
|
14366
|
+
out.push(`${scheme}://127.0.0.1:${input.port}`);
|
|
14154
14367
|
return out;
|
|
14155
14368
|
}
|
|
14156
14369
|
};
|
|
14370
|
+
/** Truncate long stdout/stderr blobs so the log entry stays readable. */
|
|
14371
|
+
function trimForLog(s, max = 800) {
|
|
14372
|
+
const trimmed = s.trim();
|
|
14373
|
+
if (trimmed.length <= max) return trimmed;
|
|
14374
|
+
return `${trimmed.slice(0, max)}…[+${trimmed.length - max} bytes]`;
|
|
14375
|
+
}
|
|
14157
14376
|
//#endregion
|
|
14158
14377
|
//#region src/tailscale-ingress.addon.ts
|
|
14159
14378
|
/**
|
|
@@ -14181,10 +14400,11 @@ var TailscaleCli = class {
|
|
|
14181
14400
|
var DEFAULT_CONFIG = {
|
|
14182
14401
|
mode: "serve",
|
|
14183
14402
|
sourcePort: 4443,
|
|
14184
|
-
targetPath: ""
|
|
14403
|
+
targetPath: "",
|
|
14404
|
+
upstreamScheme: "https+insecure"
|
|
14185
14405
|
};
|
|
14186
14406
|
var TailscaleIngressAddon = class extends BaseAddon {
|
|
14187
|
-
cli
|
|
14407
|
+
cli;
|
|
14188
14408
|
/**
|
|
14189
14409
|
* Snapshot of the running ingress params so stop()/disposer can issue
|
|
14190
14410
|
* the matching `off` call even if `this.config` mutates mid-flight.
|
|
@@ -14197,6 +14417,7 @@ var TailscaleIngressAddon = class extends BaseAddon {
|
|
|
14197
14417
|
super({ ...DEFAULT_CONFIG });
|
|
14198
14418
|
}
|
|
14199
14419
|
async onInitialize() {
|
|
14420
|
+
this.cli = new TailscaleCli(this.ctx.logger.child("tailscale-cli"));
|
|
14200
14421
|
try {
|
|
14201
14422
|
await this.cli.version();
|
|
14202
14423
|
} catch (err) {
|
|
@@ -14222,17 +14443,27 @@ var TailscaleIngressAddon = class extends BaseAddon {
|
|
|
14222
14443
|
topic: "tailscale-ingress",
|
|
14223
14444
|
phase: "verify-client"
|
|
14224
14445
|
} });
|
|
14225
|
-
await this.requireClientJoined();
|
|
14446
|
+
const meshStatus = await this.requireClientJoined();
|
|
14447
|
+
this.syncCliToDaemon(meshStatus);
|
|
14448
|
+
if (this.config.mode === "funnel") {
|
|
14449
|
+
log.info("start: phase=verify-funnel-grant", { tags: {
|
|
14450
|
+
topic: "tailscale-ingress",
|
|
14451
|
+
phase: "verify-funnel-grant"
|
|
14452
|
+
} });
|
|
14453
|
+
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.");
|
|
14454
|
+
}
|
|
14226
14455
|
const next = {
|
|
14227
14456
|
mode: this.config.mode,
|
|
14228
14457
|
sourcePort: this.config.sourcePort,
|
|
14229
|
-
targetPath: this.config.targetPath
|
|
14458
|
+
targetPath: this.config.targetPath,
|
|
14459
|
+
upstreamScheme: this.config.upstreamScheme
|
|
14230
14460
|
};
|
|
14231
14461
|
log.info("start: phase=apply-rule", {
|
|
14232
14462
|
meta: {
|
|
14233
14463
|
mode: next.mode,
|
|
14234
14464
|
sourcePort: next.sourcePort,
|
|
14235
|
-
targetPath: next.targetPath
|
|
14465
|
+
targetPath: next.targetPath,
|
|
14466
|
+
upstreamScheme: next.upstreamScheme
|
|
14236
14467
|
},
|
|
14237
14468
|
tags: {
|
|
14238
14469
|
topic: "tailscale-ingress",
|
|
@@ -14372,10 +14603,27 @@ var TailscaleIngressAddon = class extends BaseAddon {
|
|
|
14372
14603
|
const opts = {
|
|
14373
14604
|
port: rule.sourcePort,
|
|
14374
14605
|
enabled,
|
|
14606
|
+
upstreamScheme: rule.upstreamScheme,
|
|
14375
14607
|
...rule.targetPath && rule.targetPath !== "/" ? { targetPath: rule.targetPath } : {}
|
|
14376
14608
|
};
|
|
14609
|
+
this.ctx.logger.info(`applyRule: ${rule.mode}`, {
|
|
14610
|
+
meta: {
|
|
14611
|
+
...opts,
|
|
14612
|
+
mode: rule.mode
|
|
14613
|
+
},
|
|
14614
|
+
tags: {
|
|
14615
|
+
topic: "tailscale-ingress",
|
|
14616
|
+
phase: "apply-rule",
|
|
14617
|
+
verb: rule.mode
|
|
14618
|
+
}
|
|
14619
|
+
});
|
|
14377
14620
|
if (rule.mode === "funnel") await this.cli.funnel(opts);
|
|
14378
14621
|
else await this.cli.serve(opts);
|
|
14622
|
+
this.ctx.logger.info(`applyRule: ${rule.mode} done`, { tags: {
|
|
14623
|
+
topic: "tailscale-ingress",
|
|
14624
|
+
phase: "apply-rule-done",
|
|
14625
|
+
verb: rule.mode
|
|
14626
|
+
} });
|
|
14379
14627
|
}
|
|
14380
14628
|
/**
|
|
14381
14629
|
* Build the canonical endpoint from the active ingress + the
|
|
@@ -14405,15 +14653,65 @@ var TailscaleIngressAddon = class extends BaseAddon {
|
|
|
14405
14653
|
/**
|
|
14406
14654
|
* Wait for `mesh-network` (the tailscale-client provider) to be
|
|
14407
14655
|
* mounted AND report joined. Throws an actionable error otherwise.
|
|
14656
|
+
*
|
|
14657
|
+
* IMPORTANT: we route the check through the API surface
|
|
14658
|
+
* (`api.meshNetwork.getStatus.query({ addonId: 'tailscale-client' })`),
|
|
14659
|
+
* NOT through `ctx.capabilities.getCollectionEntries`. The latter
|
|
14660
|
+
* only sees providers registered in THIS process's local
|
|
14661
|
+
* CapabilityRegistry — and tailscale-client runs in a separate
|
|
14662
|
+
* `hub/tailscale-client` group runner. The API surface is
|
|
14663
|
+
* cross-process via Moleculer, so it can find the provider wherever
|
|
14664
|
+
* it lives.
|
|
14665
|
+
*
|
|
14666
|
+
* `mesh-network` is a `collection` cap (Tailscale + Headscale +
|
|
14667
|
+
* ZeroTier could all implement it in parallel), so we pin to
|
|
14668
|
+
* `tailscale-client` via the `addonId` filter — without it the API
|
|
14669
|
+
* would route to whichever provider registered first and we'd risk
|
|
14670
|
+
* emitting `tailscale serve` against e.g. a Headscale daemon that
|
|
14671
|
+
* doesn't know that verb.
|
|
14408
14672
|
*/
|
|
14409
14673
|
async requireClientJoined() {
|
|
14410
|
-
|
|
14411
|
-
|
|
14674
|
+
let status;
|
|
14675
|
+
try {
|
|
14676
|
+
status = await this.fetchMeshStatus({ addonId: "tailscale-client" });
|
|
14677
|
+
} catch (err) {
|
|
14678
|
+
throw new Error("tailscale-ingress: tailscale-client provider not registered — install + start the @camstack/addon-tailscale-client addon first.", { cause: err });
|
|
14679
|
+
}
|
|
14680
|
+
if (!status.joined) throw new Error("tailscale-ingress: tailnet not joined — open the Mesh Networks page and click \"Connect to Tailscale\" first.");
|
|
14681
|
+
return status;
|
|
14412
14682
|
}
|
|
14413
|
-
|
|
14683
|
+
/**
|
|
14684
|
+
* Rebuild `this.cli` to drive the tailscale-client's daemon. When
|
|
14685
|
+
* the client runs in `onboard` mode it spawned a private
|
|
14686
|
+
* `tailscaled`; the mesh status then carries `daemonSocket` +
|
|
14687
|
+
* `daemonCliPath` and we MUST route serve/funnel through that
|
|
14688
|
+
* socket — the system daemon is a different node with a different
|
|
14689
|
+
* mesh identity. Host mode leaves both unset → default CLI.
|
|
14690
|
+
*/
|
|
14691
|
+
syncCliToDaemon(status) {
|
|
14692
|
+
const cliLogger = this.ctx.logger.child("tailscale-cli");
|
|
14693
|
+
if (status.daemonSocket) {
|
|
14694
|
+
this.ctx.logger.info("start: routing CLI to onboard daemon", {
|
|
14695
|
+
meta: {
|
|
14696
|
+
daemonSocket: status.daemonSocket,
|
|
14697
|
+
daemonCliPath: status.daemonCliPath
|
|
14698
|
+
},
|
|
14699
|
+
tags: {
|
|
14700
|
+
topic: "tailscale-ingress",
|
|
14701
|
+
phase: "daemon-route"
|
|
14702
|
+
}
|
|
14703
|
+
});
|
|
14704
|
+
this.cli = new TailscaleCli({
|
|
14705
|
+
logger: cliLogger,
|
|
14706
|
+
socketPath: status.daemonSocket,
|
|
14707
|
+
...status.daemonCliPath ? { binPath: status.daemonCliPath } : {}
|
|
14708
|
+
});
|
|
14709
|
+
} else this.cli = new TailscaleCli({ logger: cliLogger });
|
|
14710
|
+
}
|
|
14711
|
+
async fetchMeshStatus(filter) {
|
|
14414
14712
|
const api = this.ctx.api;
|
|
14415
14713
|
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();
|
|
14714
|
+
return await api.meshNetwork.getStatus.query(filter);
|
|
14417
14715
|
}
|
|
14418
14716
|
globalSettingsSchema() {
|
|
14419
14717
|
return this.schema({ sections: [{
|
|
@@ -14453,6 +14751,27 @@ var TailscaleIngressAddon = class extends BaseAddon {
|
|
|
14453
14751
|
description: "Public-side mount path. Leave empty for root.",
|
|
14454
14752
|
default: DEFAULT_CONFIG.targetPath,
|
|
14455
14753
|
placeholder: "/"
|
|
14754
|
+
}),
|
|
14755
|
+
this.field({
|
|
14756
|
+
type: "select",
|
|
14757
|
+
key: "upstreamScheme",
|
|
14758
|
+
label: "Upstream scheme",
|
|
14759
|
+
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).",
|
|
14760
|
+
default: DEFAULT_CONFIG.upstreamScheme,
|
|
14761
|
+
options: [
|
|
14762
|
+
{
|
|
14763
|
+
value: "https+insecure",
|
|
14764
|
+
label: "HTTPS, self-signed (default)"
|
|
14765
|
+
},
|
|
14766
|
+
{
|
|
14767
|
+
value: "https",
|
|
14768
|
+
label: "HTTPS, valid cert"
|
|
14769
|
+
},
|
|
14770
|
+
{
|
|
14771
|
+
value: "http",
|
|
14772
|
+
label: "HTTP (only if hub TLS disabled)"
|
|
14773
|
+
}
|
|
14774
|
+
]
|
|
14456
14775
|
})
|
|
14457
14776
|
]
|
|
14458
14777
|
}] });
|