@homeflare/alchemy 0.56.0 → 0.58.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 (49) hide show
  1. package/dist/unifi/acl-rule-ordering.d.ts +1 -1
  2. package/dist/unifi/acl-rule.d.ts +1 -1
  3. package/dist/unifi/dns-policy.d.ts +1 -1
  4. package/dist/unifi/firewall-policy-create.d.ts +55 -0
  5. package/dist/unifi/firewall-policy-create.d.ts.map +1 -0
  6. package/dist/unifi/firewall-policy-drift.d.ts +3 -3
  7. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -1
  8. package/dist/unifi/firewall-policy-form.d.ts +27 -17
  9. package/dist/unifi/firewall-policy-form.d.ts.map +1 -1
  10. package/dist/unifi/firewall-policy-ordering.d.ts +1 -1
  11. package/dist/unifi/firewall-policy.d.ts +45 -12
  12. package/dist/unifi/firewall-policy.d.ts.map +1 -1
  13. package/dist/unifi/firewall-zone.d.ts +1 -1
  14. package/dist/unifi/index.d.ts +1 -1
  15. package/dist/unifi/index.d.ts.map +1 -1
  16. package/dist/unifi/index.js +175 -28
  17. package/dist/unifi/index.js.map +11 -10
  18. package/dist/unifi/network-update.fixtures.d.ts +53 -0
  19. package/dist/unifi/network-update.fixtures.d.ts.map +1 -0
  20. package/dist/unifi/network.d.ts +1 -1
  21. package/dist/unifi/policy.d.ts +22 -0
  22. package/dist/unifi/policy.d.ts.map +1 -1
  23. package/dist/unifi/resource.d.ts +26 -7
  24. package/dist/unifi/resource.d.ts.map +1 -1
  25. package/dist/unifi/traffic-matching-list.d.ts +1 -1
  26. package/dist/unifi/update-reconcile.d.ts +36 -2
  27. package/dist/unifi/update-reconcile.d.ts.map +1 -1
  28. package/dist/unifi/wifi-broadcast.d.ts +1 -1
  29. package/dist/unifi/wire-guard.d.ts +1 -1
  30. package/dist/unifi/wire-guard.d.ts.map +1 -1
  31. package/docs/distilled-interim.md +4 -2
  32. package/package.json +1 -1
  33. package/src/unifi/firewall-policy-create.ts +94 -0
  34. package/src/unifi/firewall-policy-drift.test.ts +64 -0
  35. package/src/unifi/firewall-policy-drift.ts +24 -7
  36. package/src/unifi/firewall-policy-form.ts +70 -17
  37. package/src/unifi/firewall-policy.test.ts +188 -1
  38. package/src/unifi/firewall-policy.ts +108 -12
  39. package/src/unifi/index.ts +1 -0
  40. package/src/unifi/network-update-accept.test.ts +59 -0
  41. package/src/unifi/network-update.fixtures.ts +74 -0
  42. package/src/unifi/network-update.test.ts +11 -69
  43. package/src/unifi/policy.ts +45 -12
  44. package/src/unifi/resource.test.ts +103 -0
  45. package/src/unifi/resource.ts +73 -21
  46. package/src/unifi/update-reconcile.ts +109 -5
  47. package/src/unifi/wire-guard.test.ts +86 -1
  48. package/src/unifi/wire-guard.ts +8 -1
  49. package/src/unifi/write-op-reference.test.ts +21 -7
