@homeflare/alchemy 0.55.0 → 0.57.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/launchd/index.js +4 -2
- package/dist/launchd/index.js.map +3 -3
- package/dist/launchd/sudo-guard-mode.d.ts +10 -4
- package/dist/launchd/sudo-guard-mode.d.ts.map +1 -1
- package/dist/linux/fake-sudo-root.d.ts.map +1 -1
- package/dist/linux/fake-sudo.d.ts +9 -1
- package/dist/linux/fake-sudo.d.ts.map +1 -1
- package/dist/linux/index.js +15 -8
- package/dist/linux/index.js.map +6 -6
- package/dist/linux/sudo-guard.d.ts +10 -1
- package/dist/linux/sudo-guard.d.ts.map +1 -1
- package/dist/linux/sudo-runner.d.ts +3 -1
- package/dist/linux/sudo-runner.d.ts.map +1 -1
- package/dist/linux/sudo-write.d.ts +4 -1
- package/dist/linux/sudo-write.d.ts.map +1 -1
- package/dist/unifi/acl-rule-ordering.d.ts +1 -1
- package/dist/unifi/acl-rule.d.ts +1 -1
- package/dist/unifi/dns-policy.d.ts +1 -1
- package/dist/unifi/firewall-policy-create.d.ts +55 -0
- package/dist/unifi/firewall-policy-create.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-form.d.ts +9 -2
- package/dist/unifi/firewall-policy-form.d.ts.map +1 -1
- package/dist/unifi/firewall-policy-ordering.d.ts +1 -1
- package/dist/unifi/firewall-policy.d.ts +45 -12
- package/dist/unifi/firewall-policy.d.ts.map +1 -1
- package/dist/unifi/firewall-zone.d.ts +1 -1
- package/dist/unifi/index.d.ts +1 -1
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +112 -21
- package/dist/unifi/index.js.map +10 -9
- package/dist/unifi/network.d.ts +1 -1
- package/dist/unifi/policy.d.ts +22 -0
- package/dist/unifi/policy.d.ts.map +1 -1
- package/dist/unifi/resource.d.ts +26 -7
- package/dist/unifi/resource.d.ts.map +1 -1
- package/dist/unifi/traffic-matching-list.d.ts +1 -1
- package/dist/unifi/update-reconcile.d.ts +36 -2
- package/dist/unifi/update-reconcile.d.ts.map +1 -1
- package/dist/unifi/wifi-broadcast.d.ts +1 -1
- package/dist/unifi/wire-guard.d.ts +1 -1
- package/dist/unifi/wire-guard.d.ts.map +1 -1
- package/docs/distilled-interim.md +4 -2
- package/docs/linux-sudo-review-2026-09-23.md +27 -0
- package/docs/linux-sudo.md +28 -40
- package/package.json +1 -1
- package/src/launchd/sudo-guard-mode.ts +11 -3
- package/src/linux/fake-sudo-root.ts +15 -0
- package/src/linux/fake-sudo.ts +20 -1
- package/src/linux/sudo-guard.ts +22 -1
- package/src/linux/sudo-runner-privileged-read.test.ts +109 -1
- package/src/linux/sudo-runner.ts +5 -3
- package/src/linux/sudo-write.ts +5 -1
- package/src/talos/talosctl.test.ts +6 -4
- package/src/unifi/firewall-policy-create.ts +94 -0
- package/src/unifi/firewall-policy-form.ts +9 -2
- package/src/unifi/firewall-policy.test.ts +188 -1
- package/src/unifi/firewall-policy.ts +108 -12
- package/src/unifi/index.ts +1 -0
- package/src/unifi/policy.ts +45 -12
- package/src/unifi/resource.test.ts +103 -0
- package/src/unifi/resource.ts +73 -21
- package/src/unifi/update-reconcile.ts +72 -2
- package/src/unifi/wire-guard.test.ts +86 -1
- package/src/unifi/wire-guard.ts +8 -1
- package/src/unifi/write-op-reference.test.ts +21 -7
|
@@ -129,7 +129,14 @@ interface ProbeProps {
|
|
|
129
129
|
const probeSpec = (
|
|
130
130
|
method: string,
|
|
131
131
|
withUpdate = false,
|
|
132
|
-
): UnifiSpec<
|
|
132
|
+
): UnifiSpec<
|
|
133
|
+
ProbeProps,
|
|
134
|
+
ProbeProps,
|
|
135
|
+
ProbeProps,
|
|
136
|
+
HttpClientError.HttpClientError,
|
|
137
|
+
never,
|
|
138
|
+
never
|
|
139
|
+
> => ({
|
|
133
140
|
type: 'Test.Probe',
|
|
134
141
|
describe: (props) => `probe/${props.id}`,
|
|
135
142
|
fetchLive: (props) =>
|
|
@@ -153,6 +160,41 @@ const probeSpec = (
|
|
|
153
160
|
: {}),
|
|
154
161
|
});
|
|
155
162
|
|
|
163
|
+
/**
|
|
164
|
+
* A create-only probe: `fetchLive` always answers absent (so `resource.ts`'s create branch always
|
|
165
|
+
* attempts the create), and `create.write` sends one raw request of the given method -- the POST
|
|
166
|
+
* the wire guard is meant to let through only under the exact conditions `resource.ts`'s own
|
|
167
|
+
* header names (no prior state, live absent).
|
|
168
|
+
*/
|
|
169
|
+
const createProbeSpec = (
|
|
170
|
+
writeMethod: string,
|
|
171
|
+
): UnifiSpec<
|
|
172
|
+
ProbeProps,
|
|
173
|
+
ProbeProps,
|
|
174
|
+
ProbeProps,
|
|
175
|
+
HttpClientError.HttpClientError,
|
|
176
|
+
never,
|
|
177
|
+
HttpClientError.HttpClientError
|
|
178
|
+
> => ({
|
|
179
|
+
type: 'Test.CreateProbe',
|
|
180
|
+
describe: (props) => `probe/${props.id}`,
|
|
181
|
+
fetchLive: () => Effect.succeed(undefined),
|
|
182
|
+
attributes: (live) => live,
|
|
183
|
+
matches: () => false,
|
|
184
|
+
create: {
|
|
185
|
+
allowedWrite: () => ({ method: 'POST' as const, tail: '/probe' }),
|
|
186
|
+
checkScope: () => Effect.void,
|
|
187
|
+
write: (props: ProbeProps) =>
|
|
188
|
+
Effect.gen(function* () {
|
|
189
|
+
const client = yield* HttpClient.HttpClient;
|
|
190
|
+
yield* client.execute(HttpClientRequest.make(writeMethod as never)(`${FAKE_BASE}/probe`));
|
|
191
|
+
return props;
|
|
192
|
+
}),
|
|
193
|
+
identify: (props: ProbeProps) => props,
|
|
194
|
+
driftOf: () => [],
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
156
198
|
const configProvider = ConfigProvider.layer(
|
|
157
199
|
ConfigProvider.fromUnknown({
|
|
158
200
|
UNIFI_NETWORK_API_BASE_URL: FAKE_BASE,
|
|
@@ -216,3 +258,46 @@ describe('guards are installed in unifiHandlers (IMPORTANT-1, red team 2026-09-2
|
|
|
216
258
|
expect(fake.seen).toEqual([]);
|
|
217
259
|
});
|
|
218
260
|
});
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The hf: create lift's own wire-level proof (`resource.ts`'s `spec.create`-aware allow list,
|
|
264
|
+
* 2026-10-19): a POST reaches the transport ONLY when there is no prior state at all, and never
|
|
265
|
+
* otherwise -- even when the family declares `create` and the attempt is "first deploy shaped".
|
|
266
|
+
*/
|
|
267
|
+
describe('guards are installed in unifiHandlers for spec.create (hf: create lift)', () => {
|
|
268
|
+
test('reconcile lets the one allowed POST through when there is no prior state', async () => {
|
|
269
|
+
const fake = fakeUnifi(() => Response.json({ id: 'w1' }));
|
|
270
|
+
const exit = await runHandler(
|
|
271
|
+
unifiHandlers(createProbeSpec('POST')).reconcile(args),
|
|
272
|
+
fake.fetch,
|
|
273
|
+
);
|
|
274
|
+
// The read-back then fails (this probe's fetchLive always answers absent) -- irrelevant to
|
|
275
|
+
// this test, which only proves the POST itself reached the transport.
|
|
276
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
277
|
+
expect(fake.seen.map((s) => s.method)).toEqual(['POST']);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('reconcile refuses a PUT even when the family declares create, not update', async () => {
|
|
281
|
+
const fake = neverReached('PUT');
|
|
282
|
+
expectRefused(
|
|
283
|
+
await runHandler(unifiHandlers(createProbeSpec('PUT')).reconcile(args), fake.fetch),
|
|
284
|
+
'PUT',
|
|
285
|
+
);
|
|
286
|
+
expect(fake.seen).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('a POST is refused when prior state is present (olds/output set) -- not a fresh create', async () => {
|
|
290
|
+
// ⚠️ A TYPED refusal here, not a wire-guard die: with `olds`/`output` set, `ops.reconcile`
|
|
291
|
+
// never even builds the create allow entry (`resource.ts`'s own condition) and falls to
|
|
292
|
+
// `updateReconcile`, which refuses "create" typed (`UnifiWriteRefused`) because this probe
|
|
293
|
+
// declares no `update` -- the request never reaches the transport either way.
|
|
294
|
+
const fake = neverReached('POST');
|
|
295
|
+
const withState = { ...args, olds: { id: 'w1' }, output: { id: 'w1' } };
|
|
296
|
+
const exit = await runHandler(
|
|
297
|
+
unifiHandlers(createProbeSpec('POST')).reconcile(withState),
|
|
298
|
+
fake.fetch,
|
|
299
|
+
);
|
|
300
|
+
expect(Exit.isFailure(exit)).toBe(true);
|
|
301
|
+
expect(fake.seen).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
});
|
package/src/unifi/wire-guard.ts
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* cloud-connector base paths differ, so the base is passed in and compared exactly, `isAllowed`).
|
|
15
15
|
* A PUT to another network id, the list route, or another family dies here.
|
|
16
16
|
*
|
|
17
|
+
* ⛔ POST, ADDED 2026-10-19 FOR THE hf: CREATE LIFT (`policy.ts`'s `isHfPolicyName`). The allow
|
|
18
|
+
* entry `unifiHandlers` builds for a create attempt (`spec.create.allowedWrite()`) names the
|
|
19
|
+
* site's fixed `/v1/sites/<siteId>/firewall/policies` COLLECTION route, never a per-row path —
|
|
20
|
+
* there is no row id before the POST creates one. This list is still the ONLY gate on method and
|
|
21
|
+
* path; the hf: NAME check is `create-reconcile.ts`'s `checkScope`, run before the POST is even
|
|
22
|
+
* attempted, not something this guard can see (it has no view of the request body).
|
|
23
|
+
*
|
|
17
24
|
* ⚠️ `Effect.die`, NOT A TYPED FAILURE — every generated SDK operation declares its OWN closed
|
|
18
25
|
* error union from the pinned OpenAPI spec and generated files are never hand-edited, so a typed
|
|
19
26
|
* `Effect.fail` here would be a failure absent from every op's declared type. A defect is the
|
|
@@ -42,7 +49,7 @@ import { UNIFI_WRITE_POLICY } from './policy.ts';
|
|
|
42
49
|
* base URL (`/v1/sites/<site>/networks/<id>`): the guard compares the request's origin and path to
|
|
43
50
|
* `baseUrl + tail` and requires an empty query and fragment (`isAllowed`).
|
|
44
51
|
*/
|
|
45
|
-
export type AllowedWrite = { readonly method: 'PUT'; readonly tail: string };
|
|
52
|
+
export type AllowedWrite = { readonly method: 'PUT' | 'POST'; readonly tail: string };
|
|
46
53
|
|
|
47
54
|
export class UnifiRefusedRequest extends Error {
|
|
48
55
|
constructor(
|
|
@@ -56,14 +56,16 @@ const sdkWriteOpNames = (): ReadonlySet<string> => {
|
|
|
56
56
|
const REAL_WRITE_OP_NAMES = sdkWriteOpNames();
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* ⛔ THE
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
59
|
+
* ⛔ THE TWO FILES ALLOWED TO NAME ONE WRITE OP EACH (Tim 2026-10-10: `Unifi.Network` update; Tim
|
|
60
|
+
* 2026-10-19 ~16:20Z: `Unifi.FirewallPolicy` create, hf:-named rows only). Keyed by path relative
|
|
61
|
+
* to `src/unifi`. Widening this object is a policy change — `policy.ts` — and a test below pins
|
|
62
|
+
* it to exactly these two entries so it cannot grow unnoticed. Everything else the SDK exports
|
|
63
|
+
* (`createNetwork`, `deleteNetwork`, `patchFirewallPolicy`, …) stays an offense even inside an
|
|
64
|
+
* allowlisted file.
|
|
64
65
|
*/
|
|
65
66
|
const WRITE_OP_ALLOWLIST: Record<string, ReadonlyArray<string>> = {
|
|
66
67
|
'network-update.ts': ['updateNetwork'],
|
|
68
|
+
'firewall-policy-create.ts': ['createFirewallPolicy'],
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
/** Recursive by construction (IMPORTANT-2): a future `src/unifi/<subdir>/*.ts` is walked too. */
|
|
@@ -187,8 +189,11 @@ describe('src/unifi never references an SDK write operation (T12)', () => {
|
|
|
187
189
|
}
|
|
188
190
|
});
|
|
189
191
|
|
|
190
|
-
test('the allowlist is exactly
|
|
191
|
-
expect(WRITE_OP_ALLOWLIST).toEqual({
|
|
192
|
+
test('the allowlist is exactly these two files, nothing else', () => {
|
|
193
|
+
expect(WRITE_OP_ALLOWLIST).toEqual({
|
|
194
|
+
'network-update.ts': ['updateNetwork'],
|
|
195
|
+
'firewall-policy-create.ts': ['createFirewallPolicy'],
|
|
196
|
+
});
|
|
192
197
|
});
|
|
193
198
|
|
|
194
199
|
test('network-update.ts may name updateNetwork but still not create/delete/patch ops', () => {
|
|
@@ -200,6 +205,15 @@ describe('src/unifi never references an SDK write operation (T12)', () => {
|
|
|
200
205
|
}
|
|
201
206
|
});
|
|
202
207
|
|
|
208
|
+
test('firewall-policy-create.ts may name createFirewallPolicy but still not update/delete ops', () => {
|
|
209
|
+
const path = join(DIR, 'firewall-policy-create.ts');
|
|
210
|
+
const src = (op: string) => `import * as f from '${SDK_MARKER}/firewall';\nf.${op}({});\n`;
|
|
211
|
+
expect(offensesIn(path, src('createFirewallPolicy'))).toEqual([]);
|
|
212
|
+
for (const op of ['updateFirewallPolicy', 'deleteFirewallPolicy', 'patchFirewallPolicy']) {
|
|
213
|
+
expect(offensesIn(path, src(op))).toEqual([`${path}: references SDK write op '${op}'`]);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
203
217
|
test('any other file naming updateNetwork is an offense', () => {
|
|
204
218
|
const path = join(DIR, 'network.ts');
|
|
205
219
|
const src = `import * as n from '${SDK_MARKER}/networks';\nn.updateNetwork({});\n`;
|