@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.
Files changed (78) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +95 -0
  3. package/dist/credentials.d.ts +39 -0
  4. package/dist/credentials.d.ts.map +1 -0
  5. package/dist/credentials.js +78 -0
  6. package/dist/credentials.js.map +1 -0
  7. package/dist/errors.d.ts +110 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +63 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +29 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +29 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/protocol.d.ts +13 -0
  16. package/dist/protocol.d.ts.map +1 -0
  17. package/dist/protocol.js +115 -0
  18. package/dist/protocol.js.map +1 -0
  19. package/dist/retry.d.ts +56 -0
  20. package/dist/retry.d.ts.map +1 -0
  21. package/dist/retry.js +49 -0
  22. package/dist/retry.js.map +1 -0
  23. package/dist/services/firewall_alias.d.ts +243 -0
  24. package/dist/services/firewall_alias.d.ts.map +1 -0
  25. package/dist/services/firewall_alias.js +222 -0
  26. package/dist/services/firewall_alias.js.map +1 -0
  27. package/dist/services/firewall_category.d.ts +122 -0
  28. package/dist/services/firewall_category.d.ts.map +1 -0
  29. package/dist/services/firewall_category.js +161 -0
  30. package/dist/services/firewall_category.js.map +1 -0
  31. package/dist/services/firewall_filter.d.ts +289 -0
  32. package/dist/services/firewall_filter.d.ts.map +1 -0
  33. package/dist/services/firewall_filter.js +225 -0
  34. package/dist/services/firewall_filter.js.map +1 -0
  35. package/dist/services/firewall_group.d.ts +122 -0
  36. package/dist/services/firewall_group.d.ts.map +1 -0
  37. package/dist/services/firewall_group.js +154 -0
  38. package/dist/services/firewall_group.js.map +1 -0
  39. package/dist/services/index.d.ts +9 -0
  40. package/dist/services/index.d.ts.map +1 -0
  41. package/dist/services/index.js +10 -0
  42. package/dist/services/index.js.map +1 -0
  43. package/dist/services/quagga_bgp.d.ts +1135 -0
  44. package/dist/services/quagga_bgp.d.ts.map +1 -0
  45. package/dist/services/quagga_bgp.js +1263 -0
  46. package/dist/services/quagga_bgp.js.map +1 -0
  47. package/dist/services/quagga_general.d.ts +55 -0
  48. package/dist/services/quagga_general.d.ts.map +1 -0
  49. package/dist/services/quagga_general.js +48 -0
  50. package/dist/services/quagga_general.js.map +1 -0
  51. package/dist/services/quagga_service.d.ts +60 -0
  52. package/dist/services/quagga_service.d.ts.map +1 -0
  53. package/dist/services/quagga_service.js +77 -0
  54. package/dist/services/quagga_service.js.map +1 -0
  55. package/dist/services/routing_settings.d.ts +192 -0
  56. package/dist/services/routing_settings.d.ts.map +1 -0
  57. package/dist/services/routing_settings.js +169 -0
  58. package/dist/services/routing_settings.js.map +1 -0
  59. package/dist/traits.d.ts +11 -0
  60. package/dist/traits.d.ts.map +1 -0
  61. package/dist/traits.js +11 -0
  62. package/dist/traits.js.map +1 -0
  63. package/package.json +75 -0
  64. package/src/credentials.ts +111 -0
  65. package/src/errors.ts +115 -0
  66. package/src/index.ts +32 -0
  67. package/src/protocol.ts +177 -0
  68. package/src/retry.ts +80 -0
  69. package/src/services/firewall_alias.ts +572 -0
  70. package/src/services/firewall_category.ts +359 -0
  71. package/src/services/firewall_filter.ts +671 -0
  72. package/src/services/firewall_group.ts +350 -0
  73. package/src/services/index.ts +9 -0
  74. package/src/services/quagga_bgp.ts +2955 -0
  75. package/src/services/quagga_general.ts +132 -0
  76. package/src/services/quagga_service.ts +182 -0
  77. package/src/services/routing_settings.ts +419 -0
  78. package/src/traits.ts +36 -0
@@ -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);