@@ -0,0 +1,53 @@
1
+ import { type NetworkProps } from './network.ts';
2
+ export declare const PATH = "/proxy/network/integration/v1/sites/site-1/networks/net-1";
3
+ export declare const IPV6: {
4
+ interfaceType: string;
5
+ prefixLength: number;
6
+ hostIpAddress: string;
7
+ clientAddressAssignment: {
8
+ slaacEnabled: boolean;
9
+ };
10
+ routerAdvertisement: {
11
+ priority: string;
12
+ };
13
+ };
14
+ export declare const BASE: NetworkProps;
15
+ export declare const WITH_V6: NetworkProps;
16
+ /** Raw live object, with a key the SDK schema has never heard of. */
17
+ export declare const live: (extra?: Record<string, unknown>) => {
18
+ name: string;
19
+ enabled: boolean;
20
+ management: string;
21
+ vlanId: number;
22
+ dhcpGuarding?: import("@distilled.cloud/unifi-network/networks").NetworkDHCPGuarding | undefined;
23
+ cellularBackupEnabled?: boolean | undefined;
24
+ internetAccessEnabled?: boolean | undefined;
25
+ ipv4Configuration?: unknown;
26
+ ipv6Configuration?: import("@distilled.cloud/unifi-network/networks").NetworkIPv6Configuration | undefined;
27
+ isolationEnabled?: boolean | undefined;
28
+ mdnsForwardingEnabled?: boolean | undefined;
29
+ zoneId?: string | undefined;
30
+ deviceId?: string | undefined;
31
+ id: string;
32
+ default: boolean;
33
+ metadata: {
34
+ origin: string;
35
+ };
36
+ futureTopLevelKey: string;
37
+ siteId: undefined;
38
+ networkId: undefined;
39
+ };
40
+ export declare const reconcile: (route: (method: string, url: URL) => Response, news: NetworkProps, olds: NetworkProps | undefined, outputArg?: object | null) => {
41
+ fake: {
42
+ fetch: typeof fetch;
43
+ seen: import("./fake-unifi.ts").Seen[];
44
+ };
45
+ notes: string[];
46
+ exit: Promise<import("effect/Exit").Exit<import("./network-form.ts").NetworkAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | import("@distilled.cloud/caddy").Conflict | import("@distilled.cloud/caddy").Forbidden | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/caddy").NotFound | import("@distilled.cloud/caddy").ServiceUnavailable | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("./policy.ts").UnifiCreateDidNotConverge | import("./policy.ts").UnifiFieldNotRemovable | import("./policy.ts").UnifiFieldNotSettable | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiImmutableFieldChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiManagementNetworkRefused | import("@distilled.cloud/unifi-network/Errors").UnifiNetworkParseError | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiUpdateWouldBeNoop | import("./policy.ts").UnifiWriteRefused | import("@distilled.cloud/unifi-network/Errors").UnknownUnifiNetworkError | import("@distilled.cloud/caddy").UnprocessableEntity>>;
47
+ };
48
+ export declare const answering: (liveBody: object, put?: () => Response) => (method: string, url: URL) => Response;
49
+ export declare const putBody: (seen: ReadonlyArray<{
50
+ method: string;
51
+ body?: unknown;
52
+ }>) => Record<string, unknown>[];
53
+ //# sourceMappingURL=network-update.fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-update.fixtures.d.ts","sourceRoot":"","sources":["../../src/unifi/network-update.fixtures.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,YAAY,EAAQ,MAAM,cAAc,CAAC;AAGvD,eAAO,MAAM,IAAI,8DAA8D,CAAC;AAChF,eAAO,MAAM,IAAI;IACf,aAAa;IACb,YAAY;IACZ,aAAa;IACb,uBAAuB;QAAI,YAAY;;IACvC,mBAAmB;QAAI,QAAQ;;CAChC,CAAC;AAEF,eAAO,MAAM,IAAI,EAAE,YASlB,CAAC;AACF,eAAO,MAAM,OAAO,EAA2C,YAAY,CAAC;AAE5E,qEAAqE;AACrE,eAAO,MAAM,IAAI,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;QAGrC,MAAM;;;;;CAMlB,CAAC;AAEH,eAAO,MAAM,SAAS,UACb,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ,QACvC,YAAY,QACZ,YAAY,GAAG,SAAS,cACnB,MAAM,GAAG,IAAI;;;;;;;CAazB,CAAC;AAEF,eAAO,MAAM,SAAS,aACT,MAAM,QAAO,MAAM,QAAQ,cAC7B,MAAM,OAAO,GAAG,aAKW,CAAC;AAEvC,eAAO,MAAM,OAAO,SAAU,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,8BACO,CAAC"}
@@ -80,7 +80,7 @@ export declare const handlers: {
80
80
  session: {
81
81
  note: (message: string) => Effect.Effect<void, never, never>;
82
82
  };
83
- }) => Effect.Effect<NetworkAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | import("@distilled.cloud/caddy").Conflict | import("@distilled.cloud/caddy").Forbidden | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/caddy").NotFound | import("@distilled.cloud/caddy").ServiceUnavailable | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("./policy.ts").UnifiFieldNotRemovable | import("./policy.ts").UnifiFieldNotSettable | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiImmutableFieldChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiManagementNetworkRefused | import("@distilled.cloud/unifi-network/Errors").UnifiNetworkParseError | import("./policy.ts").UnifiUpdateDidNotConverge | UnifiUpdateWouldBeNoop | import("./policy.ts").UnifiWriteRefused | import("@distilled.cloud/unifi-network/Errors").UnknownUnifiNetworkError | import("@distilled.cloud/caddy").UnprocessableEntity, import("effect/http/HttpClient").HttpClient>;
83
+ }) => Effect.Effect<NetworkAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | import("@distilled.cloud/caddy").Conflict | import("@distilled.cloud/caddy").Forbidden | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/caddy").NotFound | import("@distilled.cloud/caddy").ServiceUnavailable | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("./policy.ts").UnifiCreateDidNotConverge | import("./policy.ts").UnifiFieldNotRemovable | import("./policy.ts").UnifiFieldNotSettable | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiImmutableFieldChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiManagementNetworkRefused | import("@distilled.cloud/unifi-network/Errors").UnifiNetworkParseError | import("./policy.ts").UnifiUpdateDidNotConverge | UnifiUpdateWouldBeNoop | import("./policy.ts").UnifiWriteRefused | import("@distilled.cloud/unifi-network/Errors").UnknownUnifiNetworkError | import("@distilled.cloud/caddy").UnprocessableEntity, import("effect/http/HttpClient").HttpClient>;
84
84
  delete: (args: {
85
85
  olds: NetworkProps;
86
86
  }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/http/HttpClient").HttpClient>;
@@ -1,5 +1,13 @@
1
1
  import * as Effect from 'effect/Effect';
2
2
  export declare const UNIFI_WRITE_POLICY: string;
3
+ /**
4
+ * ⛔ THE ONE GATE THAT DECIDES "hf:" (Tim 2026-10-19, `nas-firewall`'s own naming:
5
+ * `hf: CT100 to NAS https`). The space after the colon is deliberate and checked: `"hf:foo"`
6
+ * (no space) is NOT an hf: name under this policy, so a typo can never silently slip past the
7
+ * create gate by omitting it. Exported so `firewall-policy-create.ts`'s `checkScope` and every
8
+ * test that proves the boundary use this ONE function, never a second regex hand-copied nearby.
9
+ */
10
+ export declare const isHfPolicyName: (name: string) => boolean;
3
11
  /** @deprecated renamed `UNIFI_WRITE_POLICY`; kept so existing consumer imports do not break. */
4
12
  export declare const UNIFI_READ_ONLY_POLICY: string;
5
13
  /** What kind of write was asked for and refused — for the message only, never acted on. */
@@ -50,6 +58,20 @@ export declare class UnifiUpdateDidNotConverge extends UnifiUpdateDidNotConverge
50
58
  }> {
51
59
  get message(): string;
52
60
  }
