@homeflare/distilled-opnsense 0.2.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/LICENSE +201 -0
- package/README.md +95 -0
- package/dist/credentials.d.ts +39 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +78 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +110 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +63 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +13 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +115 -0
- package/dist/protocol.js.map +1 -0
- package/dist/retry.d.ts +56 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +49 -0
- package/dist/retry.js.map +1 -0
- package/dist/services/firewall_alias.d.ts +243 -0
- package/dist/services/firewall_alias.d.ts.map +1 -0
- package/dist/services/firewall_alias.js +222 -0
- package/dist/services/firewall_alias.js.map +1 -0
- package/dist/services/firewall_category.d.ts +122 -0
- package/dist/services/firewall_category.d.ts.map +1 -0
- package/dist/services/firewall_category.js +161 -0
- package/dist/services/firewall_category.js.map +1 -0
- package/dist/services/firewall_filter.d.ts +289 -0
- package/dist/services/firewall_filter.d.ts.map +1 -0
- package/dist/services/firewall_filter.js +225 -0
- package/dist/services/firewall_filter.js.map +1 -0
- package/dist/services/firewall_group.d.ts +122 -0
- package/dist/services/firewall_group.d.ts.map +1 -0
- package/dist/services/firewall_group.js +154 -0
- package/dist/services/firewall_group.js.map +1 -0
- package/dist/services/index.d.ts +9 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/quagga_bgp.d.ts +1135 -0
- package/dist/services/quagga_bgp.d.ts.map +1 -0
- package/dist/services/quagga_bgp.js +1263 -0
- package/dist/services/quagga_bgp.js.map +1 -0
- package/dist/services/quagga_general.d.ts +55 -0
- package/dist/services/quagga_general.d.ts.map +1 -0
- package/dist/services/quagga_general.js +48 -0
- package/dist/services/quagga_general.js.map +1 -0
- package/dist/services/quagga_service.d.ts +60 -0
- package/dist/services/quagga_service.d.ts.map +1 -0
- package/dist/services/quagga_service.js +77 -0
- package/dist/services/quagga_service.js.map +1 -0
- package/dist/services/routing_settings.d.ts +192 -0
- package/dist/services/routing_settings.d.ts.map +1 -0
- package/dist/services/routing_settings.js +169 -0
- package/dist/services/routing_settings.js.map +1 -0
- package/dist/traits.d.ts +11 -0
- package/dist/traits.d.ts.map +1 -0
- package/dist/traits.js +11 -0
- package/dist/traits.js.map +1 -0
- package/package.json +75 -0
- package/src/credentials.ts +111 -0
- package/src/errors.ts +115 -0
- package/src/index.ts +32 -0
- package/src/protocol.ts +177 -0
- package/src/retry.ts +80 -0
- package/src/services/firewall_alias.ts +572 -0
- package/src/services/firewall_category.ts +359 -0
- package/src/services/firewall_filter.ts +671 -0
- package/src/services/firewall_group.ts +350 -0
- package/src/services/index.ts +9 -0
- package/src/services/quagga_bgp.ts +2955 -0
- package/src/services/quagga_general.ts +132 -0
- package/src/services/quagga_service.ts +182 -0
- package/src/services/routing_settings.ts +419 -0
- package/src/traits.ts +36 -0
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpnsenseProtocol — hand-written.
|
|
3
|
+
*
|
|
4
|
+
* OPNsense is a 200-with-failure vendor: `ApiMutableModelControllerBase`'s
|
|
5
|
+
* `set/add/del/toggleBase` (mirrored at `specs/core/models/Base/
|
|
6
|
+
* ApiMutableModelControllerBase.php`) answer HTTP 200 with
|
|
7
|
+
* `{"result":"failed", "validations"?: {...}}` on a validation error, and a
|
|
8
|
+
* plain `UserException`/`UserWarningException`/`UserInformationalException`
|
|
9
|
+
* thrown anywhere in a controller answers `{errorMessage, errorTitle?,
|
|
10
|
+
* errorLevel}` at 500/200/200 respectively (`src/opnsense/www/api.php`'s
|
|
11
|
+
* `catch` chain — see `errors.ts`'s module doc for the exact mapping this
|
|
12
|
+
* mirrors). `@distilled.cloud/core/protocol-rest`'s generic REST decode
|
|
13
|
+
* only ever branches on `response.status >= 400`, which is wrong for every
|
|
14
|
+
* one of those in-band failures — so, like `cloudflare` (the other
|
|
15
|
+
* envelope-with-embedded-failure provider in this monorepo), this package
|
|
16
|
+
* hand-rolls `decode` instead of calling `makeRestProtocol`. `encode` has
|
|
17
|
+
* no such quirk (OPNsense's request shape is plain REST) and reuses core's
|
|
18
|
+
* `buildRequest` directly.
|
|
19
|
+
*/
|
|
20
|
+
import * as Effect from "effect/Effect";
|
|
21
|
+
import * as Layer from "effect/Layer";
|
|
22
|
+
import * as Redacted from "effect/Redacted";
|
|
23
|
+
import type * as AST from "effect/SchemaAST";
|
|
24
|
+
import type * as HttpClient from "effect/unstable/http/HttpClient";
|
|
25
|
+
import type * as HttpClientError from "effect/unstable/http/HttpClientError";
|
|
26
|
+
import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
27
|
+
import * as API from "@distilled.cloud/core/api";
|
|
28
|
+
import { buildRequest, mapKeys } from "@distilled.cloud/core/protocol-http";
|
|
29
|
+
import { ConfigError, HTTP_STATUS_MAP } from "@distilled.cloud/core/errors";
|
|
30
|
+
import { Credentials, type Config } from "./credentials.ts";
|
|
31
|
+
import {
|
|
32
|
+
ValidationFailed,
|
|
33
|
+
OpnsenseUserError,
|
|
34
|
+
OpnsenseUserWarning,
|
|
35
|
+
OpnsenseUserNotice,
|
|
36
|
+
UnknownOpnsenseError,
|
|
37
|
+
type DefaultErrors,
|
|
38
|
+
} from "./errors.ts";
|
|
39
|
+
|
|
40
|
+
/** Error channel shared by every generated OPNsense operation. */
|
|
41
|
+
export type OpnsenseOpError =
|
|
42
|
+
| DefaultErrors
|
|
43
|
+
| ConfigError
|
|
44
|
+
| HttpClientError.HttpClientError;
|
|
45
|
+
|
|
46
|
+
/** Context (requirements) shared by every generated OPNsense operation. */
|
|
47
|
+
export type OpnsenseOpContext = Credentials | HttpClient.HttpClient;
|
|
48
|
+
|
|
49
|
+
const fail = (e: unknown): Effect.Effect<never> =>
|
|
50
|
+
Effect.fail(e) as Effect.Effect<never>;
|
|
51
|
+
|
|
52
|
+
/** `{status: 401|403|400, message}` from `ApiControllerBase::beforeExecuteRoute` — the ONE OPNsense shape with a genuine (numeric) `status` field and a non-2xx HTTP status to match it. */
|
|
53
|
+
const isAuthGateBody = (
|
|
54
|
+
body: unknown,
|
|
55
|
+
): body is { status: number; message: string } =>
|
|
56
|
+
typeof body === "object" &&
|
|
57
|
+
body !== null &&
|
|
58
|
+
typeof (body as any).status === "number" &&
|
|
59
|
+
typeof (body as any).message === "string";
|
|
60
|
+
|
|
61
|
+
/** `{errorMessage, errorTitle?, errorLevel?}` from `api.php`'s `UserException`/`DispatchException`/generic-`Exception` catch chain. */
|
|
62
|
+
const isVendorExceptionBody = (
|
|
63
|
+
body: unknown,
|
|
64
|
+
): body is { errorMessage: string; errorTitle?: string; errorLevel?: string } =>
|
|
65
|
+
typeof body === "object" &&
|
|
66
|
+
body !== null &&
|
|
67
|
+
typeof (body as any).errorMessage === "string";
|
|
68
|
+
|
|
69
|
+
/** `{result:"failed", validations?}` from `ApiMutableModelControllerBase::validate/set/add/del/toggleBase`. */
|
|
70
|
+
const isResultFailedBody = (
|
|
71
|
+
body: unknown,
|
|
72
|
+
): body is { result: string; validations?: unknown } =>
|
|
73
|
+
typeof body === "object" &&
|
|
74
|
+
body !== null &&
|
|
75
|
+
(body as any).result === "failed";
|
|
76
|
+
|
|
77
|
+
const classifyFailure = (status: number, body: unknown): unknown => {
|
|
78
|
+
if (isAuthGateBody(body)) {
|
|
79
|
+
if (body.status === 401 || body.status === 403 || body.status === 400) {
|
|
80
|
+
const Cls = HTTP_STATUS_MAP[body.status as 400 | 401 | 403];
|
|
81
|
+
return new Cls({ message: body.message });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (isVendorExceptionBody(body)) {
|
|
85
|
+
const { errorMessage: message, errorTitle: title, errorLevel } = body;
|
|
86
|
+
if (errorLevel === "warning")
|
|
87
|
+
return new OpnsenseUserWarning({ title, message });
|
|
88
|
+
if (errorLevel === "info")
|
|
89
|
+
return new OpnsenseUserNotice({ title, message });
|
|
90
|
+
if (status === 404) {
|
|
91
|
+
const Cls = HTTP_STATUS_MAP[404];
|
|
92
|
+
return new Cls({ message });
|
|
93
|
+
}
|
|
94
|
+
if (errorLevel === "error" || title !== undefined)
|
|
95
|
+
return new OpnsenseUserError({ title, message });
|
|
96
|
+
const Cls = HTTP_STATUS_MAP[500];
|
|
97
|
+
return new Cls({ message, code: undefined, retryAfter: undefined });
|
|
98
|
+
}
|
|
99
|
+
if (isResultFailedBody(body)) {
|
|
100
|
+
const validations = (body.validations ?? {}) as Record<
|
|
101
|
+
string,
|
|
102
|
+
string | string[]
|
|
103
|
+
>;
|
|
104
|
+
return new ValidationFailed({ validations });
|
|
105
|
+
}
|
|
106
|
+
const StatusCls = (
|
|
107
|
+
HTTP_STATUS_MAP as Record<number, (new (args: any) => any) | undefined>
|
|
108
|
+
)[status];
|
|
109
|
+
if (status >= 400 && StatusCls)
|
|
110
|
+
return new StatusCls({ message: `HTTP ${status}` });
|
|
111
|
+
return new UnknownOpnsenseError({ status, body });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const encode = ({
|
|
115
|
+
input,
|
|
116
|
+
inputAst,
|
|
117
|
+
}: {
|
|
118
|
+
readonly input: unknown;
|
|
119
|
+
readonly inputAst: AST.AST;
|
|
120
|
+
}) =>
|
|
121
|
+
Effect.gen(function* () {
|
|
122
|
+
const resolve = yield* Credentials;
|
|
123
|
+
const creds: Config = yield* resolve;
|
|
124
|
+
return buildRequest({
|
|
125
|
+
input,
|
|
126
|
+
inputAst,
|
|
127
|
+
baseUrl: creds.apiBaseUrl,
|
|
128
|
+
headers: {
|
|
129
|
+
Authorization: `Basic ${Buffer.from(`${creds.apiKey}:${Redacted.value(creds.apiSecret)}`).toString("base64")}`,
|
|
130
|
+
Accept: "application/json",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const decode = ({
|
|
136
|
+
response,
|
|
137
|
+
outputAst,
|
|
138
|
+
}: {
|
|
139
|
+
readonly response: HttpClientResponse.HttpClientResponse;
|
|
140
|
+
readonly outputAst: AST.AST;
|
|
141
|
+
readonly errors: ReadonlyArray<unknown>;
|
|
142
|
+
}) =>
|
|
143
|
+
Effect.gen(function* () {
|
|
144
|
+
const text = (yield* response.text.pipe(Effect.orDie)) ?? "";
|
|
145
|
+
let json: unknown;
|
|
146
|
+
let nonJson = false;
|
|
147
|
+
if (text.trim().length > 0) {
|
|
148
|
+
try {
|
|
149
|
+
json = JSON.parse(text);
|
|
150
|
+
} catch {
|
|
151
|
+
nonJson = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const status = response.status;
|
|
155
|
+
const body = nonJson ? text : (json ?? {});
|
|
156
|
+
|
|
157
|
+
// Success is NOT simply `status < 400` here: `{result:"failed"}` and the
|
|
158
|
+
// UserWarning/UserInformational vendor-exception shapes are failures at
|
|
159
|
+
// HTTP 200 (see module doc) — checked FIRST, before falling through to
|
|
160
|
+
// "this 2xx body is the payload".
|
|
161
|
+
if (
|
|
162
|
+
status >= 400 ||
|
|
163
|
+
isResultFailedBody(body) ||
|
|
164
|
+
isVendorExceptionBody(body)
|
|
165
|
+
) {
|
|
166
|
+
return yield* fail(classifyFailure(status, body));
|
|
167
|
+
}
|
|
168
|
+
return mapKeys(outputAst, body, "decode");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const OpnsenseProtocol: Layer.Layer<API.Protocol> = Layer.succeed(
|
|
172
|
+
API.Protocol,
|
|
173
|
+
API.Protocol.of({
|
|
174
|
+
encode: (args) => encode(args) as Effect.Effect<any>,
|
|
175
|
+
decode,
|
|
176
|
+
}),
|
|
177
|
+
);
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPNsense retry surface — a veneer over `@distilled.cloud/core/retry`.
|
|
3
|
+
*
|
|
4
|
+
* The `Retry` service tag is threaded into every generated operation via
|
|
5
|
+
* `API.make({ retry: Retry })`, so a caller-installed policy applies to all
|
|
6
|
+
* OPNsense calls below it and core's `makeDefault` is the fallback when none
|
|
7
|
+
* is provided.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import * as Opnsense from "@distilled.cloud/opnsense";
|
|
12
|
+
*
|
|
13
|
+
* myEffect.pipe(Opnsense.Retry.transient);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import * as Context from "effect/Context";
|
|
17
|
+
import * as Effect from "effect/Effect";
|
|
18
|
+
import * as Layer from "effect/Layer";
|
|
19
|
+
import * as Retries from "@distilled.cloud/core/retry";
|
|
20
|
+
|
|
21
|
+
export type Options = Retries.Options;
|
|
22
|
+
export type Factory = Retries.Factory;
|
|
23
|
+
export type Policy = Retries.Policy;
|
|
24
|
+
|
|
25
|
+
/** Context tag for configuring retry behavior of OPNsense API calls. */
|
|
26
|
+
export class Retry extends Context.Service<Retry, Policy>()("OpnsenseRetry") {}
|
|
27
|
+
|
|
28
|
+
/** Provides a custom retry policy to every OPNsense API call below it. */
|
|
29
|
+
export const policy: {
|
|
30
|
+
(
|
|
31
|
+
options: Options,
|
|
32
|
+
): <A, E, R>(
|
|
33
|
+
effect: Effect.Effect<A, E, R>,
|
|
34
|
+
) => Effect.Effect<A, E, Exclude<R, Retry>>;
|
|
35
|
+
(
|
|
36
|
+
factory: Factory,
|
|
37
|
+
): <A, E, R>(
|
|
38
|
+
effect: Effect.Effect<A, E, R>,
|
|
39
|
+
) => Effect.Effect<A, E, Exclude<R, Retry>>;
|
|
40
|
+
} = (optionsOrFactory: Options | Factory) =>
|
|
41
|
+
Effect.provide(Layer.succeed(Retry, optionsOrFactory));
|
|
42
|
+
|
|
43
|
+
/** Disables all automatic retries. */
|
|
44
|
+
export const none: <A, E, R>(
|
|
45
|
+
effect: Effect.Effect<A, E, R>,
|
|
46
|
+
) => Effect.Effect<A, E, Exclude<R, Retry>> = Effect.provide(
|
|
47
|
+
Layer.succeed(Retry, { while: () => false }),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The default retry policy (core's): transient/throttling/retryable errors,
|
|
52
|
+
* capped exponential backoff with jitter, server `retryAfter` hints honored
|
|
53
|
+
* with precedence.
|
|
54
|
+
*
|
|
55
|
+
* OPNsense has no rate-limit surface of its own to honor a hint FROM — this
|
|
56
|
+
* is here purely for the transient network/5xx case (a PHP fault, a proxy
|
|
57
|
+
* hiccup in front of the box). A `ValidationFailed`/`OpnsenseUserError`
|
|
58
|
+
* business-rule denial is never retryable and this policy will not retry
|
|
59
|
+
* it (see `errors.ts`'s `Category.withBadRequestError` categorization).
|
|
60
|
+
*/
|
|
61
|
+
export const makeDefault: Factory = Retries.makeDefault;
|
|
62
|
+
|
|
63
|
+
export const jittered = Retries.jittered;
|
|
64
|
+
export const capped = Retries.capped;
|
|
65
|
+
|
|
66
|
+
/** Retry options that retry all throttling errors indefinitely. */
|
|
67
|
+
export const throttlingOptions: Options = Retries.throttlingOptions;
|
|
68
|
+
|
|
69
|
+
/** Retries all throttling errors indefinitely (honoring server hints). */
|
|
70
|
+
export const throttling: <A, E, R>(
|
|
71
|
+
effect: Effect.Effect<A, E, R>,
|
|
72
|
+
) => Effect.Effect<A, E, Exclude<R, Retry>> = policy(Retries.throttlingFactory);
|
|
73
|
+
|
|
74
|
+
/** Retry options that retry all transient errors indefinitely. */
|
|
75
|
+
export const transientOptions: Options = Retries.transientOptions;
|
|
76
|
+
|
|
77
|
+
/** Retries all transient errors indefinitely (honoring server hints). */
|
|
78
|
+
export const transient: <A, E, R>(
|
|
79
|
+
effect: Effect.Effect<A, E, R>,
|
|
80
|
+
) => Effect.Effect<A, E, Exclude<R, Retry>> = policy(Retries.transientFactory);
|