homebridge-plugin-utils 1.34.0 → 2.0.0
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.
- package/README.md +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +128 -0
- package/dist/backpressure.js +273 -0
- package/dist/backpressure.js.map +1 -0
- package/dist/cli/index.d.ts +95 -0
- package/dist/cli/index.js +355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -257
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +70 -4
- package/dist/ffmpeg/fmp4.js +100 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +143 -91
- package/dist/ffmpeg/process.js +406 -265
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -560
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +473 -244
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -1
- package/dist/ffmpeg/settings.js +20 -2
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -148
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +597 -45
- package/dist/util.js +787 -68
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Nullable } from "../util.ts";
|
|
2
|
+
export declare const DEFAULT_CONFIG_FILENAME = ".hblog.json";
|
|
3
|
+
/**
|
|
4
|
+
* The shape of the optional `~/.hblog.json` config file. Every field is optional; unknown keys in the file are ignored. The file deliberately carries no `otp` field - a
|
|
5
|
+
* one-time passcode is, by definition, single-use and time-bound, so it only ever comes from the `--otp` flag or the `HBLOG_OTP` environment variable, never from a
|
|
6
|
+
* persisted file.
|
|
7
|
+
*
|
|
8
|
+
* @property host - The hostname or IP of the homebridge-config-ui-x server.
|
|
9
|
+
* @property password - The account password, for username/password authentication.
|
|
10
|
+
* @property port - The TCP port the server listens on.
|
|
11
|
+
* @property tls - Whether to use the secure (`https`/`wss`) schemes.
|
|
12
|
+
* @property token - A pre-acquired bearer token, used verbatim.
|
|
13
|
+
* @property username - The account username, for username/password authentication.
|
|
14
|
+
*
|
|
15
|
+
* @category Log Client
|
|
16
|
+
*/
|
|
17
|
+
export interface HblogConfigFile {
|
|
18
|
+
readonly host?: string;
|
|
19
|
+
readonly password?: string;
|
|
20
|
+
readonly port?: number;
|
|
21
|
+
readonly tls?: boolean;
|
|
22
|
+
readonly token?: string;
|
|
23
|
+
readonly username?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The environment-variable slice consulted by {@link resolveConnection}. The CLI reads these from `process.env` (`HBLOG_HOST`, `HBLOG_PORT`, `HBLOG_USER`, `HBLOG_PASS`,
|
|
27
|
+
* `HBLOG_TOKEN`, `HBLOG_OTP`) and passes them here as already-extracted values so the resolver itself touches no globals.
|
|
28
|
+
*
|
|
29
|
+
* @property host - The `HBLOG_HOST` value.
|
|
30
|
+
* @property otp - The `HBLOG_OTP` value (a one-time passcode).
|
|
31
|
+
* @property password - The `HBLOG_PASS` value.
|
|
32
|
+
* @property port - The `HBLOG_PORT` value (still a string here; parsed during resolution).
|
|
33
|
+
* @property token - The `HBLOG_TOKEN` value.
|
|
34
|
+
* @property username - The `HBLOG_USER` value.
|
|
35
|
+
*
|
|
36
|
+
* @category Log Client
|
|
37
|
+
*/
|
|
38
|
+
export interface HblogEnv {
|
|
39
|
+
readonly host?: string;
|
|
40
|
+
readonly otp?: string;
|
|
41
|
+
readonly password?: string;
|
|
42
|
+
readonly port?: string;
|
|
43
|
+
readonly token?: string;
|
|
44
|
+
readonly username?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The command-line connection flags consulted by {@link resolveConnection}. These are the parsed `--host`, `--port`, `--tls`, `--user`, `--pass`, `--token`, and `--otp`
|
|
48
|
+
* values. They take the highest precedence in the merge.
|
|
49
|
+
*
|
|
50
|
+
* @property host - The `--host` value.
|
|
51
|
+
* @property otp - The `--otp` value.
|
|
52
|
+
* @property password - The `--pass` value.
|
|
53
|
+
* @property port - The `--port` value (already parsed to a number by the flag parser, or omitted).
|
|
54
|
+
* @property tls - The `--tls` value.
|
|
55
|
+
* @property token - The `--token` value.
|
|
56
|
+
* @property username - The `--user` value.
|
|
57
|
+
*
|
|
58
|
+
* @category Log Client
|
|
59
|
+
*/
|
|
60
|
+
export interface HblogConnectionFlags {
|
|
61
|
+
readonly host?: string;
|
|
62
|
+
readonly otp?: string;
|
|
63
|
+
readonly password?: string;
|
|
64
|
+
readonly port?: number;
|
|
65
|
+
readonly tls?: boolean;
|
|
66
|
+
readonly token?: string;
|
|
67
|
+
readonly username?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The fully-resolved connection produced by {@link resolveConnection}: the connection target plus the credential material the CLI uses to build a
|
|
71
|
+
* {@link logclient/types!LogClientCredentials | LogClientCredentials} discriminated union. `host`, `port`, and `tls` always carry a concrete value (defaults applied);
|
|
72
|
+
* the credential fields are {@link Nullable} because none, some, or all of them may have been supplied across the three sources.
|
|
73
|
+
*
|
|
74
|
+
* @property host - The resolved hostname or IP.
|
|
75
|
+
* @property otp - The resolved one-time passcode, or `null` when none was supplied.
|
|
76
|
+
* @property password - The resolved password, or `null` when none was supplied.
|
|
77
|
+
* @property port - The resolved TCP port.
|
|
78
|
+
* @property tls - The resolved TLS flag.
|
|
79
|
+
* @property token - The resolved bearer token, or `null` when none was supplied.
|
|
80
|
+
* @property username - The resolved username, or `null` when none was supplied.
|
|
81
|
+
*
|
|
82
|
+
* @category Log Client
|
|
83
|
+
*/
|
|
84
|
+
export interface ResolvedConnection {
|
|
85
|
+
readonly host: string;
|
|
86
|
+
readonly otp: Nullable<string>;
|
|
87
|
+
readonly password: Nullable<string>;
|
|
88
|
+
readonly port: number;
|
|
89
|
+
readonly tls: boolean;
|
|
90
|
+
readonly token: Nullable<string>;
|
|
91
|
+
readonly username: Nullable<string>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Options accepted by {@link loadConfigFile}: the injectable filesystem and warning seams. All default to the real Node implementations / a `process.stderr` writer, so a
|
|
95
|
+
* caller (the CLI) can omit them in production and a test can supply doubles.
|
|
96
|
+
*
|
|
97
|
+
* @property readFile - Reads the file's UTF-8 text. Defaults to `node:fs/promises` `readFile`. A rejection whose `code` is `ENOENT` is treated as "file absent."
|
|
98
|
+
* @property stat - Stats the file for its permission mode. Defaults to `node:fs/promises` `stat`. Used only for the group/other-readable security warning.
|
|
99
|
+
* @property warn - Sink for the single one-line security warning. Defaults to a `process.stderr` writer. Injected so a test asserts the warning without capturing
|
|
100
|
+
* real stderr.
|
|
101
|
+
*
|
|
102
|
+
* @category Log Client
|
|
103
|
+
*/
|
|
104
|
+
export interface LoadConfigFileOptions {
|
|
105
|
+
readonly readFile?: (path: string) => Promise<string>;
|
|
106
|
+
readonly stat?: (path: string) => Promise<{
|
|
107
|
+
readonly mode: number;
|
|
108
|
+
}>;
|
|
109
|
+
readonly warn?: (message: string) => void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Load and parse the optional `~/.hblog.json` config file.
|
|
113
|
+
*
|
|
114
|
+
* The file is optional: when it does not exist (ENOENT) this resolves to `undefined` silently. Every other failure is thrown as a clear, actionable error naming the
|
|
115
|
+
* path: a non-ENOENT read failure (a permission or I/O fault), a file that cannot be parsed as JSON, or a file whose top-level value is not a JSON object. Recognized
|
|
116
|
+
* keys ({@link HblogConfigFile}) are picked out by type (a wrong-typed field is ignored, not coerced); any unknown key is ignored. As a security courtesy, if the file's
|
|
117
|
+
* permissions allow group or other access (`mode & 0o077`), a single one-line warning recommending `chmod 600` is emitted through the `warn` seam, because the file may
|
|
118
|
+
* store a password or a long-lived token in plaintext.
|
|
119
|
+
*
|
|
120
|
+
* @param path - The absolute path of the config file to load (the CLI resolves `~/.hblog.json`, or honors `HBLOG_CONFIG`).
|
|
121
|
+
* @param options - The injectable filesystem and warning seams. See {@link LoadConfigFileOptions}.
|
|
122
|
+
*
|
|
123
|
+
* @returns The parsed {@link HblogConfigFile}, or `undefined` when the file is absent (ENOENT).
|
|
124
|
+
*
|
|
125
|
+
* @throws `Error` when the file exists but cannot be read (a non-ENOENT read failure), contains malformed JSON, or whose top-level value is not a JSON object.
|
|
126
|
+
*
|
|
127
|
+
* @category Log Client
|
|
128
|
+
*/
|
|
129
|
+
export declare function loadConfigFile(path: string, options?: LoadConfigFileOptions): Promise<HblogConfigFile | undefined>;
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the absolute path of the config file to load.
|
|
132
|
+
*
|
|
133
|
+
* The `HBLOG_CONFIG` environment variable overrides everything when set to a non-empty value (handy for tests and non-standard layouts); otherwise the default
|
|
134
|
+
* {@link DEFAULT_CONFIG_FILENAME} (`.hblog.json`) under the supplied home directory is used. Home-dir only - there is no project-local config file, so a config carrying
|
|
135
|
+
* a password or token is never tempting to commit alongside a plugin's source. The join uses a single forward slash, which both POSIX and Windows accept in a path passed
|
|
136
|
+
* to `readFile`, so no `node:path` import is needed on this hot setup path.
|
|
137
|
+
*
|
|
138
|
+
* @param sources - The path inputs.
|
|
139
|
+
* @param sources.env - The environment map; only `HBLOG_CONFIG` is consulted.
|
|
140
|
+
* @param sources.homedir - The user's home directory, the anchor for the default config-file path.
|
|
141
|
+
*
|
|
142
|
+
* @returns The absolute path to load the config file from.
|
|
143
|
+
*
|
|
144
|
+
* @category Log Client
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveConfigPath(sources: {
|
|
147
|
+
env: NodeJS.ProcessEnv;
|
|
148
|
+
homedir: string;
|
|
149
|
+
}): string;
|
|
150
|
+
/**
|
|
151
|
+
* Resolve the three configuration sources into a single {@link ResolvedConnection}, applying the precedence flags > environment > file > defaults.
|
|
152
|
+
*
|
|
153
|
+
* This is a PURE function: it reads only its arguments and allocates only the result, performing no I/O. The caller is responsible for having loaded the file (via
|
|
154
|
+
* {@link loadConfigFile}), extracted the environment slice, and parsed the flags. `host`, `port`, and `tls` always resolve to a concrete value (their defaults are
|
|
155
|
+
* `localhost`, `8581`, and `false`); the credential fields resolve to `null` when no source supplied them, leaving the CLI to decide which
|
|
156
|
+
* {@link logclient/types!LogClientCredentials | LogClientCredentials} arm the resolved material implies. The `port` from the environment is a string, so it is
|
|
157
|
+
* parsed here; a non-numeric `HBLOG_PORT` is ignored (falls through to the next source) rather than producing a `NaN` port.
|
|
158
|
+
*
|
|
159
|
+
* @param sources - The three configuration sources.
|
|
160
|
+
* @param sources.env - The environment slice. See {@link HblogEnv}.
|
|
161
|
+
* @param sources.file - The loaded config file, or `undefined` when absent. See {@link HblogConfigFile}.
|
|
162
|
+
* @param sources.flags - The parsed command-line flags. See {@link HblogConnectionFlags}.
|
|
163
|
+
*
|
|
164
|
+
* @returns The fully-resolved connection.
|
|
165
|
+
*
|
|
166
|
+
* @category Log Client
|
|
167
|
+
*/
|
|
168
|
+
export declare function resolveConnection(sources: {
|
|
169
|
+
env: HblogEnv;
|
|
170
|
+
file: HblogConfigFile | undefined;
|
|
171
|
+
flags: HblogConnectionFlags;
|
|
172
|
+
}): ResolvedConnection;
|
|
173
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* logclient/config.ts: CLI-layer config-file loading and pure connection resolution for the hblog tool.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* CLI-layer configuration for the `hblog` tool: loading the optional `~/.hblog.json` file and the pure merge of file / environment / flags into a connection.
|
|
7
|
+
*
|
|
8
|
+
* This module lives at the CLI layer on purpose: the engine ({@link logclient/client!HomebridgeLogClient | HomebridgeLogClient} and its transports) never reads the
|
|
9
|
+
* user's home directory or any config file - all file I/O stays here, behind injectable seams, so the engine is portable and side-effect-free. This module exports the
|
|
10
|
+
* following pieces:
|
|
11
|
+
*
|
|
12
|
+
* - {@link resolveConfigPath} - resolve the absolute config-file path, honoring the `HBLOG_CONFIG` environment override over the home-directory default
|
|
13
|
+
* ({@link DEFAULT_CONFIG_FILENAME}).
|
|
14
|
+
* - {@link loadConfigFile} - read and parse the optional `~/.hblog.json`. A missing file resolves to `undefined` silently (the file is optional); a malformed file raises
|
|
15
|
+
* a clear, actionable error; unknown keys are ignored; and a file whose permissions are group/other-readable triggers a single one-line warning recommending `chmod
|
|
16
|
+
* 600`, because the file may carry a password or a long-lived token in plaintext. The `readFile`, `stat`, and `warn` seams are injected so the loader is unit-tested
|
|
17
|
+
* without touching the real filesystem.
|
|
18
|
+
* - {@link resolveConnection} - a PURE merge of the configuration sources into one resolved connection, applying the precedence flags > environment > file > defaults.
|
|
19
|
+
* It performs no I/O of its own; the caller supplies the already-loaded file, the environment slice, and the parsed flags.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
import { DEFAULT_HOST, DEFAULT_PORT } from "./settings.js";
|
|
24
|
+
import { formatErrorMessage } from "../util.js";
|
|
25
|
+
// The default path of the optional config file, relative to the user's home directory. Home-dir only (no project-local file) so a config carrying a password or token is
|
|
26
|
+
// never tempting to commit alongside a plugin's source. The CLI resolves this against the real home directory; the `HBLOG_CONFIG` environment variable overrides it.
|
|
27
|
+
export const DEFAULT_CONFIG_FILENAME = ".hblog.json";
|
|
28
|
+
// The permission mask that flags a config file as too permissive: any group or other read/write/execute bit set. A file carrying a plaintext credential should be owner-
|
|
29
|
+
// only (mode 600); when any of these bits is present we warn once and recommend `chmod 600`. We mask the raw `mode` (which carries file-type bits in its high bits) with
|
|
30
|
+
// this so only the group/other permission triplet is consulted.
|
|
31
|
+
const GROUP_OTHER_MASK = 0o077;
|
|
32
|
+
// The default `readFile` seam: read the file as UTF-8 text via `node:fs/promises`. Imported lazily inside the default so a caller that always injects its own seam pays
|
|
33
|
+
// no `node:fs/promises` import cost, and the engine-adjacent module stays free of an unconditional Node filesystem dependency at load time.
|
|
34
|
+
async function defaultReadFile(path) {
|
|
35
|
+
const { readFile } = await import("node:fs/promises");
|
|
36
|
+
return readFile(path, "utf8");
|
|
37
|
+
}
|
|
38
|
+
// The default `stat` seam: stat the file via `node:fs/promises`, narrowing to just the `mode` field the permission check needs. Lazily imported for the same reason as
|
|
39
|
+
// the read seam.
|
|
40
|
+
async function defaultStat(path) {
|
|
41
|
+
const { stat } = await import("node:fs/promises");
|
|
42
|
+
const stats = await stat(path);
|
|
43
|
+
return { mode: stats.mode };
|
|
44
|
+
}
|
|
45
|
+
// Test whether a thrown filesystem error denotes a missing file. Node's `readFile`/`stat` reject with an error carrying `code: "ENOENT"` when the path does not exist;
|
|
46
|
+
// that is the silent "no config file" case rather than a failure. We read `code` defensively off an unknown error shape.
|
|
47
|
+
function isFileNotFound(error) {
|
|
48
|
+
return (typeof error === "object") && (error !== null) && ("code" in error) && (error.code === "ENOENT");
|
|
49
|
+
}
|
|
50
|
+
// Narrow an unknown parsed JSON value to a plain record (a JSON object) so individual fields can be read without unsafe member access. An array is excluded: a config
|
|
51
|
+
// file whose top level is an array (or any non-object) is malformed, so it must not be treated as a record.
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return (typeof value === "object") && (value !== null) && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
// Read one optional string field off the parsed config record. A present non-string value is ignored (treated as absent) rather than coerced, so a malformed field type
|
|
56
|
+
// degrades to "unset" rather than smuggling a number/boolean into a string slot.
|
|
57
|
+
function readString(record, key) {
|
|
58
|
+
const value = record[key];
|
|
59
|
+
return (typeof value === "string") ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
// Read the named optional numeric field off the parsed config record. A present non-number value is ignored (treated as absent) rather than coerced.
|
|
62
|
+
function readNumber(record, key) {
|
|
63
|
+
const value = record[key];
|
|
64
|
+
return (typeof value === "number") ? value : undefined;
|
|
65
|
+
}
|
|
66
|
+
// Read the named optional boolean field off the parsed config record. A present non-boolean value is ignored (treated as absent) rather than coerced.
|
|
67
|
+
function readBoolean(record, key) {
|
|
68
|
+
const value = record[key];
|
|
69
|
+
return (typeof value === "boolean") ? value : undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Load and parse the optional `~/.hblog.json` config file.
|
|
73
|
+
*
|
|
74
|
+
* The file is optional: when it does not exist (ENOENT) this resolves to `undefined` silently. Every other failure is thrown as a clear, actionable error naming the
|
|
75
|
+
* path: a non-ENOENT read failure (a permission or I/O fault), a file that cannot be parsed as JSON, or a file whose top-level value is not a JSON object. Recognized
|
|
76
|
+
* keys ({@link HblogConfigFile}) are picked out by type (a wrong-typed field is ignored, not coerced); any unknown key is ignored. As a security courtesy, if the file's
|
|
77
|
+
* permissions allow group or other access (`mode & 0o077`), a single one-line warning recommending `chmod 600` is emitted through the `warn` seam, because the file may
|
|
78
|
+
* store a password or a long-lived token in plaintext.
|
|
79
|
+
*
|
|
80
|
+
* @param path - The absolute path of the config file to load (the CLI resolves `~/.hblog.json`, or honors `HBLOG_CONFIG`).
|
|
81
|
+
* @param options - The injectable filesystem and warning seams. See {@link LoadConfigFileOptions}.
|
|
82
|
+
*
|
|
83
|
+
* @returns The parsed {@link HblogConfigFile}, or `undefined` when the file is absent (ENOENT).
|
|
84
|
+
*
|
|
85
|
+
* @throws `Error` when the file exists but cannot be read (a non-ENOENT read failure), contains malformed JSON, or whose top-level value is not a JSON object.
|
|
86
|
+
*
|
|
87
|
+
* @category Log Client
|
|
88
|
+
*/
|
|
89
|
+
export async function loadConfigFile(path, options = {}) {
|
|
90
|
+
const readFile = options.readFile ?? defaultReadFile;
|
|
91
|
+
const stat = options.stat ?? defaultStat;
|
|
92
|
+
const warn = options.warn ?? ((message) => { process.stderr.write(message + "\n"); });
|
|
93
|
+
let text;
|
|
94
|
+
try {
|
|
95
|
+
text = await readFile(path);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// A missing file is the silent, expected "no config" case; any other read failure (a permission error, an I/O fault) is a genuine problem the user should see.
|
|
99
|
+
if (isFileNotFound(error)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
throw new Error("Unable to read the config file " + path + ": " + formatErrorMessage(error) + ".");
|
|
103
|
+
}
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(text);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error("The config file " + path + " is not valid JSON: " + formatErrorMessage(error) + ". Fix the file or remove it.");
|
|
110
|
+
}
|
|
111
|
+
if (!isRecord(parsed)) {
|
|
112
|
+
throw new Error("The config file " + path + " must contain a JSON object. Fix the file or remove it.");
|
|
113
|
+
}
|
|
114
|
+
// Stat the file for its permission mode and warn once if it is group/other-readable. A stat failure here is non-fatal: the file parsed fine, so we skip the security
|
|
115
|
+
// courtesy rather than fail the load over an inability to read permissions (e.g., on a filesystem that does not report a meaningful mode).
|
|
116
|
+
try {
|
|
117
|
+
const stats = await stat(path);
|
|
118
|
+
if ((stats.mode & GROUP_OTHER_MASK) !== 0) {
|
|
119
|
+
warn("Warning: the config file " + path + " is readable by other users; it may contain credentials. Run `chmod 600 " + path + "` to restrict it to your account.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Permissions could not be determined; skip the security warning rather than fail an otherwise-valid load.
|
|
124
|
+
}
|
|
125
|
+
// Pick out the recognized keys by type; unknown keys and wrong-typed values are ignored. Building a fresh object (rather than returning `parsed`) keeps the result
|
|
126
|
+
// exactly the declared shape and drops any extraneous keys the file carried.
|
|
127
|
+
return {
|
|
128
|
+
host: readString(parsed, "host"),
|
|
129
|
+
password: readString(parsed, "password"),
|
|
130
|
+
port: readNumber(parsed, "port"),
|
|
131
|
+
tls: readBoolean(parsed, "tls"),
|
|
132
|
+
token: readString(parsed, "token"),
|
|
133
|
+
username: readString(parsed, "username")
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the absolute path of the config file to load.
|
|
138
|
+
*
|
|
139
|
+
* The `HBLOG_CONFIG` environment variable overrides everything when set to a non-empty value (handy for tests and non-standard layouts); otherwise the default
|
|
140
|
+
* {@link DEFAULT_CONFIG_FILENAME} (`.hblog.json`) under the supplied home directory is used. Home-dir only - there is no project-local config file, so a config carrying
|
|
141
|
+
* a password or token is never tempting to commit alongside a plugin's source. The join uses a single forward slash, which both POSIX and Windows accept in a path passed
|
|
142
|
+
* to `readFile`, so no `node:path` import is needed on this hot setup path.
|
|
143
|
+
*
|
|
144
|
+
* @param sources - The path inputs.
|
|
145
|
+
* @param sources.env - The environment map; only `HBLOG_CONFIG` is consulted.
|
|
146
|
+
* @param sources.homedir - The user's home directory, the anchor for the default config-file path.
|
|
147
|
+
*
|
|
148
|
+
* @returns The absolute path to load the config file from.
|
|
149
|
+
*
|
|
150
|
+
* @category Log Client
|
|
151
|
+
*/
|
|
152
|
+
export function resolveConfigPath(sources) {
|
|
153
|
+
const override = sources.env["HBLOG_CONFIG"];
|
|
154
|
+
if ((override !== undefined) && (override.length > 0)) {
|
|
155
|
+
return override;
|
|
156
|
+
}
|
|
157
|
+
return sources.homedir.replace(/[/\\]+$/, "") + "/" + DEFAULT_CONFIG_FILENAME;
|
|
158
|
+
}
|
|
159
|
+
// Resolve one optional string across the three sources in precedence order (flags > env > file), returning `null` when none supplied. The first defined value wins; an
|
|
160
|
+
// empty string is a defined value (the user explicitly passed it) so it is honored rather than skipped.
|
|
161
|
+
function pickString(flag, env, file) {
|
|
162
|
+
return flag ?? env ?? file ?? null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Resolve the three configuration sources into a single {@link ResolvedConnection}, applying the precedence flags > environment > file > defaults.
|
|
166
|
+
*
|
|
167
|
+
* This is a PURE function: it reads only its arguments and allocates only the result, performing no I/O. The caller is responsible for having loaded the file (via
|
|
168
|
+
* {@link loadConfigFile}), extracted the environment slice, and parsed the flags. `host`, `port`, and `tls` always resolve to a concrete value (their defaults are
|
|
169
|
+
* `localhost`, `8581`, and `false`); the credential fields resolve to `null` when no source supplied them, leaving the CLI to decide which
|
|
170
|
+
* {@link logclient/types!LogClientCredentials | LogClientCredentials} arm the resolved material implies. The `port` from the environment is a string, so it is
|
|
171
|
+
* parsed here; a non-numeric `HBLOG_PORT` is ignored (falls through to the next source) rather than producing a `NaN` port.
|
|
172
|
+
*
|
|
173
|
+
* @param sources - The three configuration sources.
|
|
174
|
+
* @param sources.env - The environment slice. See {@link HblogEnv}.
|
|
175
|
+
* @param sources.file - The loaded config file, or `undefined` when absent. See {@link HblogConfigFile}.
|
|
176
|
+
* @param sources.flags - The parsed command-line flags. See {@link HblogConnectionFlags}.
|
|
177
|
+
*
|
|
178
|
+
* @returns The fully-resolved connection.
|
|
179
|
+
*
|
|
180
|
+
* @category Log Client
|
|
181
|
+
*/
|
|
182
|
+
export function resolveConnection(sources) {
|
|
183
|
+
const { env, file, flags } = sources;
|
|
184
|
+
// Parse the environment port from its string form. A non-numeric value is discarded so it does not mask a lower-precedence source with a `NaN`; an integer parse is
|
|
185
|
+
// used since a port is a whole number.
|
|
186
|
+
const envPort = (env.port !== undefined) ? Number.parseInt(env.port, 10) : undefined;
|
|
187
|
+
const envPortValid = (envPort !== undefined) && Number.isFinite(envPort);
|
|
188
|
+
return {
|
|
189
|
+
host: flags.host ?? env.host ?? file?.host ?? DEFAULT_HOST,
|
|
190
|
+
// A one-time passcode is single-use and time-bound, so the config file deliberately never persists one; the file source here is intentionally `undefined`.
|
|
191
|
+
otp: pickString(flags.otp, env.otp, undefined),
|
|
192
|
+
password: pickString(flags.password, env.password, file?.password),
|
|
193
|
+
port: flags.port ?? (envPortValid ? envPort : undefined) ?? file?.port ?? DEFAULT_PORT,
|
|
194
|
+
tls: flags.tls ?? file?.tls ?? false,
|
|
195
|
+
token: pickString(flags.token, env.token, file?.token),
|
|
196
|
+
username: pickString(flags.username, env.username, file?.username)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/logclient/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connection target shared by every URL builder in this module.
|
|
3
|
+
*
|
|
4
|
+
* @property host - The hostname or IP address of the homebridge-config-ui-x server.
|
|
5
|
+
* @property port - The TCP port the server listens on.
|
|
6
|
+
* @property tls - When `true`, URLs use the secure scheme (`https`/`wss`); when `false` or omitted, the plaintext scheme (`http`/`ws`).
|
|
7
|
+
*
|
|
8
|
+
* @category Log Client
|
|
9
|
+
*/
|
|
10
|
+
export interface EndpointTarget {
|
|
11
|
+
readonly host: string;
|
|
12
|
+
readonly port: number;
|
|
13
|
+
readonly tls?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the HTTP(S) base URL (scheme + authority, no trailing slash) for the REST and auth endpoints.
|
|
17
|
+
*
|
|
18
|
+
* The returned string is the origin only - callers append the specific API path. We construct it through the platform `URL` so host and port are normalized and
|
|
19
|
+
* encoded consistently, then read `URL.origin`, which yields the scheme + authority with no trailing slash, so callers can concatenate a leading-slash path without
|
|
20
|
+
* producing a double slash.
|
|
21
|
+
*
|
|
22
|
+
* @param target - The connection target. See {@link EndpointTarget}.
|
|
23
|
+
*
|
|
24
|
+
* @returns The origin, e.g. `https://localhost:8581`, with no trailing slash.
|
|
25
|
+
*
|
|
26
|
+
* @category Log Client
|
|
27
|
+
*/
|
|
28
|
+
export declare function httpBaseUrl(target: EndpointTarget): string;
|
|
29
|
+
/**
|
|
30
|
+
* The connection target plus the raw handshake token, used to build the WebSocket connect URL.
|
|
31
|
+
*
|
|
32
|
+
* @property token - The raw JWT, passed verbatim in the query string. The server's WebSocket guard reads `client.handshake.query.token` with no `Bearer` prefix and no
|
|
33
|
+
* fallback, so the token must be the bare JWT.
|
|
34
|
+
*
|
|
35
|
+
* @category Log Client
|
|
36
|
+
*/
|
|
37
|
+
export interface SocketTarget extends EndpointTarget {
|
|
38
|
+
readonly token: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the WebSocket connect URL for the live-log Socket.IO stream.
|
|
42
|
+
*
|
|
43
|
+
* The URL carries the Engine.IO version, the WebSocket transport selector, and the raw token in its query string; the server's handshake guard authenticates from that
|
|
44
|
+
* token alone. We build through the platform `URL` and `searchParams` so the token is percent-encoded correctly (a JWT contains `.` and may carry `-`/`_` from base64url,
|
|
45
|
+
* which are URL-safe, but routing through `searchParams` keeps encoding correct regardless of token shape).
|
|
46
|
+
*
|
|
47
|
+
* @param target - The connection target plus the raw token. See {@link SocketTarget}.
|
|
48
|
+
*
|
|
49
|
+
* @returns The full `ws(s)://host:port/socket.io/?EIO=4&transport=websocket&token=<rawjwt>` connect URL.
|
|
50
|
+
*
|
|
51
|
+
* @category Log Client
|
|
52
|
+
*/
|
|
53
|
+
export declare function socketUrl(target: SocketTarget): string;
|
|
54
|
+
//# sourceMappingURL=endpoints.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* logclient/endpoints.ts: Authority and URL construction for the Homebridge UI log client.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Single authority for constructing the URLs the log client connects to.
|
|
7
|
+
*
|
|
8
|
+
* Every transport (`auth.ts`, `rest.ts`, `socket.ts`) applies the same TLS-to-scheme mapping (http/https for REST and auth, ws/wss for the socket) and authority assembly
|
|
9
|
+
* over the same host + port. Routing that derivation through one module keeps the mapping and assembly in exactly one place, so a change to how URLs are built propagates
|
|
10
|
+
* everywhere rather than being re-derived (and potentially diverging) at each call site. The functions are pure string builders with no I/O.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { SOCKET_PATH } from "./settings.js";
|
|
15
|
+
// Format a host for inclusion in a URL authority, bracketing a bare IPv6 literal as the URL syntax requires. A literal IPv6 address contains colons, which collide with
|
|
16
|
+
// the host:port separator, so the URL grammar requires it to be wrapped in square brackets (`[::1]:8581`). A hostname or IPv4 address passes through unchanged. We
|
|
17
|
+
// detect an IPv6 literal by the presence of a colon and the absence of existing brackets, which is sufficient because no hostname or IPv4 address contains a colon.
|
|
18
|
+
function formatHost(host) {
|
|
19
|
+
if (host.includes(":") && !host.startsWith("[")) {
|
|
20
|
+
return "[" + host + "]";
|
|
21
|
+
}
|
|
22
|
+
return host;
|
|
23
|
+
}
|
|
24
|
+
// Build the scheme + authority origin for a target under a given scheme. We assemble the authority string by hand (bracketing IPv6) and construct a `URL` from the
|
|
25
|
+
// complete string so the platform validates and normalizes it in one pass - rather than mutating an empty `URL`'s `hostname`, whose setter silently rejects a bare IPv6
|
|
26
|
+
// literal. The `URL.origin` is the scheme + authority with no trailing slash; `httpBaseUrl` exposes that origin string for downstream path concatenation, while
|
|
27
|
+
// `socketUrl` keeps the `URL` object and configures it further via `pathname`/`searchParams`.
|
|
28
|
+
function originFor(scheme, target) {
|
|
29
|
+
return new URL(scheme + "://" + formatHost(target.host) + ":" + target.port.toString());
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the HTTP(S) base URL (scheme + authority, no trailing slash) for the REST and auth endpoints.
|
|
33
|
+
*
|
|
34
|
+
* The returned string is the origin only - callers append the specific API path. We construct it through the platform `URL` so host and port are normalized and
|
|
35
|
+
* encoded consistently, then read `URL.origin`, which yields the scheme + authority with no trailing slash, so callers can concatenate a leading-slash path without
|
|
36
|
+
* producing a double slash.
|
|
37
|
+
*
|
|
38
|
+
* @param target - The connection target. See {@link EndpointTarget}.
|
|
39
|
+
*
|
|
40
|
+
* @returns The origin, e.g. `https://localhost:8581`, with no trailing slash.
|
|
41
|
+
*
|
|
42
|
+
* @category Log Client
|
|
43
|
+
*/
|
|
44
|
+
export function httpBaseUrl(target) {
|
|
45
|
+
// The scheme follows the TLS flag: `https` when secure, `http` otherwise. `URL.origin` yields the scheme + authority with no path - exactly the trailing-slash-free
|
|
46
|
+
// base callers want to append a leading-slash path to.
|
|
47
|
+
return originFor(target.tls === true ? "https" : "http", target).origin;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the WebSocket connect URL for the live-log Socket.IO stream.
|
|
51
|
+
*
|
|
52
|
+
* The URL carries the Engine.IO version, the WebSocket transport selector, and the raw token in its query string; the server's handshake guard authenticates from that
|
|
53
|
+
* token alone. We build through the platform `URL` and `searchParams` so the token is percent-encoded correctly (a JWT contains `.` and may carry `-`/`_` from base64url,
|
|
54
|
+
* which are URL-safe, but routing through `searchParams` keeps encoding correct regardless of token shape).
|
|
55
|
+
*
|
|
56
|
+
* @param target - The connection target plus the raw token. See {@link SocketTarget}.
|
|
57
|
+
*
|
|
58
|
+
* @returns The full `ws(s)://host:port/socket.io/?EIO=4&transport=websocket&token=<rawjwt>` connect URL.
|
|
59
|
+
*
|
|
60
|
+
* @category Log Client
|
|
61
|
+
*/
|
|
62
|
+
export function socketUrl(target) {
|
|
63
|
+
// The WebSocket schemes mirror the HTTP schemes: `wss` for TLS, `ws` otherwise. We start from the validated origin, then set the Socket.IO mount path and the standard
|
|
64
|
+
// Engine.IO query parameters; the server requires `EIO=4` and the `websocket` transport for a raw WebSocket connection. Routing the token through `searchParams`
|
|
65
|
+
// percent-encodes it correctly regardless of its character set.
|
|
66
|
+
const url = originFor(target.tls === true ? "wss" : "ws", target);
|
|
67
|
+
url.pathname = SOCKET_PATH;
|
|
68
|
+
url.searchParams.set("EIO", "4");
|
|
69
|
+
url.searchParams.set("transport", "websocket");
|
|
70
|
+
url.searchParams.set("token", target.token);
|
|
71
|
+
return url.toString();
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=endpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoints.js","sourceRoot":"","sources":["../../src/logclient/endpoints.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;GAQG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,wKAAwK;AACxK,mKAAmK;AACnK,oKAAoK;AACpK,SAAS,UAAU,CAAC,IAAY;IAE9B,IAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAE/C,OAAO,GAAG,GAAG,IAAI,GAAG,GAAG,CAAC;IAC1B,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,mKAAmK;AACnK,wKAAwK;AACxK,gKAAgK;AAChK,8FAA8F;AAC9F,SAAS,SAAS,CAAC,MAAc,EAAE,MAAsB;IAEvD,OAAO,IAAI,GAAG,CAAC,MAAM,GAAG,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC1F,CAAC;AAkBD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,MAAsB;IAEhD,oKAAoK;IACpK,uDAAuD;IACvD,OAAO,SAAS,CAAC,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC;AAC1E,CAAC;AAeD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAC,MAAoB;IAE5C,uKAAuK;IACvK,iKAAiK;IACjK,gEAAgE;IAChE,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAElE,GAAG,CAAC,QAAQ,GAAG,WAAW,CAAC;IAC3B,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACjC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAC/C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAE5C,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure {@link LogRecord} filter construction.
|
|
3
|
+
*
|
|
4
|
+
* {@link createLogFilter} compiles a set of optional criteria - a substring/regex grep, a level allow-list, a plugin allow-list - into a single predicate a consumer
|
|
5
|
+
* folds over any record stream. The module is deliberately I/O-free and warning-free: it owns matching logic only. The orchestration layer is responsible for the
|
|
6
|
+
* user-facing "a level filter is active but no record carried a level" advisory (a level filter is only meaningful when the Homebridge process emits ANSI color), which
|
|
7
|
+
* is a policy concern that does not belong in a pure predicate.
|
|
8
|
+
*
|
|
9
|
+
* An empty criteria set compiles to a pass-all predicate, so a consumer can always build a filter and apply it unconditionally rather than branching on "are any filters
|
|
10
|
+
* active." Multiple criteria combine with AND - a record must satisfy every active criterion to pass.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { LogLevel, LogRecord } from "./types.ts";
|
|
15
|
+
/**
|
|
16
|
+
* The filter criteria. Every field is optional; an omitted field imposes no constraint, and an all-omitted object yields a pass-all predicate.
|
|
17
|
+
*
|
|
18
|
+
* @property grep - A regular expression tested against the record's ANSI-stripped `message`. A record passes the grep criterion when its message matches.
|
|
19
|
+
* @property levels - An allow-list of severity levels. A record passes when its `level` is in the list. A record whose `level` is `null` never satisfies a non-empty
|
|
20
|
+
* level allow-list, since an unknown severity cannot be affirmatively matched against a requested one.
|
|
21
|
+
* @property plugins - An allow-list of plugin names, matched case-insensitively against the record's `plugin`. A record whose `plugin` is `null` never satisfies a
|
|
22
|
+
* non-empty plugin allow-list.
|
|
23
|
+
*
|
|
24
|
+
* @category Log Client
|
|
25
|
+
*/
|
|
26
|
+
export interface LogFilterCriteria {
|
|
27
|
+
readonly grep?: RegExp;
|
|
28
|
+
readonly levels?: readonly LogLevel[];
|
|
29
|
+
readonly plugins?: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compile filter criteria into a single predicate over {@link LogRecord}.
|
|
33
|
+
*
|
|
34
|
+
* The returned predicate evaluates each active criterion and returns `true` only when all of them pass (logical AND). Criteria are normalized once at construction - the
|
|
35
|
+
* plugin allow-list is lower-cased into a `Set` for O(1) case-insensitive membership, and the level allow-list into a `Set` - so the per-record hot path does no
|
|
36
|
+
* repeated allocation or case folding. An all-omitted (or all-empty) criteria object compiles to a predicate that always returns `true`.
|
|
37
|
+
*
|
|
38
|
+
* @param criteria - The filter criteria. See {@link LogFilterCriteria}.
|
|
39
|
+
*
|
|
40
|
+
* @returns A predicate that returns `true` when a record satisfies every active criterion.
|
|
41
|
+
*
|
|
42
|
+
* @category Log Client
|
|
43
|
+
*/
|
|
44
|
+
export declare function createLogFilter(criteria?: LogFilterCriteria): (record: LogRecord) => boolean;
|
|
45
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* logclient/filter.ts: Pure predicate builder for filtering parsed log records.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Compile filter criteria into a single predicate over {@link LogRecord}.
|
|
7
|
+
*
|
|
8
|
+
* The returned predicate evaluates each active criterion and returns `true` only when all of them pass (logical AND). Criteria are normalized once at construction - the
|
|
9
|
+
* plugin allow-list is lower-cased into a `Set` for O(1) case-insensitive membership, and the level allow-list into a `Set` - so the per-record hot path does no
|
|
10
|
+
* repeated allocation or case folding. An all-omitted (or all-empty) criteria object compiles to a predicate that always returns `true`.
|
|
11
|
+
*
|
|
12
|
+
* @param criteria - The filter criteria. See {@link LogFilterCriteria}.
|
|
13
|
+
*
|
|
14
|
+
* @returns A predicate that returns `true` when a record satisfies every active criterion.
|
|
15
|
+
*
|
|
16
|
+
* @category Log Client
|
|
17
|
+
*/
|
|
18
|
+
export function createLogFilter(criteria = {}) {
|
|
19
|
+
const { grep, levels, plugins } = criteria;
|
|
20
|
+
// Normalize the allow-lists once. We lower-case plugin names into a `Set` so per-record plugin matching is a single case-insensitive `Set.has`, and we treat an empty
|
|
21
|
+
// array the same as an omitted field (no constraint) by leaving the corresponding `Set` undefined.
|
|
22
|
+
const levelSet = (levels !== undefined) && (levels.length > 0) ? new Set(levels) : undefined;
|
|
23
|
+
const pluginSet = (plugins !== undefined) && (plugins.length > 0) ? new Set(plugins.map((plugin) => plugin.toLowerCase())) : undefined;
|
|
24
|
+
return (record) => {
|
|
25
|
+
// Grep: a record fails when an active pattern does not match its message. We test against the ANSI-stripped `message` so a color escape never interferes with the
|
|
26
|
+
// user's pattern. The caller may hand us a `/g`- or `/y`-flagged RegExp whose `.test()` is stateful (it advances `lastIndex` between calls); we reset `lastIndex`
|
|
27
|
+
// to 0 before each test so the predicate is order-independent and a stateful flag cannot cause it to skip a matching record.
|
|
28
|
+
if (grep !== undefined) {
|
|
29
|
+
grep.lastIndex = 0;
|
|
30
|
+
if (!grep.test(record.message)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Level allow-list: a record fails when its level is null (no severity known) or is absent from the allow-list. A null level cannot affirmatively match a requested
|
|
35
|
+
// level, so it is excluded rather than passed.
|
|
36
|
+
if (levelSet !== undefined) {
|
|
37
|
+
if ((record.level === null) || !levelSet.has(record.level)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Plugin allow-list: a record fails when its plugin is null or, case-insensitively, absent from the allow-list.
|
|
42
|
+
if (pluginSet !== undefined) {
|
|
43
|
+
if ((record.plugin === null) || !pluginSet.has(record.plugin.toLowerCase())) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Every active criterion passed (or none were active), so the record is admitted.
|
|
48
|
+
return true;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../../src/logclient/filter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmCH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,WAA8B,EAAE;IAE9D,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC;IAE3C,sKAAsK;IACtK,mGAAmG;IACnG,MAAM,QAAQ,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAW,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACvG,MAAM,SAAS,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAS,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE/I,OAAO,CAAC,MAAiB,EAAW,EAAE;QAEpC,kKAAkK;QAClK,kKAAkK;QAClK,6HAA6H;QAC7H,IAAG,IAAI,KAAK,SAAS,EAAE,CAAC;YAEtB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;YAEnB,IAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBAE9B,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,oKAAoK;QACpK,+CAA+C;QAC/C,IAAG,QAAQ,KAAK,SAAS,EAAE,CAAC;YAE1B,IAAG,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBAE1D,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,gHAAgH;QAChH,IAAG,SAAS,KAAK,SAAS,EAAE,CAAC;YAE3B,IAAG,CAAC,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;gBAE3E,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,kFAAkF;QAClF,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;AACJ,CAAC"}
|