@mxpicture/tesla-superchargers-http 0.1.10 → 0.1.12

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/dist/auth.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * True when the `Authorization` header carries the expected bearer token.
3
+ * Both sides are hashed before the constant-time compare so it never leaks the
4
+ * token length, and a missing or malformed header is simply unauthorized.
5
+ */
6
+ export declare const isAuthorized: (header: string | undefined, token: string) => boolean;
7
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,eAAO,MAAM,YAAY,GACvB,QAAQ,MAAM,GAAG,SAAS,EAC1B,OAAO,MAAM,KACZ,OAIF,CAAC"}
package/dist/auth.js ADDED
@@ -0,0 +1,14 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ const sha256 = (value) => createHash("sha256").update(value).digest();
3
+ /**
4
+ * True when the `Authorization` header carries the expected bearer token.
5
+ * Both sides are hashed before the constant-time compare so it never leaks the
6
+ * token length, and a missing or malformed header is simply unauthorized.
7
+ */
8
+ export const isAuthorized = (header, token) => {
9
+ const match = /^Bearer +(.+)$/i.exec((header ?? "").trim());
10
+ if (!match)
11
+ return false;
12
+ return timingSafeEqual(sha256(match[1]), sha256(token));
13
+ };
14
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,MAAM,GAAG,CAAC,KAAa,EAAU,EAAE,CACvC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;AAE9C;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,MAA0B,EAC1B,KAAa,EACJ,EAAE;IACX,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5D,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,OAAO,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1D,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { CommandRegistry } from "./types.js";
2
+ /**
3
+ * The HTTP command registry, each entry wired to the api layer directly (never
4
+ * the CLI). Read-only lookups answer GET with query-string params; the
5
+ * side-effecting actions answer POST with a JSON body.
6
+ */
7
+ export declare const buildCommands: () => CommandRegistry;
8
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAU,MAAM,YAAY,CAAC;AAwB1D;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAO,eA4C/B,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { chargersApi, chargingProcessesApi, failuresApi, invoicesApi, } from "@mxpicture/tesla-superchargers-api/api";
2
+ import { HttpError } from "./errors.js";
3
+ import { coordinate, flag, locale, optString, optTimestamp, radiusKm, source, threshold, } from "./params.js";
4
+ /** reset-failures: exactly one of `id` (a single row) or `all` (the source). */
5
+ const resetFailures = async (params) => {
6
+ const src = source(params);
7
+ const id = optString(params, "id");
8
+ const all = flag(params, "all");
9
+ if ((id !== undefined) === all)
10
+ throw new HttpError(400, "provide exactly one of 'id' or 'all'");
11
+ if (id !== undefined) {
12
+ if (src === "all")
13
+ throw new HttpError(400, "'id' requires a concrete 'source' (charges, chargers or slugs)");
14
+ await failuresApi().resetFailed(src, id);
15
+ return { source: src, id };
16
+ }
17
+ return await failuresApi().resetAllFailed(src);
18
+ };
19
+ /**
20
+ * The HTTP command registry, each entry wired to the api layer directly (never
21
+ * the CLI). Read-only lookups answer GET with query-string params; the
22
+ * side-effecting actions answer POST with a JSON body.
23
+ */
24
+ export const buildCommands = () => ({
25
+ "find-charger": {
26
+ method: "GET",
27
+ run: (p) => chargersApi().find(coordinate(p), locale(p), radiusKm(p)),
28
+ },
29
+ "find-prices": {
30
+ method: "GET",
31
+ run: (p) => chargersApi().findPrices(coordinate(p), optTimestamp(p, "at") ?? new Date(), radiusKm(p)),
32
+ },
33
+ "find-unpriced-charges": {
34
+ method: "GET",
35
+ run: () => chargingProcessesApi().getUnpricedCharges(),
36
+ },
37
+ "find-failures": {
38
+ method: "GET",
39
+ run: (p) => failuresApi().getFailed(source(p), threshold(p)),
40
+ },
41
+ "find-unmatched-invoices": {
42
+ method: "GET",
43
+ run: () => invoicesApi().getUnmatched(),
44
+ },
45
+ "sync-invoices-dropbox": {
46
+ method: "POST",
47
+ run: () => invoicesApi().syncDropbox(),
48
+ },
49
+ "process-unpriced-charges": {
50
+ method: "POST",
51
+ run: () => chargingProcessesApi().runPricing(),
52
+ },
53
+ "reset-failures": { method: "POST", run: resetFailures },
54
+ "notify-failures": {
55
+ method: "POST",
56
+ run: (p) => failuresApi().notifyFailures(threshold(p), console.log),
57
+ },
58
+ "notify-unmatched-invoices": {
59
+ method: "POST",
60
+ run: () => invoicesApi().notifyUnmatched(console.log),
61
+ },
62
+ });
63
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /** An error carrying the HTTP status to answer with; anything else is a 500. */
2
+ export declare class HttpError extends Error {
3
+ readonly status: number;
4
+ constructor(status: number, message: string);
5
+ }
6
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,qBAAa,SAAU,SAAQ,KAAK;aAEhB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAC9B,OAAO,EAAE,MAAM;CAKlB"}
package/dist/errors.js ADDED
@@ -0,0 +1,10 @@
1
+ /** An error carrying the HTTP status to answer with; anything else is a 500. */
2
+ export class HttpError extends Error {
3
+ status;
4
+ constructor(status, message) {
5
+ super(message);
6
+ this.status = status;
7
+ this.name = "HttpError";
8
+ }
9
+ }
10
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEhB;IADlB,YACkB,MAAc,EAC9B,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,WAAM,GAAN,MAAM,CAAQ;QAI9B,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * teslamate-superchargers-http
4
+ * ============================
5
+ * A small `node:http` server that exposes the same api the CLI drives (see
6
+ * `@mxpicture/tesla-superchargers-api`) as bearer-token-authenticated
7
+ * endpoints — one per command:
8
+ *
9
+ * GET /find-charger?lat&lon
10
+ * GET /find-prices?lat&lon[&at]
11
+ * GET /find-unpriced-charges
12
+ * GET /find-failures[?source&threshold]
13
+ * GET /find-unmatched-invoices
14
+ * POST /sync-invoices-dropbox
15
+ * POST /process-unpriced-charges
16
+ * POST /reset-failures body: { source, id } | { source, all: true }
17
+ * POST /notify-failures body: { threshold? }
18
+ * POST /notify-unmatched-invoices
19
+ * GET /health (unauthenticated liveness probe)
20
+ *
21
+ * The database connection comes from the standard `PG*` environment variables;
22
+ * alerts need `PUSHOVER_API_TOKEN` / `PUSHOVER_USER_KEY` (off when unset).
23
+ *
24
+ * Required: `HTTP_AUTH_TOKEN` (the bearer token clients must present).
25
+ * Optional: `HTTP_PORT` (default 8080).
26
+ */
27
+ import { endSql } from "@mxpicture/tesla-superchargers-mate/store";
28
+ import { buildCommands } from "./commands.js";
29
+ import { createServer } from "./server.js";
30
+ const token = process.env.HTTP_AUTH_TOKEN;
31
+ if (!token) {
32
+ console.error("HTTP_AUTH_TOKEN is required (the bearer token clients must present)");
33
+ process.exit(1);
34
+ }
35
+ const port = Number(process.env.HTTP_PORT) || 8080;
36
+ const server = createServer({ token, commands: buildCommands() });
37
+ server.listen(port, () => console.log(`http server listening on :${port}`));
38
+ // stop accepting requests, close the shared postgres pool, then exit
39
+ const shutdown = () => {
40
+ server.close(() => void endSql().finally(() => process.exit(0)));
41
+ };
42
+ process.on("SIGINT", shutdown);
43
+ process.on("SIGTERM", shutdown);
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;AAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;IACX,OAAO,CAAC,KAAK,CACX,qEAAqE,CACtE,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;AACnD,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC,CAAC;AAE5E,qEAAqE;AACrE,MAAM,QAAQ,GAAG,GAAS,EAAE;IAC1B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC,CAAC;AACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { FailureSource } from "@mxpicture/tesla-superchargers-api/types";
2
+ import type { Params } from "./types.js";
3
+ /** A present, non-empty scalar as a string; absent/empty → undefined. */
4
+ export declare const optString: (params: Params, key: string) => string | undefined;
5
+ /** The `lat`/`lon` pair as a coordinate; missing or non-numeric → 400. */
6
+ export declare const coordinate: (params: Params) => {
7
+ latitude: number;
8
+ longitude: number;
9
+ };
10
+ export declare const radiusKm: (params: Params) => number;
11
+ export declare const locale: (params: Params) => string;
12
+ /** An optional ISO-8601 timestamp; present but unparseable → 400. */
13
+ export declare const optTimestamp: (params: Params, key: string) => Date | undefined;
14
+ /** The failure source, defaulting to "all"; an unknown value → 400. */
15
+ export declare const source: (params: Params) => FailureSource | "all";
16
+ /** The minimum failure count, defaulting to the deprioritize threshold. */
17
+ export declare const threshold: (params: Params) => number;
18
+ /** A boolean flag: true only for an explicit true / "true" / "1". */
19
+ export declare const flag: (params: Params, key: string) => boolean;
20
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.d.ts","sourceRoot":"","sources":["../src/params.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AAE9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAMzC,yEAAyE;AACzE,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,GAAG,SAOhE,CAAC;AAcF,0EAA0E;AAC1E,eAAO,MAAM,UAAU,GACrB,QAAQ,MAAM,KACb;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAGtC,CAAC;AAEH,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,KAAG,MACT,CAAC;AAElC,eAAO,MAAM,MAAM,GAAI,QAAQ,MAAM,KAAG,MACF,CAAC;AAEvC,qEAAqE;AACrE,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,KAAK,MAAM,KAAG,IAAI,GAAG,SASjE,CAAC;AAIF,uEAAuE;AACvE,eAAO,MAAM,MAAM,GAAI,QAAQ,MAAM,KAAG,aAAa,GAAG,KAMvD,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,KAAG,MAQ1C,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,IAAI,GAAI,QAAQ,MAAM,EAAE,KAAK,MAAM,KAAG,OAGlD,CAAC"}
package/dist/params.js ADDED
@@ -0,0 +1,72 @@
1
+ import { FAILURE_DEPRIORITIZE_THRESHOLD } from "@mxpicture/tesla-superchargers-api/common";
2
+ import { HttpError } from "./errors.js";
3
+ const bad = (message) => {
4
+ throw new HttpError(400, message);
5
+ };
6
+ /** A present, non-empty scalar as a string; absent/empty → undefined. */
7
+ export const optString = (params, key) => {
8
+ const value = params[key];
9
+ if (value === undefined || value === null || value === "")
10
+ return undefined;
11
+ if (typeof value === "string")
12
+ return value;
13
+ if (typeof value === "number" || typeof value === "boolean")
14
+ return String(value);
15
+ return bad(`'${key}' must be a string`);
16
+ };
17
+ const numberValue = (params, key) => {
18
+ const value = params[key];
19
+ const n = typeof value === "number"
20
+ ? value
21
+ : typeof value === "string" && value.trim() !== ""
22
+ ? Number(value)
23
+ : NaN;
24
+ if (!Number.isFinite(n))
25
+ return bad(`'${key}' must be a number`);
26
+ return n;
27
+ };
28
+ /** The `lat`/`lon` pair as a coordinate; missing or non-numeric → 400. */
29
+ export const coordinate = (params) => ({
30
+ latitude: numberValue(params, "lat"),
31
+ longitude: numberValue(params, "lon"),
32
+ });
33
+ export const radiusKm = (params) => numberValue(params, "radiusKm");
34
+ export const locale = (params) => params.locale ?? "de-DE";
35
+ /** An optional ISO-8601 timestamp; present but unparseable → 400. */
36
+ export const optTimestamp = (params, key) => {
37
+ const value = params[key];
38
+ if (value === undefined || value === null || value === "")
39
+ return undefined;
40
+ if (typeof value !== "string" && typeof value !== "number")
41
+ return bad(`'${key}' must be an ISO 8601 timestamp`);
42
+ const date = new Date(value);
43
+ if (Number.isNaN(date.getTime()))
44
+ return bad(`'${key}' must be an ISO 8601 timestamp`);
45
+ return date;
46
+ };
47
+ const SOURCES = ["charges", "chargers", "slugs", "all"];
48
+ /** The failure source, defaulting to "all"; an unknown value → 400. */
49
+ export const source = (params) => {
50
+ const value = optString(params, "source");
51
+ if (value === undefined)
52
+ return "all";
53
+ if (SOURCES.includes(value))
54
+ return value;
55
+ return bad(`'source' must be one of ${SOURCES.join(", ")}`);
56
+ };
57
+ /** The minimum failure count, defaulting to the deprioritize threshold. */
58
+ export const threshold = (params) => {
59
+ const value = params["threshold"];
60
+ if (value === undefined || value === null || value === "")
61
+ return FAILURE_DEPRIORITIZE_THRESHOLD;
62
+ const n = typeof value === "number" ? value : Number(value);
63
+ if (!Number.isInteger(n) || n < 0)
64
+ return bad("'threshold' must be a non-negative integer");
65
+ return n;
66
+ };
67
+ /** A boolean flag: true only for an explicit true / "true" / "1". */
68
+ export const flag = (params, key) => {
69
+ const value = params[key];
70
+ return value === true || value === "true" || value === "1";
71
+ };
72
+ //# sourceMappingURL=params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"params.js","sourceRoot":"","sources":["../src/params.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type Server } from "node:http";
2
+ import type { CommandRegistry } from "./types.js";
3
+ export interface ServerOptions {
4
+ token: string;
5
+ commands: CommandRegistry;
6
+ }
7
+ /** A `node:http` server exposing the command registry behind bearer-token auth. */
8
+ export declare const createServer: (opts: ServerOptions) => Server;
9
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EAEZ,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,eAAe,EAAU,MAAM,YAAY,CAAC;AAK1D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,eAAe,CAAC;CAC3B;AA6DD,mFAAmF;AACnF,eAAO,MAAM,YAAY,GAAI,MAAM,aAAa,KAAG,MAQ/C,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,59 @@
1
+ import { createServer as createHttpServer, } from "node:http";
2
+ import { isAuthorized } from "./auth.js";
3
+ import { HttpError } from "./errors.js";
4
+ /** Reject bodies larger than this (bytes) before buffering them. */
5
+ const MAX_BODY_BYTES = 1_000_000;
6
+ const sendJson = (res, status, payload) => {
7
+ res.writeHead(status, { "content-type": "application/json" });
8
+ res.end(JSON.stringify(payload ?? null));
9
+ };
10
+ /** Buffer and JSON-parse the request body into a params object ({} when empty). */
11
+ const readJsonBody = async (req) => {
12
+ const chunks = [];
13
+ let size = 0;
14
+ for await (const chunk of req) {
15
+ size += chunk.length;
16
+ if (size > MAX_BODY_BYTES)
17
+ throw new HttpError(413, "request body too large");
18
+ chunks.push(chunk);
19
+ }
20
+ if (size === 0)
21
+ return {};
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
25
+ }
26
+ catch {
27
+ throw new HttpError(400, "invalid JSON body");
28
+ }
29
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
30
+ throw new HttpError(400, "request body must be a JSON object");
31
+ return parsed;
32
+ };
33
+ const handle = async (req, res, opts) => {
34
+ const url = new URL(req.url ?? "/", "http://localhost");
35
+ // unauthenticated liveness probe
36
+ if (req.method === "GET" && url.pathname === "/health")
37
+ return sendJson(res, 200, { status: "ok" });
38
+ if (!isAuthorized(req.headers.authorization, opts.token))
39
+ return sendJson(res, 401, { error: "unauthorized" });
40
+ const command = opts.commands[url.pathname.replace(/^\/+/, "")];
41
+ if (!command)
42
+ return sendJson(res, 404, { error: "not found" });
43
+ if (command.method !== req.method)
44
+ return sendJson(res, 405, { error: "method not allowed" });
45
+ const params = command.method === "GET"
46
+ ? Object.fromEntries(url.searchParams)
47
+ : await readJsonBody(req);
48
+ sendJson(res, 200, await command.run(params));
49
+ };
50
+ /** A `node:http` server exposing the command registry behind bearer-token auth. */
51
+ export const createServer = (opts) => createHttpServer((req, res) => {
52
+ handle(req, res, opts).catch((error) => {
53
+ if (error instanceof HttpError)
54
+ return sendJson(res, error.status, { error: error.message });
55
+ console.error(error);
56
+ sendJson(res, 500, { error: "internal error" });
57
+ });
58
+ });
59
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A flat bag of request parameters: query-string values for GET commands
3
+ * (always strings) or JSON-body fields for POST commands (arbitrary JSON).
4
+ * The parsers in `params.ts` narrow individual fields.
5
+ */
6
+ export type Params = Record<string, unknown>;
7
+ /** One HTTP-exposed command: the method it answers and the work it runs. */
8
+ export interface Command {
9
+ method: "GET" | "POST";
10
+ run: (params: Params) => unknown | Promise<unknown>;
11
+ }
12
+ /** Command name (the URL path without its leading slash) → command. */
13
+ export type CommandRegistry = Record<string, Command>;
14
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7C,4EAA4E;AAC5E,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrD;AAED,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxpicture/tesla-superchargers-http",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Http server for interacting with superchargers",
5
5
  "type": "module",
6
6
  "author": "MXPicture",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mxpicture/tesla-superchargers-api": "workspace:*",
36
- "@mxpicture/tesla-superchargers-mate": "workspace:*"
35
+ "@mxpicture/tesla-superchargers-api": "^0.1.12",
36
+ "@mxpicture/tesla-superchargers-mate": "^0.1.12"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^25.2.3",