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