61
+ declare const UnifiCreateDidNotConverge_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 & {
62
+ readonly _tag: "UnifiCreateDidNotConverge";
63
+ } & Readonly<A>;
64
+ /** The POST answered, a read-back ran, yet the read-back object still differs from the
65
+ * declaration — the hf: create path's own convergence check (`create-reconcile.ts`'s sibling in
66
+ * `update-reconcile.ts`), same shape as `UnifiUpdateDidNotConverge` for the same reason: the
67
+ * object now exists on the console, unrolled-back, so the operator needs to know and re-import. */
68
+ export declare class UnifiCreateDidNotConverge extends UnifiCreateDidNotConverge_base<{
69
+ readonly type: string;
70
+ readonly identity: string;
71
+ readonly fields: ReadonlyArray<string>;
72
+ }> {
73
+ get message(): string;
74
+ }
53
75
  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
76
  readonly _tag: "UnifiFieldNotRemovable";
55
77
  } & Readonly<A>;
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/unifi/policy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/unifi/policy.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ 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
4
  import type * as HttpClient from 'effect/http/HttpClient';
5
- import { type ReconcileArgs, type UnifiUpdate } from './update-reconcile.ts';
5
+ import { type ReconcileArgs, type UnifiCreate, type UnifiUpdate } from './update-reconcile.ts';
6
6
  /** What every handler needs from the caller's runtime once `CredentialsFromEnv` is baked in. */
7
7
  export type UnifiRequirements = HttpClient.HttpClient;
8
8
  /**
@@ -11,7 +11,7 @@ export type UnifiRequirements = HttpClient.HttpClient;
11
11
  * union, with `NotFound` already folded away) so the precise per-operation error union flows
12
12
  * through instead of being widened by hand here. `E2` is the error union of the optional write.
13
13
  */
