@homeflare/alchemy 0.50.0 → 0.51.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/dist/unifi/acl-rule-ordering.d.ts +6 -1
- package/dist/unifi/acl-rule-ordering.d.ts.map +1 -1
- package/dist/unifi/acl-rule.d.ts +6 -1
- package/dist/unifi/acl-rule.d.ts.map +1 -1
- package/dist/unifi/dns-policy.d.ts +6 -1
- package/dist/unifi/dns-policy.d.ts.map +1 -1
- package/dist/unifi/fake-unifi.d.ts +4 -0
- package/dist/unifi/fake-unifi.d.ts.map +1 -1
- package/dist/unifi/firewall-policy-ordering.d.ts +6 -1
- package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -1
- package/dist/unifi/firewall-policy.d.ts +6 -1
- package/dist/unifi/firewall-policy.d.ts.map +1 -1
- package/dist/unifi/firewall-zone.d.ts +6 -1
- package/dist/unifi/firewall-zone.d.ts.map +1 -1
- package/dist/unifi/index.d.ts +5 -3
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +339 -86
- package/dist/unifi/index.js.map +10 -6
- package/dist/unifi/network-scope.d.ts +31 -0
- package/dist/unifi/network-scope.d.ts.map +1 -0
- package/dist/unifi/network-update.d.ts +44 -0
- package/dist/unifi/network-update.d.ts.map +1 -0
- package/dist/unifi/network.d.ts +32 -12
- package/dist/unifi/network.d.ts.map +1 -1
- package/dist/unifi/policy.d.ts +89 -1
- package/dist/unifi/policy.d.ts.map +1 -1
- package/dist/unifi/resource.d.ts +17 -43
- package/dist/unifi/resource.d.ts.map +1 -1
- package/dist/unifi/traffic-matching-list.d.ts +6 -1
- package/dist/unifi/traffic-matching-list.d.ts.map +1 -1
- package/dist/unifi/update-reconcile.d.ts +31 -0
- package/dist/unifi/update-reconcile.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast.d.ts +6 -1
- package/dist/unifi/wifi-broadcast.d.ts.map +1 -1
- package/dist/unifi/wire-guard.d.ts +33 -0
- package/dist/unifi/wire-guard.d.ts.map +1 -0
- package/docs/unifi-network-write.md +90 -0
- package/docs/unifi.md +36 -36
- package/package.json +1 -1
- package/src/unifi/fake-unifi.ts +15 -2
- package/src/unifi/handler-allow-news.test.ts +80 -0
- package/src/unifi/index.ts +21 -5
- package/src/unifi/network-scope.test.ts +203 -0
- package/src/unifi/network-scope.ts +75 -0
- package/src/unifi/network-update.test.ts +250 -0
- package/src/unifi/network-update.ts +95 -0
- package/src/unifi/network.test.ts +27 -1
- package/src/unifi/network.ts +56 -12
- package/src/unifi/policy.ts +128 -10
- package/src/unifi/resource.test.ts +15 -3
- package/src/unifi/resource.ts +64 -105
- package/src/unifi/update-reconcile.ts +126 -0
- package/src/unifi/wire-guard-anchor.test.ts +198 -0
- package/src/unifi/wire-guard-round2.test.ts +120 -0
- package/src/unifi/wire-guard.test.ts +218 -0
- package/src/unifi/wire-guard.ts +192 -0
- package/src/unifi/write-op-reference.test.ts +38 -2
- package/src/unifi/get-only-guard.test.ts +0 -165
package/dist/unifi/policy.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as Effect from 'effect/Effect';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const UNIFI_WRITE_POLICY: string;
|
|
3
|
+
/** @deprecated renamed `UNIFI_WRITE_POLICY`; kept so existing consumer imports do not break. */
|
|
4
|
+
export declare const UNIFI_READ_ONLY_POLICY: string;
|
|
3
5
|
/** What kind of write was asked for and refused — for the message only, never acted on. */
|
|
4
6
|
export type UnifiWriteAction = 'create' | 'update' | 'delete';
|
|
5
7
|
declare const UnifiWriteRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
@@ -16,5 +18,91 @@ export declare class UnifiWriteRefused extends UnifiWriteRefused_base<{
|
|
|
16
18
|
}
|
|
17
19
|
/** The only thing `reconcile`/`delete` ever return for a write this family cannot make. */
|
|
18
20
|
export declare const refuseWrite: (type: string, identity: string, action: UnifiWriteAction) => Effect.Effect<never, UnifiWriteRefused, never>;
|
|
21
|
+
declare const UnifiLiveDriftedSinceDeploy_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
22
|
+
readonly _tag: "UnifiLiveDriftedSinceDeploy";
|
|
23
|
+
} & Readonly<A>;
|
|
24
|
+
/** Live no longer equals what the last deploy wrote: a hand edit is never overwritten. */
|
|
25
|
+
export declare class UnifiLiveDriftedSinceDeploy extends UnifiLiveDriftedSinceDeploy_base<{
|
|
26
|
+
readonly type: string;
|
|
27
|
+
readonly identity: string;
|
|
28
|
+
readonly fields: ReadonlyArray<string>;
|
|
29
|
+
}> {
|
|
30
|
+
get message(): string;
|
|
31
|
+
}
|
|
32
|
+
declare const UnifiUpdateWouldBeNoop_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
33
|
+
readonly _tag: "UnifiUpdateWouldBeNoop";
|
|
34
|
+
} & Readonly<A>;
|
|
35
|
+
/** The merged body equals live: a PUT would change nothing (keeps a re-PUT loop impossible). */
|
|
36
|
+
export declare class UnifiUpdateWouldBeNoop extends UnifiUpdateWouldBeNoop_base<{
|
|
37
|
+
readonly type: string;
|
|
38
|
+
readonly identity: string;
|
|
39
|
+
}> {
|
|
40
|
+
get message(): string;
|
|
41
|
+
}
|
|
42
|
+
declare const UnifiUpdateDidNotConverge_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
43
|
+
readonly _tag: "UnifiUpdateDidNotConverge";
|
|
44
|
+
} & Readonly<A>;
|
|
45
|
+
/** The PUT answered, yet the response still differs from the declaration. */
|
|
46
|
+
export declare class UnifiUpdateDidNotConverge extends UnifiUpdateDidNotConverge_base<{
|
|
47
|
+
readonly type: string;
|
|
48
|
+
readonly identity: string;
|
|
49
|
+
readonly fields: ReadonlyArray<string>;
|
|
50
|
+
}> {
|
|
51
|
+
get message(): string;
|
|
52
|
+
}
|
|
53
|
+
declare const UnifiFieldNotRemovable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
54
|
+
readonly _tag: "UnifiFieldNotRemovable";
|
|
55
|
+
} & Readonly<A>;
|
|
56
|
+
/** A patch would remove a key outside the removable allowlist (an omitted block is turned off). */
|
|
57
|
+
export declare class UnifiFieldNotRemovable extends UnifiFieldNotRemovable_base<{
|
|
58
|
+
readonly type: string;
|
|
59
|
+
readonly identity: string;
|
|
60
|
+
readonly fields: ReadonlyArray<string>;
|
|
61
|
+
}> {
|
|
62
|
+
get message(): string;
|
|
63
|
+
}
|
|
64
|
+
declare const UnifiFieldNotSettable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
65
|
+
readonly _tag: "UnifiFieldNotSettable";
|
|
66
|
+
} & Readonly<A>;
|
|
67
|
+
/** A patch would SET a key outside the settable allowlist (only `ipv6Configuration` may be written). */
|
|
68
|
+
export declare class UnifiFieldNotSettable extends UnifiFieldNotSettable_base<{
|
|
69
|
+
readonly type: string;
|
|
70
|
+
readonly identity: string;
|
|
71
|
+
readonly fields: ReadonlyArray<string>;
|
|
72
|
+
}> {
|
|
73
|
+
get message(): string;
|
|
74
|
+
}
|
|
75
|
+
declare const UnifiManagementNetworkRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
76
|
+
readonly _tag: "UnifiManagementNetworkRefused";
|
|
77
|
+
} & Readonly<A>;
|
|
78
|
+
/** The live object is the site's default (management) network: never written. */
|
|
79
|
+
export declare class UnifiManagementNetworkRefused extends UnifiManagementNetworkRefused_base<{
|
|
80
|
+
readonly type: string;
|
|
81
|
+
readonly identity: string;
|
|
82
|
+
}> {
|
|
83
|
+
get message(): string;
|
|
84
|
+
}
|
|
85
|
+
declare const UnifiImmutableFieldChanged_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
86
|
+
readonly _tag: "UnifiImmutableFieldChanged";
|
|
87
|
+
} & Readonly<A>;
|
|
88
|
+
/** A patch changes a field that decides where the network lives or what serves it. */
|
|
89
|
+
export declare class UnifiImmutableFieldChanged extends UnifiImmutableFieldChanged_base<{
|
|
90
|
+
readonly type: string;
|
|
91
|
+
readonly identity: string;
|
|
92
|
+
readonly fields: ReadonlyArray<string>;
|
|
93
|
+
}> {
|
|
94
|
+
get message(): string;
|
|
95
|
+
}
|
|
96
|
+
declare const UnifiIdentityChanged_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
97
|
+
readonly _tag: "UnifiIdentityChanged";
|
|
98
|
+
} & Readonly<A>;
|
|
99
|
+
/** `olds` and `news` name different objects: the prior state is no baseline for this write. */
|
|
100
|
+
export declare class UnifiIdentityChanged extends UnifiIdentityChanged_base<{
|
|
101
|
+
readonly type: string;
|
|
102
|
+
readonly identity: string;
|
|
103
|
+
readonly previous: string;
|
|
104
|
+
}> {
|
|
105
|
+
get message(): string;
|
|
106
|
+
}
|
|
19
107
|
export {};
|
|
20
108
|
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/unifi/policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/unifi/policy.ts"],"names":[],"mappings":"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"}
|
package/dist/unifi/resource.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { Input } from 'alchemy/Input';
|
|
2
2
|
import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
|
|
3
3
|
import * as Effect from 'effect/Effect';
|
|
4
|
-
import * as HttpClient from 'effect/http/HttpClient';
|
|
4
|
+
import type * as HttpClient from 'effect/http/HttpClient';
|
|
5
|
+
import { type ReconcileArgs, type UnifiUpdate } from './update-reconcile.ts';
|
|
5
6
|
/** What every handler needs from the caller's runtime once `CredentialsFromEnv` is baked in. */
|
|
6
7
|
export type UnifiRequirements = HttpClient.HttpClient;
|
|
7
8
|
/**
|
|
8
9
|
* One UniFi Network object's read. `Live` is whatever the SDK decodes (a `NetworkDetails`, a
|
|
9
10
|
* `FirewallZone`, …); `E` is left to each resource file to declare (its own `fetchLive`'s error
|
|
10
11
|
* union, with `NotFound` already folded away) so the precise per-operation error union flows
|
|
11
|
-
* through instead of being widened by hand here.
|
|
12
|
+
* through instead of being widened by hand here. `E2` is the error union of the optional write.
|
|
12
13
|
*/
|
|
13
|
-
export type UnifiSpec<Props extends object, Live, Attributes extends object, E> = {
|
|
14
|
+
export type UnifiSpec<Props extends object, Live, Attributes extends object, E, E2 = never> = {
|
|
14
15
|
/** The vendor type string, e.g. `Unifi.Network` — for the refusal message only. */
|
|
15
16
|
readonly type: string;
|
|
16
17
|
/** Already folds a genuine not-found to `undefined`; nothing else is folded (see header). */
|
|
@@ -19,8 +20,10 @@ export type UnifiSpec<Props extends object, Live, Attributes extends object, E>
|
|
|
19
20
|
readonly matches: (attributes: Attributes, props: Props) => boolean;
|
|
20
21
|
/** For the refusal message and read-back failures. */
|
|
21
22
|
readonly describe: (props: Props) => string;
|
|
23
|
+
/** Present only on a family allowed to write (`Unifi.Network`); see `update-reconcile.ts`. */
|
|
24
|
+
readonly update?: UnifiUpdate<Props, Live, E2>;
|
|
22
25
|
};
|
|
23
|
-
export declare const unifiOperations: <Props extends object, Live, Attributes extends object, E>(spec: UnifiSpec<Props, Live, Attributes, E>) => {
|
|
26
|
+
export declare const unifiOperations: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>) => {
|
|
24
27
|
read: (props: Props) => Effect.Effect<Attributes | undefined, E, UnifiNetworkOpContext>;
|
|
25
28
|
readHandler: ({ olds, output }: {
|
|
26
29
|
olds: Props;
|
|
@@ -31,51 +34,17 @@ export declare const unifiOperations: <Props extends object, Live, Attributes ex
|
|
|
31
34
|
} | {
|
|
32
35
|
readonly action: 'noop';
|
|
33
36
|
} | undefined, E, UnifiNetworkOpContext>;
|
|
34
|
-
|
|
37
|
+
/** Pre-state callers (tests, adopt-only probes) pass just `news`: `olds`/`output` unset. */
|
|
38
|
+
reconcile: (news: Props, rest?: Partial<Omit<ReconcileArgs<Props>, 'news'>>) => Effect.Effect<Attributes, E | E2 | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
|
|
35
39
|
destroy: (olds: Props) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, never>;
|
|
36
40
|
};
|
|
37
41
|
/**
|
|
38
|
-
*
|
|
39
|
-
* `reconcile`/`destroy` — every path this engine exposes already returns that refusal instead of
|
|
40
|
-
* calling an SDK write op. This guard stops the same thing one layer lower, AT THE WIRE: it wraps
|
|
41
|
-
* whatever `HttpClient` the caller already provides (the stack's `FetchHttpClient.layer` in
|
|
42
|
-
* production, `fakeUnifiLayer` in tests — `openbao/bao-http.ts`'s header names this same
|
|
43
|
-
* "wrap, don't replace" seam) so a future resource file that, by mistake, called an SDK write
|
|
44
|
-
* operation directly — bypassing `unifiOperations` entirely — would still never reach the vendor
|
|
45
|
-
* API. `write-op-reference.test.ts` proves no such call exists today; this is what stops one from
|
|
46
|
-
* ever taking effect if one is ever added.
|
|
47
|
-
*
|
|
48
|
-
* ⚠️ `Effect.die`, NOT A TYPED FAILURE — classified per distilled-doctrine's "classify every
|
|
49
|
-
* die/orDie". Every generated SDK operation (`networks.getNetworkDetails`, …) declares its OWN
|
|
50
|
-
* closed error union from the pinned OpenAPI spec, and generated files are never hand-edited to
|
|
51
|
-
* add a member to it. A typed `Effect.fail` here would be a failure mode absent from every op's
|
|
52
|
-
* declared type the moment it occurred — a type-system lie. A defect is the sound way to add a
|
|
53
|
-
* NEW "this must be structurally impossible" failure underneath types this package does not
|
|
54
|
-
* own, exactly like `openbao`'s `refuse()` helpers' `Effect.die(new Error(...))` for a
|
|
55
|
-
* configuration move that should never be reachable.
|
|
56
|
-
*/
|
|
57
|
-
export declare class UnifiNonGetRequest extends Error {
|
|
58
|
-
readonly method: string;
|
|
59
|
-
/** PATH ONLY, never the full URL — see `requestPath` below. */
|
|
60
|
-
readonly path: string;
|
|
61
|
-
constructor(method: string,
|
|
62
|
-
/** PATH ONLY, never the full URL — see `requestPath` below. */
|
|
63
|
-
path: string);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* A `Layer` that reads whatever `HttpClient` is already in the calling context and replaces it,
|
|
67
|
-
* for everything downstream, with `guardGetOnly`'s wrapped version (`HttpClient.layerMergedContext`
|
|
68
|
-
* — see the guard above). Exported so `get-only-guard.test.ts` can prove the mechanism directly,
|
|
69
|
-
* against a bare `HttpClient` service, without needing `CredentialsFromEnv` or any env var at all.
|
|
70
|
-
*/
|
|
71
|
-
export declare const GetOnlyHttpClient: import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
|
|
72
|
-
/**
|
|
73
|
-
* The four provider handlers for a spec'd read-only UniFi object, wired once.
|
|
42
|
+
* The four provider handlers for a spec'd UniFi object, wired once.
|
|
74
43
|
*
|
|
75
44
|
* ⛔ `list` ANSWERS EMPTY. `GET /v1/sites/{siteId}/networks` answers every network on the site;
|
|
76
45
|
* adoption stays explicit, the same reasoning `Proxmox.User`'s and NetBox's `list` give.
|
|
77
46
|
*/
|
|
78
|
-
export declare const unifiHandlers: <Props extends object, Live, Attributes extends object, E>(spec: UnifiSpec<Props, Live, Attributes, E>) => {
|
|
47
|
+
export declare const unifiHandlers: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>) => {
|
|
79
48
|
list: () => Effect.Effect<never[], never, never>;
|
|
80
49
|
read: (args: {
|
|
81
50
|
olds: Props;
|
|
@@ -91,7 +60,12 @@ export declare const unifiHandlers: <Props extends object, Live, Attributes exte
|
|
|
91
60
|
} | undefined, E, HttpClient.HttpClient>;
|
|
92
61
|
reconcile: (args: {
|
|
93
62
|
news: Props;
|
|
94
|
-
|
|
63
|
+
olds: Props | undefined;
|
|
64
|
+
output: Attributes | undefined;
|
|
65
|
+
session: {
|
|
66
|
+
note: (message: string) => Effect.Effect<void, never, never>;
|
|
67
|
+
};
|
|
68
|
+
}) => Effect.Effect<Attributes, E | E2 | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
|
|
95
69
|
delete: (args: {
|
|
96
70
|
olds: Props;
|
|
97
71
|
}) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,7 +42,12 @@ export declare const handlers: {
|
|
|
42
42
|
} | undefined, trafficMatchingLists.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
|
|
43
43
|
reconcile: (args: {
|
|
44
44
|
news: TrafficMatchingListProps;
|
|
45
|
-
|
|
45
|
+
olds: TrafficMatchingListProps | undefined;
|
|
46
|
+
output: TrafficMatchingListAttributes | undefined;
|
|
47
|
+
session: {
|
|
48
|
+
note: (message: string) => Effect.Effect<void, never, never>;
|
|
49
|
+
};
|
|
50
|
+
}) => Effect.Effect<TrafficMatchingListAttributes, import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused | trafficMatchingLists.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
|
|
46
51
|
delete: (args: {
|
|
47
52
|
olds: TrafficMatchingListProps;
|
|
48
53
|
}) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/http/HttpClient").HttpClient>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traffic-matching-list.d.ts","sourceRoot":"","sources":["../../src/unifi/traffic-matching-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,uDAAuD,CAAC;AAC9F,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAE9B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EACV,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAE3D,MAAM,WAAW,wBAAyB,SAAQ,QAAQ,CACxD,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,EAC7B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,wBAAwB,2DAGpC,CAAC;AAEF;oBACoB;AACpB,eAAO,MAAM,mBAAmB,OAAQ,MAAM,SAAS,wBAAwB,gGACxB,CAAC;AAExD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,wBAAwB,EACxB,oBAAoB,CAAC,mBAAmB,EACxC,6BAA6B,EAC7B,oBAAoB,CAAC,2BAA2B,CAcjD,CAAC;AAEF,eAAO,MAAM,QAAQ
|
|
1
|
+
{"version":3,"file":"traffic-matching-list.d.ts","sourceRoot":"","sources":["../../src/unifi/traffic-matching-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,uDAAuD,CAAC;AAC9F,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAE9B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EACV,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAE3D,MAAM,WAAW,wBAAyB,SAAQ,QAAQ,CACxD,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,EAC7B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,wBAAwB,2DAGpC,CAAC;AAEF;oBACoB;AACpB,eAAO,MAAM,mBAAmB,OAAQ,MAAM,SAAS,wBAAwB,gGACxB,CAAC;AAExD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,wBAAwB,EACxB,oBAAoB,CAAC,mBAAmB,EACxC,6BAA6B,EAC7B,oBAAoB,CAAC,2BAA2B,CAcjD,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,gCAAgC,qIAI1C,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import type { FieldDrift } from './drift.ts';
|
|
4
|
+
import { UnifiIdentityChanged, UnifiLiveDriftedSinceDeploy, UnifiUpdateDidNotConverge } from './policy.ts';
|
|
5
|
+
import type { UnifiSpec } from './resource.ts';
|
|
6
|
+
import type { AllowedWrite } from './wire-guard.ts';
|
|
7
|
+
export type Patch<Props> = {
|
|
8
|
+
set: Partial<Props>;
|
|
9
|
+
unset: ReadonlyArray<keyof Props>;
|
|
10
|
+
};
|
|
11
|
+
/** The write half of a spec: everything needed to PUT one object, and nothing to create/delete. */
|
|
12
|
+
export type UnifiUpdate<Props extends object, Live, E2> = {
|
|
13
|
+
/** The one PUT the wire guard may pass for this row (path pinned to the row's own ids). */
|
|
14
|
+
readonly allowedWrite: (props: Props) => AllowedWrite;
|
|
15
|
+
/** Declarable keys; never the identity keys. */
|
|
16
|
+
readonly patchKeys: ReadonlyArray<keyof Props>;
|
|
17
|
+
readonly driftOf: (live: Live, props: Props) => ReadonlyArray<FieldDrift>;
|
|
18
|
+
/** Refuses (typed, before any note or request) a patch outside this family's write scope. */
|
|
19
|
+
readonly checkScope: (live: Live, patch: Patch<Props>, props: Props) => Effect.Effect<void, E2>;
|
|
20
|
+
readonly write: (live: Live, patch: Patch<Props>, props: Props) => Effect.Effect<Live, E2, UnifiNetworkOpContext>;
|
|
21
|
+
};
|
|
22
|
+
/** Keys whose `news` value differs from `olds`: set when still declared, unset when dropped. */
|
|
23
|
+
export declare const computePatch: <Props extends object>(news: Props, olds: Props, patchKeys: ReadonlyArray<keyof Props>) => Patch<Props>;
|
|
24
|
+
export type ReconcileArgs<Props extends object> = {
|
|
25
|
+
readonly news: Props;
|
|
26
|
+
readonly olds: Props | undefined;
|
|
27
|
+
readonly output: unknown;
|
|
28
|
+
readonly note: (message: string) => Effect.Effect<void>;
|
|
29
|
+
};
|
|
30
|
+
export declare const updateReconcile: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>, { news, olds, output, note }: ReconcileArgs<Props>) => Effect.Effect<Attributes, E | E2 | UnifiIdentityChanged | UnifiLiveDriftedSinceDeploy | UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
|
|
31
|
+
//# sourceMappingURL=update-reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-reconcile.d.ts","sourceRoot":"","sources":["../../src/unifi/update-reconcile.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,oBAAoB,EACpB,2BAA2B,EAC3B,yBAAyB,EAE1B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,MAAM,KAAK,CAAC,KAAK,IAAI;IAAE,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC,MAAM,KAAK,CAAC,CAAA;CAAE,CAAC;AAEtF,mGAAmG;AACnG,MAAM,MAAM,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI;IACxD,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,YAAY,CAAC;IACtD,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,MAAM,KAAK,CAAC,CAAC;IAC/C,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC,UAAU,CAAC,CAAC;IAC1E,6FAA6F;IAC7F,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAChG,QAAQ,CAAC,KAAK,EAAE,CACd,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EACnB,KAAK,EAAE,KAAK,KACT,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,qBAAqB,CAAC,CAAC;CACrD,CAAC;AAEF,gGAAgG;AAChG,eAAO,MAAM,YAAY,GAAI,KAAK,SAAS,MAAM,QACzC,KAAK,QACL,KAAK,aACA,aAAa,CAAC,MAAM,KAAK,CAAC,KACpC,KAAK,CAAC,KAAK,CASb,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI;IAChD,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACzD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,EAAE,EAAE,QACpF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC,gCACjB,aAAa,CAAC,KAAK,CAAC,wLA2ChD,CAAC"}
|
|
@@ -64,7 +64,12 @@ export declare const handlers: {
|
|
|
64
64
|
} | undefined, UnifiPaginationInconsistent | wifiBroadcasts.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
|
|
65
65
|
reconcile: (args: {
|
|
66
66
|
news: WifiBroadcastProps;
|
|
67
|
-
|
|
67
|
+
olds: WifiBroadcastProps | undefined;
|
|
68
|
+
output: WifiBroadcastAttributes | undefined;
|
|
69
|
+
session: {
|
|
70
|
+
note: (message: string) => Effect.Effect<void, never, never>;
|
|
71
|
+
};
|
|
72
|
+
}) => Effect.Effect<WifiBroadcastAttributes, import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | UnifiPaginationInconsistent | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused | wifiBroadcasts.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
|
|
68
73
|
delete: (args: {
|
|
69
74
|
olds: WifiBroadcastProps;
|
|
70
75
|
}) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/http/HttpClient").HttpClient>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wifi-broadcast.d.ts","sourceRoot":"","sources":["../../src/unifi/wifi-broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,cAAc,MAAM,gDAAgD,CAAC;AACjF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAExB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,KAAK,2BAA2B,EAAW,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpD,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AAEH,kGAAkG;AAClG,eAAO,MAAM,aAAa,OAAQ,MAAM,SAAS,kBAAkB,0FAClB,CAAC;AAElD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,kBAAkB,EAClB,cAAc,CAAC,qBAAqB,EACpC,uBAAuB,EACvB,cAAc,CAAC,yBAAyB,GAAG,2BAA2B,CAWvE,CAAC;AAEF,eAAO,MAAM,QAAQ
|
|
1
|
+
{"version":3,"file":"wifi-broadcast.d.ts","sourceRoot":"","sources":["../../src/unifi/wifi-broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,cAAc,MAAM,gDAAgD,CAAC;AACjF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAExB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,KAAK,2BAA2B,EAAW,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpD,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AAEH,kGAAkG;AAClG,eAAO,MAAM,aAAa,OAAQ,MAAM,SAAS,kBAAkB,0FAClB,CAAC;AAElD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,kBAAkB,EAClB,cAAc,CAAC,qBAAqB,EACpC,uBAAuB,EACvB,cAAc,CAAC,yBAAyB,GAAG,2BAA2B,CAWvE,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,0BAA0B,+HACwD,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as HttpClient from 'effect/http/HttpClient';
|
|
2
|
+
/**
|
|
3
|
+
* One non-GET request the guard may let through. `tail` is the exact route below the configured
|
|
4
|
+
* base URL (`/v1/sites/<site>/networks/<id>`): the guard compares the request's origin and path to
|
|
5
|
+
* `baseUrl + tail` and requires an empty query and fragment (`isAllowed`).
|
|
6
|
+
*/
|
|
7
|
+
export type AllowedWrite = {
|
|
8
|
+
readonly method: 'PUT';
|
|
9
|
+
readonly tail: string;
|
|
10
|
+
};
|
|
11
|
+
export declare class UnifiRefusedRequest extends Error {
|
|
12
|
+
readonly method: string;
|
|
13
|
+
/** PATH ONLY, never the full URL — see `requestPath` below. */
|
|
14
|
+
readonly path: string;
|
|
15
|
+
constructor(method: string,
|
|
16
|
+
/** PATH ONLY, never the full URL — see `requestPath` below. */
|
|
17
|
+
path: string, detail?: string);
|
|
18
|
+
}
|
|
19
|
+
/** @deprecated renamed `UnifiRefusedRequest`; kept so existing consumer imports do not break. */
|
|
20
|
+
export declare const UnifiNonGetRequest: typeof UnifiRefusedRequest;
|
|
21
|
+
export type UnifiNonGetRequest = UnifiRefusedRequest;
|
|
22
|
+
/**
|
|
23
|
+
* A `Layer` that reads whatever `HttpClient` is already in the calling context and replaces it,
|
|
24
|
+
* for everything downstream, with the guarded version. Exported so `wire-guard.test.ts` can prove
|
|
25
|
+
* the mechanism against a bare `HttpClient`, without `CredentialsFromEnv` or any env var.
|
|
26
|
+
*/
|
|
27
|
+
export declare const guardedHttpClient: (allow: ReadonlyArray<AllowedWrite>, baseUrl?: string) => import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
|
|
28
|
+
/**
|
|
29
|
+
* No write may pass; the posture of `read`/`diff`/`delete` and of every non-updatable family. A 3xx
|
|
30
|
+
* answer to a GET is refused here exactly as under the allow-list posture — see REDIRECTS above.
|
|
31
|
+
*/
|
|
32
|
+
export declare const GetOnlyHttpClient: import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
|
|
33
|
+
//# sourceMappingURL=wire-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-guard.d.ts","sourceRoot":"","sources":["../../src/unifi/wire-guard.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAKrD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHvB,YACW,MAAM,EAAE,MAAM;IACvB,+DAA+D;IACtD,IAAI,EAAE,MAAM,EACrB,MAAM,SAAiC,EASxC;CACF;AAED,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,4BAAsB,CAAC;AACtD,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AA0FrD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,UAAW,aAAa,CAAC,YAAY,CAAC,YAAY,MAAM,sFASrF,CAAC;AAkBF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,mFAAwB,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# `Unifi.Network` write scope — what the one PUT may and may not do
|
|
2
|
+
|
|
3
|
+
`docs/unifi.md` describes the three-way update. This page records the limits placed on it after
|
|
4
|
+
the red team rounds 1 and 2 of PR 377. The only intended consumer is ONE VLAN's
|
|
5
|
+
`ipv6Configuration`: a patch may SET only `ipv6Configuration` and REMOVE only `ipv6Configuration`;
|
|
6
|
+
everything else a whole-object `PUT` could reach is refused with a typed error that carries field
|
|
7
|
+
names only, never values.
|
|
8
|
+
|
|
9
|
+
| refusal | when |
|
|
10
|
+
| ------------------------------- | --------------------------------------------------------------------------- |
|
|
11
|
+
| `UnifiFieldNotSettable` | a patch sets any key but `ipv6Configuration` (`enabled`, `dhcpGuarding`, …) |
|
|
12
|
+
| `UnifiFieldNotRemovable` | a patch drops any key but `ipv6Configuration` |
|
|
13
|
+
| `UnifiManagementNetworkRefused` | the live object has `default: true` (the management LAN) |
|
|
14
|
+
| `UnifiImmutableFieldChanged` | a patch sets or drops `zoneId`, `vlanId`, `management`, `deviceId` |
|
|
15
|
+
| `UnifiIdentityChanged` | `olds` names another `siteId`/`networkId` than `news` |
|
|
16
|
+
| `UnifiUpdateWouldBeNoop` | the merged body equals live |
|
|
17
|
+
|
|
18
|
+
Every one of them fires after the live GET and before any PUT.
|
|
19
|
+
|
|
20
|
+
⛔ **Why a removable allowlist.** A key in `patch.unset` is deleted from the PUT body, and the
|
|
21
|
+
controller reads an omitted `dhcpGuarding`/`ipv4Configuration`/`ipv6Configuration` as "off". So
|
|
22
|
+
leaving a field out of a declaration would turn it off on the console. Only `ipv6Configuration`
|
|
23
|
+
(the block this path exists for, and the revert target) may be removed. `network-scope.ts` holds
|
|
24
|
+
the lists (`SETTABLE_KEYS`, `REMOVABLE_KEYS`, `IMMUTABLE_KEYS`); widening them is a kit change.
|
|
25
|
+
|
|
26
|
+
⚠️ **No version check: a hand edit between the read and the write is overwritten.** The PUT is the
|
|
27
|
+
live GET plus the patch, sent whole; the API offers no ETag or revision, so a change made on the
|
|
28
|
+
console between that GET and the PUT is replaced by the GET's copy. The window is one reconcile.
|
|
29
|
+
|
|
30
|
+
Non-USER `metadata.origin` is deliberately not handled: there is no rule for it yet.
|
|
31
|
+
|
|
32
|
+
## The wire guard (`wire-guard.ts`)
|
|
33
|
+
|
|
34
|
+
⛔ **The allowed PUT is anchored to the configured base URL**, not matched by suffix. The request
|
|
35
|
+
must share the base's origin, its path must be exactly `<base path>/v1/sites/<site>/networks/<id>`,
|
|
36
|
+
and it must carry no query string or fragment. `?force=true`, a nested
|
|
37
|
+
`/v1/sites/OTHER/v1/sites/s/networks/n`, a different base path and `//v1/...` all die before the
|
|
38
|
+
transport. `unifiHandlers` reads the base URL from the same `Credentials` the SDK uses; with none
|
|
39
|
+
the guard allows nothing.
|
|
40
|
+
|
|
41
|
+
⛔ **`redirect: 'manual'` is set per request, on the calling fiber's `RequestInit`, for EVERY
|
|
42
|
+
guarded request — GETs included — and any 3xx answer dies with `UnifiRefusedRequest`, whatever the
|
|
43
|
+
method.** Effect 4.0.1 `layerMergedContext` lets an outer `FetchHttpClient.RequestInit` REPLACE one
|
|
44
|
+
the layer provides, so a layer-provided `redirect: 'manual'` is silently lost under any consumer
|
|
45
|
+
that sets its own `RequestInit`. The guard reads the fiber's value and merges `redirect: 'manual'`
|
|
46
|
+
over it, the `openbao/bao-http.ts` `overSocket` pattern. `wire-guard-anchor.test.ts` proves it
|
|
47
|
+
against a real `Bun.serve` answering 301/302/303/307/308.
|
|
48
|
+
|
|
49
|
+
A GET's redirect is refused for the same reason a PUT's is, under both postures: fetch would
|
|
50
|
+
follow it and re-send the request — still carrying the API-key header — to a URL the guard never
|
|
51
|
+
vetted. Before the 2026-10-10 fix only reconcile's GETs ran `manual` (a 3xx surfaced raw and failed
|
|
52
|
+
the SDK decode) while `read`/`diff` followed it silently; one rule everywhere now, fail closed.
|
|
53
|
+
|
|
54
|
+
⛔ **Where a TLS `RequestInit` must live: on the CALLING FIBER**, provided around the program with
|
|
55
|
+
`Effect.provideService(FetchHttpClient.RequestInit, { tls: … })`, not inside the fetch layer's
|
|
56
|
+
build context (`FetchHttpClient.layer.pipe(Layer.provide(…))`). The guard re-provides `RequestInit` per
|
|
57
|
+
request, merged over the fiber's value, so an option set only inside the layer is overridden on every
|
|
58
|
+
guarded request, GETs included, and a consumer wired that way silently loses its TLS option (it
|
|
59
|
+
fails closed: the handshake is refused, nothing is sent insecurely).
|
|
60
|
+
|
|
61
|
+
⛔ **Backstop: the response URL must equal the request URL.** If a consumer's own transform provides
|
|
62
|
+
`RequestInit` WITHOUT merging, `manual` is lost and fetch follows a redirect. The guard dies when
|
|
63
|
+
`res.url` differs from `HttpClientRequest.toUrl(res.request)`. The allowed-path check likewise reads
|
|
64
|
+
`toUrl`, so a `setUrlParam` query is seen and refused. Tests: `wire-guard-round2.test.ts`.
|
|
65
|
+
|
|
66
|
+
⛔ **Refusal messages redact the cloud connector Console ID**: the path segment after `/consoles/`
|
|
67
|
+
becomes `<redacted>`.
|
|
68
|
+
|
|
69
|
+
## The static check (`write-op-reference.test.ts`)
|
|
70
|
+
|
|
71
|
+
It harvests the SDK's real write-op export names straight from its own service modules (every
|
|
72
|
+
name starting `create`/`update`/`delete`/`patch`/`execute`/`remove`/`adopt`) and fails any file
|
|
73
|
+
that mentions the SDK's package specifier and contains one of those names as a token anywhere — an
|
|
74
|
+
identifier, a bracket key, a destructured binding, a re-export or a dynamic-import property access
|
|
75
|
+
all read the same way, so one scan catches every syntax form without parsing which one it is. Its
|
|
76
|
+
`WRITE_OP_ALLOWLIST` is exactly `network-update.ts` → `updateNetwork`; any other file naming that
|
|
77
|
+
op, or that file naming `createNetwork`/`deleteNetwork`, is an offense.
|
|
78
|
+
|
|
79
|
+
## The PUT is attempted once
|
|
80
|
+
|
|
81
|
+
`updateNetwork` runs under `Retry.none`. The SDK's default policy would replay a write whose first
|
|
82
|
+
attempt may already have landed; a replay then runs against a controller state this reconcile never
|
|
83
|
+
read. A failure surfaces for the operator instead. Reads keep the default policy.
|
|
84
|
+
|
|
85
|
+
## After a PUT that landed
|
|
86
|
+
|
|
87
|
+
`UnifiUpdateDidNotConverge` means the PUT LANDED and live now differs from the declaration. It is
|
|
88
|
+
not rolled back. The next deploy sees drift and answers `UnifiLiveDriftedSinceDeploy`, whose
|
|
89
|
+
message names both causes (a hand edit, or an earlier non-converging PUT). Recovery in both cases
|
|
90
|
+
is a re-import of the live object.
|
package/docs/unifi.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# UniFi Network — `@homeflare/alchemy/unifi`
|
|
2
2
|
|
|
3
|
-
⛔ **
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
stack can pass. See `packages/alchemy/src/unifi/policy.ts`.
|
|
3
|
+
⛔ **ADOPT-ONLY, EXCEPT ONE WRITE (Tim 2026-09-24, lifted for one case 2026-10-10).** Every UniFi
|
|
4
|
+
family reads and adopts; `Unifi.Network` alone may UPDATE (a three-way whole-object PUT, below).
|
|
5
|
+
Create and delete, and every other family's update, fail with a typed `UnifiWriteRefused`. Widening
|
|
6
|
+
the rule is a kit change, not a flag a stack can pass. See `packages/alchemy/src/unifi/policy.ts`.
|
|
8
7
|
|
|
9
8
|
This page covers `Unifi.Network`/`Unifi.FirewallZone` only. Every other read-only family has its
|
|
10
9
|
own doc, per family (`docs/unifi.md` would breach the 200-line cap otherwise): [`Unifi.DnsPolicy`](./unifi-dns-policy.md), [`Unifi.AclRule`/`Unifi.AclRuleOrdering`](./unifi-acl-rule.md),
|
|
@@ -59,7 +58,7 @@ credential, untouched by this family. Only reads were probed, by hand, outside t
|
|
|
59
58
|
test suite — nothing in this PR's code exercises the vendor API live, and a write path would still
|
|
60
59
|
need its own verification.
|
|
61
60
|
|
|
62
|
-
## Adopt is the default posture, and
|
|
61
|
+
## Adopt is the default posture, and the one write is a three-way Network update
|
|
63
62
|
|
|
64
63
|
`adopt(true)` is piped onto every resource by its convenience constructor (`network(...)`,
|
|
65
64
|
`firewallZone(...)`) — H5. A cold read of a live match answers `Unowned(attrs)` (H1, the
|
|
@@ -68,36 +67,36 @@ objects carry no tag or metadata field this stack could stamp), and `adopt(true)
|
|
|
68
67
|
a silent bind instead of an `OwnedBySomeoneElse` refusal — the deploy never needs `--adopt`.
|
|
69
68
|
|
|
70
69
|
⚠️ **A forced post-adoption reconcile still runs once (H6), and it still makes no write.**
|
|
71
|
-
Beta.
|
|
70
|
+
Beta.81 forces one `reconcile` call after every cold adoption whether or not `diff` said noop.
|
|
72
71
|
`resource.ts`'s `reconcile` handles this the same way `discord/resource.ts` does: it reads the
|
|
73
|
-
live object again
|
|
74
|
-
|
|
72
|
+
live object again; an exact match returns the live attributes and calls nothing, a missing object
|
|
73
|
+
refuses `create`, and drift refuses `update` unless the row has prior state (`olds` AND `output`)
|
|
74
|
+
and a `Unifi.Network` spec: then `update-reconcile.ts` refuses if live differs from `olds` (a hand
|
|
75
|
+
edit), else PUTs live-plus-the-`news≠olds`-patch (`network-update.ts`; a dropped key is removed).
|
|
76
|
+
|
|
77
|
+
The limits on that PUT (removable allowlist, management LAN, immutable fields, anchored path,
|
|
78
|
+
no retry) are in [`unifi-network-write.md`](./unifi-network-write.md).
|
|
75
79
|
|
|
76
80
|
## `list` answers `[]`
|
|
77
81
|
|
|
78
82
|
`GET /v1/sites/{siteId}/networks` answers every network on the site. Adoption stays explicit —
|
|
79
83
|
the same reasoning `Proxmox.User`'s and NetBox's own `list` give.
|
|
80
84
|
|
|
81
|
-
## Defense in depth: GET-only at the wire (2026-09-26)
|
|
82
|
-
|
|
83
|
-
`policy.ts`'s `UnifiWriteRefused` stops write INTENT at `reconcile`/`delete`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
same way, so one scan catches every syntax form without parsing which one it is.
|
|
97
|
-
`get-only-guard.test.ts` proves the wire guard's mechanism directly against a fake `HttpClient`,
|
|
98
|
-
AND that `unifiHandlers` actually installs it (a handler-level probe, not just the mechanism in
|
|
99
|
-
isolation — `network.test.ts`/`firewall-zone.test.ts` call the lower-level `unifiOperations(spec)`
|
|
100
|
-
directly and never exercise `withCredentials` at all).
|
|
85
|
+
## Defense in depth: GET-only at the wire, one per-row exception (2026-09-26, 2026-10-10)
|
|
86
|
+
|
|
87
|
+
`policy.ts`'s `UnifiWriteRefused` stops write INTENT at `reconcile`/`delete`. `wire-guard.ts`'s
|
|
88
|
+
`guardedHttpClient(allow)` (`GetOnlyHttpClient` = empty list; barrel-exported so `homeflare-network`'s
|
|
89
|
+
import-layer guard can reuse it), installed in `unifiHandlers`, stops it one layer lower, AT THE
|
|
90
|
+
WIRE: any non-`GET` dies with `UnifiRefusedRequest` (alias `UnifiNonGetRequest`; PATH only, never
|
|
91
|
+
the host — a cloud base URL embeds the Console ID) unless `(method, path)` matches the row's own
|
|
92
|
+
allow entry, `PUT /v1/sites/<siteId>/networks/<networkId>`, supplied only to `reconcile` and only
|
|
93
|
+
by a spec with `update`. The guard also sets fetch `redirect: 'manual'` on every request, GETs
|
|
94
|
+
included, and dies on any 3xx answer (fetch would re-send the request — a PUT with its body, a GET
|
|
95
|
+
with the API key — to a URL the guard never saw).
|
|
96
|
+
`wire-guard.test.ts` proves the guard directly against a fake `HttpClient`, AND that
|
|
97
|
+
`unifiHandlers` installs it (the family tests call `unifiOperations(spec)` and never exercise
|
|
98
|
+
`withCredentials`). The static check `write-op-reference.test.ts` is described in
|
|
99
|
+
[`unifi-network-write.md`](./unifi-network-write.md).
|
|
101
100
|
|
|
102
101
|
## Pagination helper
|
|
103
102
|
|
|
@@ -144,8 +143,7 @@ UniFi Network Integration API **10.4.57** (OpenAPI 3.1.0, 44 paths), via
|
|
|
144
143
|
(`docs/distilled-interim.md`). Traps in the underlying API that this read-only family cannot
|
|
145
144
|
trigger but a future write path must respect — whole-object `PUT`, ordering endpoints that
|
|
146
145
|
replace the whole list, `removeDevice` unadopting and factory-resetting hardware — are recorded
|
|
147
|
-
in the distilled package's own `README.md`
|
|
148
|
-
(outside this repo).
|
|
146
|
+
in the distilled package's own `README.md` (`packages/distilled-unifi-network`).
|
|
149
147
|
|
|
150
148
|
## Spec-version check against the live-measured console (2026-09-24)
|
|
151
149
|
|
|
@@ -192,8 +190,10 @@ attributed, declared or compared. Both declaration renderers and both `matches`
|
|
|
192
190
|
`value == null` / `deepEqual(..., { stripNullish: true })` consistently, per `network-form.ts`'s
|
|
193
191
|
own header. No departure found — nothing in this family needed changing.
|
|
194
192
|
|
|
195
|
-
⚠️ **Correction (2026-09-26):** the line above is no longer true as written.
|
|
196
|
-
has exactly
|
|
197
|
-
|
|
198
|
-
NEW defect for a failure mode outside every
|
|
199
|
-
|
|
193
|
+
⚠️ **Correction (2026-09-26, recounted 2026-10-10):** the line above is no longer true as written.
|
|
194
|
+
The family has exactly TWO `Effect.die` call sites, both in `wire-guard.ts`'s guard (see "Defense
|
|
195
|
+
in depth" above): the request-side refusal of a non-GET off the row's allow list, and the
|
|
196
|
+
response-side refusal of a 3xx answer. Each raises a NEW defect for a failure mode outside every
|
|
197
|
+
generated SDK operation's declared error union, per distilled-doctrine's "classify every
|
|
198
|
+
die/orDie" — neither converts an existing typed failure the way `orDie` would. Still no
|
|
199
|
+
`Effect.orDie` anywhere.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
package/src/unifi/fake-unifi.ts
CHANGED
|
@@ -17,7 +17,14 @@ import * as FetchHttpClient from 'effect/http/FetchHttpClient';
|
|
|
17
17
|
export const FAKE_BASE = 'https://unifi.example.com/proxy/network/integration';
|
|
18
18
|
export const FAKE_KEY = 'placeholder-unifi-key';
|
|
19
19
|
|
|
20
|
-
export type Seen = {
|
|
20
|
+
export type Seen = {
|
|
21
|
+
readonly method: string;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
/** Parsed JSON request body, when one was sent. */
|
|
24
|
+
readonly body?: unknown;
|
|
25
|
+
/** The `redirect` mode fetch was handed (`FetchHttpClient.RequestInit`), when set. */
|
|
26
|
+
readonly redirect?: NonNullable<RequestInit['redirect']>;
|
|
27
|
+
};
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* Plain-text failure. `protocol.ts`'s `errorEnvelope` also decodes a documented JSON shape
|
|
@@ -42,7 +49,13 @@ export const fakeUnifi = (route: (method: string, url: URL) => Response) => {
|
|
|
42
49
|
const request =
|
|
43
50
|
input instanceof Request ? new Request(input, init) : new Request(String(input), init);
|
|
44
51
|
const url = new URL(request.url);
|
|
45
|
-
|
|
52
|
+
const text = request.method === 'GET' ? '' : await request.clone().text();
|
|
53
|
+
seen.push({
|
|
54
|
+
method: request.method,
|
|
55
|
+
path: `${url.pathname}${url.search}`,
|
|
56
|
+
...(text === '' ? {} : { body: JSON.parse(text) as unknown }),
|
|
57
|
+
...(init?.redirect === undefined ? {} : { redirect: init.redirect }),
|
|
58
|
+
});
|
|
46
59
|
return route(request.method, url);
|
|
47
60
|
}) as typeof globalThis.fetch;
|
|
48
61
|
return { fetch, seen };
|