14
- export type UnifiSpec<Props extends object, Live, Attributes extends object, E, E2 = never> = {
14
+ export type UnifiSpec<Props extends object, Live, Attributes extends object, E, E2 = never, E3 = never> = {
15
15
  /** The vendor type string, e.g. `Unifi.Network` — for the refusal message only. */
16
16
  readonly type: string;
17
17
  /** Already folds a genuine not-found to `undefined`; nothing else is folded (see header). */
@@ -22,8 +22,11 @@ export type UnifiSpec<Props extends object, Live, Attributes extends object, E,
22
22
  readonly describe: (props: Props) => string;
23
23
  /** Present only on a family allowed to write (`Unifi.Network`); see `update-reconcile.ts`. */
24
24
  readonly update?: UnifiUpdate<Props, Live, E2>;
25
+ /** Present only on a family allowed to create (`Unifi.FirewallPolicy`, `hf:` rows only); see
26
+ * `update-reconcile.ts`'s `createReconcile`. */
27
+ readonly create?: UnifiCreate<Props, Live, E3>;
25
28
  };
26
- export declare const unifiOperations: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>) => {
29
+ export declare const unifiOperations: <Props extends object, Live, Attributes extends object, E, E2, E3>(spec: UnifiSpec<Props, Live, Attributes, E, E2, E3>) => {
27
30
  read: (props: Props) => Effect.Effect<Attributes | undefined, E, UnifiNetworkOpContext>;
28
31
  readHandler: ({ olds, output }: {
29
32
  olds: Props;
@@ -34,8 +37,24 @@ export declare const unifiOperations: <Props extends object, Live, Attributes ex
34
37
  } | {
35
38
  readonly action: 'noop';
36
39
  } | undefined, E, UnifiNetworkOpContext>;
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>;
40
+ /**
41
+ * Pre-state callers (tests, adopt-only probes) pass just `news`: `olds`/`output` unset — the
42
+ * SAME calling convention a plain cold adoption already uses (H6), so the existence GET must
43
+ * run FIRST, for every name, exactly as it always has: a row that already exists is adopted
44
+ * (exact match, zero writes) regardless of its name, and ONLY an absent row ever reaches
45
+ * `create.checkScope`. "A non-hf: create sends nothing" therefore means no WRITE (no
46
+ * POST/PUT/DELETE) — the same guarantee every other refusal in this engine gives, and the one
47
+ * `wire-guard.ts`'s allow list enforces at the wire; the existence GET is not a write.
48
+ *
49
+ * ⛔ THE CREATE BRANCH ONLY RUNS WHEN THERE IS NO PRIOR STATE AT ALL (`olds`/`output` both
50
+ * absent — a genuine first deploy, never a re-declare of an adopted row) AND the family
51
+ * declares `create` AND the live read comes back absent. A row that already exists (someone
52
+ * else's object with a colliding identity, or a retried deploy that already created it)
53
+ * takes the ordinary `updateReconcile` path instead — exact match reports with zero writes,
54
+ * anything else refuses (there is no `olds` to PUT a patch against, so it can never silently
55
+ * fall through to an update of an existing row).
56
+ */
57
+ reconcile: (news: Props, rest?: Partial<Omit<ReconcileArgs<Props>, 'news'>>) => Effect.Effect<Attributes, E | E2 | E3 | import("./policy.ts").UnifiCreateDidNotConverge | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
39
58
  destroy: (olds: Props) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, never>;
40
59
  };
41
60
  /**
@@ -44,7 +63,7 @@ export declare const unifiOperations: <Props extends object, Live, Attributes ex
44
63
  * ⛔ `list` ANSWERS EMPTY. `GET /v1/sites/{siteId}/networks` answers every network on the site;
45
64
  * adoption stays explicit, the same reasoning `Proxmox.User`'s and NetBox's `list` give.
46
65
  */
47
- export declare const unifiHandlers: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>) => {
66
+ export declare const unifiHandlers: <Props extends object, Live, Attributes extends object, E, E2, E3>(spec: UnifiSpec<Props, Live, Attributes, E, E2, E3>) => {
48
67
  list: () => Effect.Effect<never[], never, never>;
49
68
  read: (args: {
50
69
  olds: Props;
@@ -65,7 +84,7 @@ export declare const unifiHandlers: <Props extends object, Live, Attributes exte
65
84
  session: {
66
85
  note: (message: string) => Effect.Effect<void, never, never>;
67
86
  };
68
- }) => Effect.Effect<Attributes, E | E2 | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
87
+ }) => Effect.Effect<Attributes, E | E2 | E3 | import("./policy.ts").UnifiCreateDidNotConverge | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
69
88
  delete: (args: {
70
89
  olds: Props;
71
90
  }) => 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":"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"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,EAGjB,MAAM,uBAAuB,CAAC;AAG/B,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CACnB,KAAK,SAAS,MAAM,EACpB,IAAI,EACJ,UAAU,SAAS,MAAM,EACzB,CAAC,EACD,EAAE,GAAG,KAAK,EACV,EAAE,GAAG,KAAK,IACR;IACF,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAChG,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC5C,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IAC/C;qDACiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;CAChD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,QACxF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC;kBAE9B,KAAK;oCAQQ;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;iBAUlE,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBAIZ,QAAQ;;yBAElC,MAAM;;IAIzB;;;;;;;;;;;;;;;;OAgBG;sBACe,KAAK,SAAQ,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;oBAc1D,KAAK;CAExB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,QACtF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC;IAejD,IAAI;IACJ,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAE5D,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBAlEtB,QAAQ;;yBAElC,MAAM;;IAmEzB,SAAS,SAAS;QAChB,IAAI,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC;QACxB,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;QAC/B,OAAO,EAAE;YAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;SAAE,CAAC;KAC3E;IAqBD,MAAM,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAEjC,CAAC"}
@@ -47,7 +47,7 @@ export declare const handlers: {
47
47
  session: {
48
48
  note: (message: string) => Effect.Effect<void, never, never>;
49
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>;
50
+ }) => Effect.Effect<TrafficMatchingListAttributes, import("./policy.ts").UnifiCreateDidNotConverge | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused | trafficMatchingLists.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
51
51
  delete: (args: {
52
52
  olds: TrafficMatchingListProps;
53
53
  }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/http/HttpClient").HttpClient>;
@@ -1,7 +1,7 @@
1
1
  import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
2
2
  import * as Effect from 'effect/Effect';
3
3
  import type { FieldDrift } from './drift.ts';
4
- import { UnifiIdentityChanged, UnifiLiveDriftedSinceDeploy, UnifiUpdateDidNotConverge } from './policy.ts';
4
+ import { UnifiCreateDidNotConverge, UnifiIdentityChanged, UnifiLiveDriftedSinceDeploy, UnifiUpdateDidNotConverge } from './policy.ts';
5
5
  import type { UnifiSpec } from './resource.ts';
6
6
  import type { AllowedWrite } from './wire-guard.ts';
7
7
  export type Patch<Props> = {
@@ -27,5 +27,39 @@ export type ReconcileArgs<Props extends object> = {
27
27
  readonly output: unknown;
28
28
  readonly note: (message: string) => Effect.Effect<void>;
29
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>;
30
+ export declare const updateReconcile: <Props extends object, Live, Attributes extends object, E, E2, E3>(spec: UnifiSpec<Props, Live, Attributes, E, E2, E3>, { news, olds, output, note }: ReconcileArgs<Props>) => Effect.Effect<Attributes, E | E2 | UnifiIdentityChanged | UnifiLiveDriftedSinceDeploy | UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
31
+ /**
32
+ * The write half of a CREATE-capable family: today, only `Unifi.FirewallPolicy`, and only for an
33
+ * `hf:`-named row (`policy.ts`'s `isHfPolicyName`). Deliberately NOT a merge-with-live update —
34
+ * there is no live object yet — so there is no `patchKeys`/`checkScope(live, patch, …)` here: a
35
+ * create sends exactly the fields the declaration owns, nothing echoed from elsewhere, nothing to
36
+ * accidentally reset.
37
+ *
38
+ * ⛔ `checkScope` RUNS BEFORE THE POST (same ordering `update.checkScope` already has): a
39
+ * non-`hf:` name is refused here, typed, never reaching the wire at all.
40
+ * ★ `identify` CLOSES THE LOOP FROM THE POST'S RESPONSE TO A FRESH GET. The vendor assigns the id
41
+ * on create; `write`'s response carries it, but a response body is not proof the object is
42
+ * actually readable back (`createReconcile` re-fetches with the id `identify` folds in, rather
43
+ * than trusting the POST's own echo — same reasoning `network-update.ts`'s header gives for
44
+ * never trusting a write's own response over a real read).
45
+ */
46
+ export type UnifiCreate<Props extends object, Live, E3> = {
47
+ /** The one POST the wire guard may pass for this family (the site's fixed collection route,
48
+ * built from `props` the same way `UnifiUpdate.allowedWrite` reads a row's ids from `props`). */
49
+ readonly allowedWrite: (props: Props) => AllowedWrite;
50
+ /** Refuses (typed, before any note or request) a create outside this family's write scope. */
51
+ readonly checkScope: (props: Props) => Effect.Effect<void, E3>;
52
+ readonly write: (props: Props) => Effect.Effect<Live, E3, UnifiNetworkOpContext>;
53
+ /** `props` plus the vendor id the POST's response assigned, for the read-back `fetchLive`. */
54
+ readonly identify: (props: Props, written: Live) => Props;
55
+ readonly driftOf: (live: Live, props: Props) => ReadonlyArray<FieldDrift>;
56
+ };
57
+ /**
58
+ * CREATE for a family whose live read came back absent AND which has no prior Alchemy state
59
+ * (`resource.ts`'s `unifiOperations.reconcile` is the only caller, and only under that same
60
+ * condition — see its own header). Refuses (untyped-to-typed, same `refuseWrite` every other
61
+ * refusal uses) when the family declares no `create` at all; `create.checkScope` then refuses
62
+ * again, more specifically, for a declared row the create scope itself does not allow.
63
+ */
64
+ export declare const createReconcile: <Props extends object, Live, Attributes extends object, E, E2, E3>(spec: UnifiSpec<Props, Live, Attributes, E, E2, E3>, news: Props, note: (message: string) => Effect.Effect<void, never, never>) => Effect.Effect<Attributes, E | E3 | UnifiCreateDidNotConverge | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
31
65
  //# sourceMappingURL=update-reconcile.d.ts.map
@@ -1 +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"}
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,yBAAyB,EACzB,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;AAiBF,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,EAAE,EAAE,QACxF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,gCACrB,aAAa,CAAC,KAAK,CAAC,wLA8DhD,CAAC;AAEL;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI;IACxD;sGACkG;IAClG,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,YAAY,CAAC;IACtD,8FAA8F;IAC9F,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC/D,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,qBAAqB,CAAC,CAAC;IACjF,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,KAAK,KAAK,CAAC;IAC1D,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC,UAAU,CAAC,CAAC;CAC3E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,QACxF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,QAC7C,KAAK,QACL,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,mIA8B1D,CAAC"}
@@ -69,7 +69,7 @@ export declare const handlers: {
69
69
  session: {
70
70
  note: (message: string) => Effect.Effect<void, never, never>;
71
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>;
72
+ }) => Effect.Effect<WifiBroadcastAttributes, import("./policy.ts").UnifiCreateDidNotConverge | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | UnifiPaginationInconsistent | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused | wifiBroadcasts.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
73
73
  delete: (args: {
74
74
  olds: WifiBroadcastProps;
75
75
  }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/http/HttpClient").HttpClient>;
@@ -5,7 +5,7 @@ import * as HttpClient from 'effect/http/HttpClient';
5
5
  * `baseUrl + tail` and requires an empty query and fragment (`isAllowed`).
6
6
  */
7
7
  export type AllowedWrite = {
8
- readonly method: 'PUT';
8
+ readonly method: 'PUT' | 'POST';
9
9
  readonly tail: string;
10
10
  };
11
11
  export declare class UnifiRefusedRequest extends Error {
@@ -1 +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"}
1
+ {"version":3,"file":"wire-guard.d.ts","sourceRoot":"","sources":["../../src/unifi/wire-guard.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAKrD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtF,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"}
@@ -122,8 +122,10 @@ Two checks, both real, neither skippable:
122
122
  No special-casing: add a changeset (`bun changeset`, minor — new package),
123
123
  push, merge. `scripts/publish.ts` packs with `bun pm pack` (resolves
124
124
  `workspace:`/`catalog:` the way `npm pack` cannot) and publishes with `npm
125
- publish --provenance` from CI. `README.md`/`LICENSE`/`scripts/smoke.ts` ship
126
- in `files`; nothing about the release pipeline needs to know this package is
125
+ publish --provenance` from CI. `README.md`/`LICENSE` ship in `files`;
126
+ `scripts/smoke.ts` is outside `files` so it never packs — `scripts/pack.ts`
127
+ additionally strips the `scripts`/`devDependencies` manifest fields at pack
128
+ time. Nothing about the release pipeline needs to know this package is
127
129
  an interim stand-in.
128
130
 
129
131
  ### 5. Alias it in, then cut over later
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.56.0",
3
+ "version": "0.58.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",
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `Unifi.FirewallPolicy`'s CREATE — the one write this family may make, and only for a row whose
3
+ * declared `name` passes `policy.ts`'s `isHfPolicyName` (Tim 2026-10-19 ~16:20Z, "Allow kit to
4
+ * create hf: rules"). THE ONLY FILE UNDER `src/unifi` ALLOWED TO NAME `createFirewallPolicy`
5
+ * (`write-op-reference.test.ts`'s `WRITE_OP_ALLOWLIST` pins that, alongside `network-update.ts`'s
6
+ * own `updateNetwork` entry).
7
+ *
8
+ * ⛔ A CREATE SENDS ONLY THE FIELDS THE DECLARATION OWNS — NEVER A SPREAD OF ANYTHING ELSE. Unlike
9
+ * `network-update.ts`'s whole-object PUT (which must carry the untouched live object forward so
10
+ * an omitted optional block is not read by the controller as "turn it off"), there IS no live
11
+ * object yet: `createBody` builds `CreateFirewallPolicyRequest` field by field from `props`,
12
+ * omitting every optional key `props` itself leaves `undefined` rather than sending it as
13
+ * `null`/`undefined` (UniFi's "missing = off" rule cuts both ways — a key that is actually absent
14
+ * from the JSON body is what "this policy does not restrict on this axis" means here, same as a
15
+ * declaration that never sets `connectionStateFilter` means "match every connection state").
16
+ * ⛔ `checkScope` RUNS BEFORE THE REQUEST, TYPED, NEVER A STRING COMPARISON INLINE AT THE CALL
17
+ * SITE — one function (`policy.ts`'s `isHfPolicyName`) decides "hf:" everywhere this boundary is
18
+ * checked, so a future second caller cannot drift from this one with a slightly different regex.
19
+ *
20
+ * 🔴 `checkScope` ALSO REFUSES A `firewallPolicyId` THAT IS ALREADY SET (Opus red team,
21
+ * 2026-10-19, PRIMARY FINDING). `resource.ts`'s `create` branch runs for EVERY
22
+ * `Unifi.FirewallPolicy` declaration with no prior Alchemy state, not only ones meant to
23
+ * create — including every `firewallPolicy()` import `.pipe(adopt(true))`, and Tim's own live
24
+ * console ALREADY names hand-made rules `hf: <desc>` (this file's sibling `policy.ts` cites
25
+ * `hf: Vpn to MGMT (Magic WAN site-to-site)` as the existing convention). Before this check, an
26
+ * hf:-named IMPORTED row whose `firewallPolicyId` stopped resolving (state lost, the rule
27
+ * hand-deleted and recreated with a new id, a wrong id in a generated declaration) would GET
28
+ * 404, fall into `createReconcile`, pass the name check, and POST A DUPLICATE — silently, with
29
+ * a green plan, where every other family still refuses "declared row absent" outright
30
+ * (`update-reconcile.ts`'s original behavior, unchanged for every family but this one). A
31
+ * declaration carrying a vendor id is BY DEFINITION an existing row, adopted or updated, never
32
+ * a fresh create; `firewallPolicyId !== undefined` is refused here before `isHfPolicyName` is
33
+ * even checked, so this holds regardless of what the name says.
34
+ */
35
+ import * as firewall from '@distilled.cloud/unifi-network/firewall';
36
+ import * as Effect from 'effect/Effect';
37
+ import { isHfPolicyName, refuseWrite } from './policy.ts';
38
+ import type { FirewallPolicyProps } from './firewall-policy-form.ts';
39
+ import type { AllowedWrite } from './wire-guard.ts';
40
+
41
+ /** The site's fixed firewall-policies COLLECTION route — never a per-row path, since there is no
42
+ * row id before this POST creates one. Takes `props` (not just a bare `siteId`) to match
43
+ * `UnifiUpdate.allowedWrite`'s own shape, so `resource.ts`'s wiring treats both the same way. */
44
+ export const firewallPolicyCreateAllowedWrite = (props: FirewallPolicyProps): AllowedWrite => ({
45
+ method: 'POST',
46
+ tail: `/v1/sites/${props.siteId}/firewall/policies`,
47
+ });
48
+
49
+ const describeByName = (props: FirewallPolicyProps) =>
50
+ `sites/${props.siteId}/firewall/policies/by-name/${props.name}`;
51
+
52
+ /** Refuses, typed, before any request: the one gate `createReconcile` calls before `write`.
53
+ * Order matters for the message only (both refuse the same way) — the identity check runs
54
+ * first because it is the one that must hold regardless of what the name says (see header). */
55
+ export const checkHfCreateScope = (props: FirewallPolicyProps) => {
56
+ if (props.firewallPolicyId !== undefined) {
57
+ return refuseWrite(
58
+ 'Unifi.FirewallPolicy',
59
+ `sites/${props.siteId}/firewall/policies/${props.firewallPolicyId}`,
60
+ 'create',
61
+ );
62
+ }
63
+ return isHfPolicyName(props.name)
64
+ ? Effect.void
65
+ : refuseWrite('Unifi.FirewallPolicy', describeByName(props), 'create');
66
+ };
67
+
68
+ /** Every field `CreateFirewallPolicyRequest` accepts, straight from the declaration, with an
69
+ * absent optional field left OUT of the body entirely (never sent as `undefined`/`null`) —
70
+ * `JSON.stringify` already drops an `undefined` value, but building the object field-by-field
71
+ * rather than spreading `props` keeps this list the single place that enumerates exactly what a
72
+ * create may send, so a future prop added to `FirewallPolicyProps` for some OTHER purpose cannot
73
+ * silently become a wire field here without this file changing too. */
74
+ export const firewallPolicyCreateBody = (
75
+ props: FirewallPolicyProps,
76
+ ): firewall.CreateFirewallPolicyRequest => ({
77
+ siteId: props.siteId,
78
+ action: props.action,
79
+ ...(props.connectionStateFilter === undefined
80
+ ? {}
81
+ : { connectionStateFilter: props.connectionStateFilter }),
82
+ ...(props.description === undefined ? {} : { description: props.description }),
83
+ destination: props.destination,
84
+ enabled: props.enabled,
85
+ ipProtocolScope: props.ipProtocolScope,
86
+ ...(props.ipsecFilter === undefined ? {} : { ipsecFilter: props.ipsecFilter }),
87
+ loggingEnabled: props.loggingEnabled,
88
+ name: props.name,
89
+ ...(props.schedule === undefined ? {} : { schedule: props.schedule }),
90
+ source: props.source,
91
+ });
92
+
93
+ export const writeFirewallPolicyCreate = (props: FirewallPolicyProps) =>
94
+ firewall.createFirewallPolicy(firewallPolicyCreateBody(props));
@@ -149,3 +149,67 @@ describe('I2 -- every declarable field is wired into the drift field list', () =
149
149
  },
150
150
  );
151
151
  });
152
+
153
+ describe('set-like nested filter arrays compare as sets (homeflare-network PR 79)', () => {
154
+ const dest = (ports: number[], ips: string[]) =>
155
+ ({
156
+ zoneId: 'zone-storage',
157
+ trafficFilter: {
158
+ type: 'IP_ADDRESS',
159
+ ipAddressFilter: {
160
+ type: 'IP_ADDRESSES',
161
+ matchOpposite: false,
162
+ items: ips.map((value) => ({ type: 'IP_ADDRESS', value })),
163
+ },
164
+ portFilter: {
165
+ type: 'PORTS',
166
+ matchOpposite: false,
167
+ items: ports.map((value) => ({ type: 'PORT_NUMBER', value })),
168
+ },
169
+ },
170
+ }) as unknown as FirewallPolicyProps['destination'];
171
+
172
+ test('the measured case: declared [443, 9100], live [9100, 443] is a noop', () => {
173
+ const props = { ...PROPS, destination: dest([443, 9100], ['10.20.14.10']) };
174
+ const live = liveFirewallPolicy({ destination: dest([9100, 443], ['10.20.14.10']) as never });
175
+ expect(matches(attributesOf(live, props), props)).toBe(true);
176
+ expect(driftOf(live, props)).toEqual([]);
177
+ });
178
+
179
+ test('IP items in another order are a noop too, on source as well as destination', () => {
180
+ const props = { ...PROPS, source: dest([], ['10.0.0.1', '10.0.0.2']) as never };
181
+ const live = liveFirewallPolicy({ source: dest([], ['10.0.0.2', '10.0.0.1']) as never });
182
+ expect(driftOf(live, props)).toEqual([]);
183
+ });
184
+
185
+ test('a membership change still drifts: a port swapped, added or dropped', () => {
186
+ const props = { ...PROPS, destination: dest([443, 9100], ['10.20.14.10']) };
187
+ for (const ports of [[443, 22], [443, 9100, 22], [443]]) {
188
+ const live = liveFirewallPolicy({ destination: dest(ports, ['10.20.14.10']) as never });
189
+ expect(
190
+ driftOf(live, props).map((d) => d.field),
191
+ String(ports),
192
+ ).toEqual(['destination']);
193
+ }
194
+ });
195
+
196
+ test('a duplicate is not collapsed away: [443, 443] differs from [443]', () => {
197
+ const props = { ...PROPS, destination: dest([443], ['10.20.14.10']) };
198
+ const live = liveFirewallPolicy({ destination: dest([443, 443], ['10.20.14.10']) as never });
199
+ expect(driftOf(live, props).map((d) => d.field)).toEqual(['destination']);
200
+ });
201
+
202
+ test('mixed port items sort the same whatever their key order (range vs number)', () => {
203
+ const range = { start: 8000, stop: 8100, type: 'PORT_NUMBER_RANGE' };
204
+ const rangeDecoded = { type: 'PORT_NUMBER_RANGE', start: 8000, stop: 8100 };
205
+ const one = { type: 'PORT_NUMBER', value: 443 };
206
+ const withItems = (items: unknown[]) =>
207
+ ({
208
+ zoneId: 'zone-storage',
209
+ trafficFilter: { type: 'PORT', portFilter: { type: 'PORTS', matchOpposite: false, items } },
210
+ }) as unknown as FirewallPolicyProps['destination'];
211
+ const props = { ...PROPS, destination: withItems([range, one]) };
212
+ const live = liveFirewallPolicy({ destination: withItems([one, rangeDecoded]) as never });
213
+ expect(driftOf(live, props)).toEqual([]);
214
+ });
215
+ });
@@ -10,9 +10,9 @@
10
10
  * before comparing — never applied to only `live` or only `declared` (the specific gap MEDIUM-4's
11
11
  * own mutant found in `network-drift.ts`; see its header, and `acl-rule-drift.ts`'s own repeat of
12
12
  * this same rule for `protocolFilter`/`enforcingDeviceFilter`).
13
- * ★ `action`/`destination`/`ipProtocolScope`/`schedule`/`source` compare with the plain default
14
- * `equal` (whole-value `deepEqual`) — see `firewall-policy-form.ts`'s header for the accepted
15
- * known gap this leaves in their own nested set-like arrays.
13
+ * ★ `destination`/`ipProtocolScope`/`schedule`/`source` run through `normalizeSetArrays` on BOTH
14
+ * sides (set-like nested arrays compare as sets; `firewall-policy-form.ts`'s header), then the
15
+ * default `deepEqual`. `action` compares wholesale.
16
16
  */
17
17
  import type * as firewall from '@distilled.cloud/unifi-network/firewall';
18
18
  import { makeDriftOf } from './drift.ts';
@@ -21,6 +21,7 @@ import {
21
21
  type FirewallPolicyProps,
22
22
  attributesOf,
23
23
  normalizeConnectionStateFilter,
24
+ normalizeSetArrays,
24
25
  } from './firewall-policy-form.ts';
25
26
 
26
27
  const fieldDrift = makeDriftOf<FirewallPolicyAttributes, FirewallPolicyProps>([
@@ -31,14 +32,30 @@ const fieldDrift = makeDriftOf<FirewallPolicyAttributes, FirewallPolicyProps>([
31
32
  declared: (p) => normalizeConnectionStateFilter(p.connectionStateFilter),
32
33
  },
33
34
  { field: 'description', live: (a) => a.description, declared: (p) => p.description },
34
- { field: 'destination', live: (a) => a.destination, declared: (p) => p.destination },
35
+ {
36
+ field: 'destination',
37
+ live: (a) => normalizeSetArrays(a.destination),
38
+ declared: (p) => normalizeSetArrays(p.destination),
39
+ },
35
40
  { field: 'enabled', live: (a) => a.enabled, declared: (p) => p.enabled },
36
- { field: 'ipProtocolScope', live: (a) => a.ipProtocolScope, declared: (p) => p.ipProtocolScope },
41
+ {
42
+ field: 'ipProtocolScope',
43
+ live: (a) => normalizeSetArrays(a.ipProtocolScope),
44
+ declared: (p) => normalizeSetArrays(p.ipProtocolScope),
45
+ },
37
46
  { field: 'ipsecFilter', live: (a) => a.ipsecFilter, declared: (p) => p.ipsecFilter },
38
47
  { field: 'loggingEnabled', live: (a) => a.loggingEnabled, declared: (p) => p.loggingEnabled },
39
48
  { field: 'name', live: (a) => a.name, declared: (p) => p.name },
40
- { field: 'schedule', live: (a) => a.schedule, declared: (p) => p.schedule },
41
- { field: 'source', live: (a) => a.source, declared: (p) => p.source },
49
+ {
50
+ field: 'schedule',
51
+ live: (a) => normalizeSetArrays(a.schedule),
52
+ declared: (p) => normalizeSetArrays(p.schedule),
53
+ },
54
+ {
55
+ field: 'source',
56
+ live: (a) => normalizeSetArrays(a.source),
57
+ declared: (p) => normalizeSetArrays(p.source),
58
+ },
42
59
  ]);
43
60
 
44
61
  export const matches = (