@homeflare/alchemy 0.27.3 → 0.28.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/cloudflare/mesh-node-token.d.ts +1 -1
- package/dist/cloudflare/mesh-node-token.d.ts.map +1 -1
- package/dist/forgejo/branch-protection.d.ts +4 -4
- package/dist/forgejo/org-actions-secrets.d.ts +4 -4
- package/dist/forgejo/org-label.d.ts +4 -4
- package/dist/forgejo/org-team.d.ts +4 -4
- package/dist/forgejo/repo-webhook.d.ts +4 -4
- package/dist/forgejo/repository.d.ts +4 -4
- package/dist/forgejo/team-member.d.ts +4 -4
- package/dist/github/index.d.ts +1 -1
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js +99 -3
- package/dist/github/index.js.map +8 -7
- package/dist/github/repository-ruleset-errors.d.ts +34 -1
- package/dist/github/repository-ruleset-errors.d.ts.map +1 -1
- package/dist/github/repository-ruleset-guards.d.ts +12 -0
- package/dist/github/repository-ruleset-guards.d.ts.map +1 -1
- package/dist/github/repository-ruleset-narrowing-guards.d.ts +39 -0
- package/dist/github/repository-ruleset-narrowing-guards.d.ts.map +1 -0
- package/dist/github/repository-ruleset-props.d.ts +88 -0
- package/dist/github/repository-ruleset-props.d.ts.map +1 -0
- package/dist/github/repository-ruleset-reconcile.d.ts.map +1 -1
- package/dist/github/repository-ruleset.d.ts +3 -31
- package/dist/github/repository-ruleset.d.ts.map +1 -1
- package/dist/netbox/fake-netbox.d.ts +43 -0
- package/dist/netbox/fake-netbox.d.ts.map +1 -0
- package/dist/netbox/index.d.ts +1 -2
- package/dist/netbox/index.d.ts.map +1 -1
- package/dist/netbox/index.js +71 -119
- package/dist/netbox/index.js.map +5 -6
- package/dist/netbox/prefix.d.ts +33 -2
- package/dist/netbox/prefix.d.ts.map +1 -1
- package/dist/netbox/resource.d.ts +73 -48
- package/dist/netbox/resource.d.ts.map +1 -1
- package/docs/credentials.md +10 -0
- package/docs/peers.md +11 -0
- package/docs/upstream-conformance.md +17 -2
- package/package.json +7 -2
- package/src/github/index.ts +2 -0
- package/src/github/repository-ruleset-errors.ts +45 -0
- package/src/github/repository-ruleset-exact-adopt.test.ts +179 -0
- package/src/github/repository-ruleset-guards.ts +8 -2
- package/src/github/repository-ruleset-narrowing-guards.ts +141 -0
- package/src/github/repository-ruleset-narrowing.test.ts +210 -0
- package/src/github/repository-ruleset-props.ts +92 -0
- package/src/github/repository-ruleset-reconcile.ts +35 -0
- package/src/github/repository-ruleset-write-refusals.test.ts +6 -26
- package/src/github/repository-ruleset.ts +27 -41
- package/src/netbox/fake-netbox.ts +56 -0
- package/src/netbox/index.ts +8 -2
- package/src/netbox/prefix.test.ts +223 -0
- package/src/netbox/prefix.ts +68 -32
- package/src/netbox/resource.test.ts +67 -0
- package/src/netbox/resource.ts +133 -112
- package/dist/netbox/client.d.ts +0 -52
- package/dist/netbox/client.d.ts.map +0 -1
- package/src/netbox/client.test.ts +0 -67
- package/src/netbox/client.ts +0 -146
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GitHub.RepositoryRuleset`'s declared shape — split out of repository-ruleset.ts purely to
|
|
3
|
+
* keep that file (the provider wiring) under the 250-line cap; nothing here is provider logic.
|
|
4
|
+
*/
|
|
5
|
+
import type { RulesetProps } from 'alchemy/GitHub';
|
|
6
|
+
type UpstreamRules = NonNullable<RulesetProps['rules']>;
|
|
7
|
+
export interface RepositoryRulesetReviewerRule {
|
|
8
|
+
readonly filePatterns: readonly string[];
|
|
9
|
+
readonly minimumApprovals: number;
|
|
10
|
+
readonly reviewer: {
|
|
11
|
+
readonly actorId: number;
|
|
12
|
+
readonly actorType: 'Team';
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface RepositoryRulesetPullRequestRule extends NonNullable<UpstreamRules['pullRequest']> {
|
|
16
|
+
/** @default every method GitHub allows — the house baseline always pins `['squash']`. */
|
|
17
|
+
readonly allowedMergeMethods?: readonly ('merge' | 'squash' | 'rebase')[];
|
|
18
|
+
readonly requiredReviewers?: readonly RepositoryRulesetReviewerRule[];
|
|
19
|
+
/** `require_extra_approval_for_unattributed_changes` — see repository-ruleset.ts's H15. Only
|
|
20
|
+
* `false` is modeled: sending `true` has no house use, and GitHub already defaults absent to
|
|
21
|
+
* `true`. */
|
|
22
|
+
readonly extraApprovalForUnattributedChanges?: false;
|
|
23
|
+
}
|
|
24
|
+
export interface RepositoryRulesetStatusChecksRule extends NonNullable<UpstreamRules['requiredStatusChecks']> {
|
|
25
|
+
readonly doNotEnforceOnCreate?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface RepositoryRulesetRules extends Omit<UpstreamRules, 'pullRequest' | 'requiredStatusChecks'> {
|
|
28
|
+
/** `undefined`: no opinion (refused if the live ruleset already has one and this is not an
|
|
29
|
+
* exact-adopt pass-through — see repository-ruleset-guards.ts's `undeclaredLiveRuleRefusal`).
|
|
30
|
+
* `false`: declare its ABSENCE — either "this repo never had one" (a create, or an adopt
|
|
31
|
+
* where live already has none, both no-ops) or a deliberate removal of a live rule, which
|
|
32
|
+
* `ruleNarrowingRefusal` gates behind `acknowledgeRuleNarrowing` (see below). Added 2026-09-23 for
|
|
33
|
+
* exact-adopt pass-through: `requiredStatusChecks` already had this `false` escape; `deletion`/
|
|
34
|
+
* `nonFastForward`/etc already have it implicitly (plain `boolean`, so `false` is already a
|
|
35
|
+
* value); `pullRequest` was the one modeled rule with NO way to express "not present at all" —
|
|
36
|
+
* a repo like `taslabs-net/taslabs-net` (measured live 2026-09-23: `rules = [deletion,
|
|
37
|
+
* non_fast_forward]`, no `pull_request`) could not be adopted through this resource without
|
|
38
|
+
* either refusing to omit the key (a type error) or silently declaring a brand-new rule. */
|
|
39
|
+
readonly pullRequest?: RepositoryRulesetPullRequestRule | false;
|
|
40
|
+
/** `undefined`: no opinion (refused if the live ruleset already has one — see
|
|
41
|
+
* repository-ruleset-guards.ts). `false`: declare its ABSENCE — removes a live one. */
|
|
42
|
+
readonly requiredStatusChecks?: RepositoryRulesetStatusChecksRule | false;
|
|
43
|
+
}
|
|
44
|
+
export interface RepositoryRulesetProps extends Omit<RulesetProps, 'rules'> {
|
|
45
|
+
readonly rules?: RepositoryRulesetRules;
|
|
46
|
+
/**
|
|
47
|
+
* An explicit, reasoned sign-off that THIS declaration deliberately drops a live bypass actor
|
|
48
|
+
* (`bypassActors` is a strict subset of what is live). Without this, `reconcile` refuses the
|
|
49
|
+
* write rather than silently loosen a ruleset that already protects something — see
|
|
50
|
+
* `bypassNarrowingRefusal` in repository-ruleset-narrowing-guards.ts. `reason` is not logged or
|
|
51
|
+
* sent to GitHub; it exists so the declaration's own diff carries WHY, for the next reader
|
|
52
|
+
* (S21: a refusal names the fact it checked — this is the human-authored counterpart for the
|
|
53
|
+
* one case a fact alone cannot settle: "is this narrowing intentional?").
|
|
54
|
+
*
|
|
55
|
+
* ⛔ SEPARATE FROM `acknowledgeRuleNarrowing` ON PURPOSE (2026-09-23 review finding: a single
|
|
56
|
+
* shared `acknowledgeNarrowing` let one reason worded for a rule drop silently also rubber-
|
|
57
|
+
* stamp an unrelated bypass-actor drop in the SAME declaration — a `bypassActors: []` left
|
|
58
|
+
* over from an unrelated edit slipped through under a reason that never mentioned bypass at
|
|
59
|
+
* all). Each prop authorizes only the narrowing its own guard checks; a declaration that
|
|
60
|
+
* narrows both needs both, each with its own reason.
|
|
61
|
+
*
|
|
62
|
+
* ⛔ ALSO A STANDING PROP, NOT A ONE-TIME TOKEN — remove it from the declaration once the
|
|
63
|
+
* narrowing it describes has landed. Left in place, it keeps disabling the bypass-narrowing
|
|
64
|
+
* guard for every future `reconcile` of this resource, not only the change it was written
|
|
65
|
+
* for — the same way `rules: { deletion: false }` stays a standing "this rule stays absent"
|
|
66
|
+
* fact rather than a one-time action, only here the standing fact is "bypass narrowing needs
|
|
67
|
+
* no review", which is a fact you want true for as short a window as possible. A reviewer
|
|
68
|
+
* reading a diff that adds a SECOND, unrelated bypass drop under an OLD
|
|
69
|
+
* `acknowledgeBypassNarrowing` left over from a prior change is the failure mode this note
|
|
70
|
+
* exists to head off.
|
|
71
|
+
*/
|
|
72
|
+
readonly acknowledgeBypassNarrowing?: {
|
|
73
|
+
readonly reason: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The `rules`-side counterpart to `acknowledgeBypassNarrowing` above — an explicit, reasoned
|
|
77
|
+
* sign-off that THIS declaration deliberately drops a live rule (a modeled rule type declared
|
|
78
|
+
* `false`/boolean-`false` while the live ruleset still has it). Without this, `reconcile`
|
|
79
|
+
* refuses the write — see `ruleNarrowingRefusal` in repository-ruleset-narrowing-guards.ts.
|
|
80
|
+
* Same standing-prop caution as `acknowledgeBypassNarrowing`: remove it once the rule drop it
|
|
81
|
+
* describes has landed, or it keeps excusing every later rule drop on this resource too.
|
|
82
|
+
*/
|
|
83
|
+
readonly acknowledgeRuleNarrowing?: {
|
|
84
|
+
readonly reason: string;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=repository-ruleset-props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository-ruleset-props.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset-props.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,KAAK,aAAa,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;AAExD,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7E;AAED,MAAM,WAAW,gCAAiC,SAAQ,WAAW,CACnE,aAAa,CAAC,aAAa,CAAC,CAC7B;IACC,yFAAyF;IACzF,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,CAAC,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC;IAC1E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,6BAA6B,EAAE,CAAC;IACtE;;iBAEa;IACb,QAAQ,CAAC,mCAAmC,CAAC,EAAE,KAAK,CAAC;CACtD;AAED,MAAM,WAAW,iCAAkC,SAAQ,WAAW,CACpE,aAAa,CAAC,sBAAsB,CAAC,CACtC;IACC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAClD,aAAa,EACb,aAAa,GAAG,sBAAsB,CACvC;IACC;;;;;;;;;;gGAU4F;IAC5F,QAAQ,CAAC,WAAW,CAAC,EAAE,gCAAgC,GAAG,KAAK,CAAC;IAChE;2FACuF;IACvF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,iCAAiC,GAAG,KAAK,CAAC;CAC3E;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,sBAAsB,CAAC;IACxC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,QAAQ,CAAC,0BAA0B,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE;;;;;;;OAOG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository-ruleset-reconcile.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset-reconcile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEnG,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC9B,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"repository-ruleset-reconcile.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset-reconcile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEnG,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC9B,MAAM,gCAAgC,CAAC;AAgBxC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,aAAa,EAEnB,MAAM,+BAA+B,CAAC;AA+CvC;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,GAAG,KAAK,SAAS;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAAC;IACzD,QAAQ,CAAC,iBAAiB,EAAE,CAC1B,KAAK,EAAE,sBAAsB,KAC1B,wBAAwB,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,2BAA2B,CAAC;CACrE,KAAG,MAAM,CAAC,MAAM,CAAC,2BAA2B,EAAE,sBAAsB,GAAG,KAAK,EAAE,CAAC,CAoH5E,CAAC"}
|
|
@@ -40,38 +40,11 @@
|
|
|
40
40
|
* is not hand-rolled — only the choice of Octokit over distilled is a recorded divergence.
|
|
41
41
|
*/
|
|
42
42
|
import { Resource } from 'alchemy';
|
|
43
|
-
import type { GitHubCredentials
|
|
43
|
+
import type { GitHubCredentials } from 'alchemy/GitHub';
|
|
44
44
|
import * as Provider from 'alchemy/Provider';
|
|
45
45
|
import { type RulesetRecord } from './repository-ruleset-probe.ts';
|
|
46
|
-
type
|
|
47
|
-
export
|
|
48
|
-
readonly filePatterns: readonly string[];
|
|
49
|
-
readonly minimumApprovals: number;
|
|
50
|
-
readonly reviewer: {
|
|
51
|
-
readonly actorId: number;
|
|
52
|
-
readonly actorType: 'Team';
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
export interface RepositoryRulesetPullRequestRule extends NonNullable<UpstreamRules['pullRequest']> {
|
|
56
|
-
/** @default every method GitHub allows — the house baseline always pins `['squash']`. */
|
|
57
|
-
readonly allowedMergeMethods?: readonly ('merge' | 'squash' | 'rebase')[];
|
|
58
|
-
readonly requiredReviewers?: readonly RepositoryRulesetReviewerRule[];
|
|
59
|
-
/** `require_extra_approval_for_unattributed_changes` — see H15 above. Only `false` is
|
|
60
|
-
* modeled: sending `true` has no house use, and GitHub already defaults absent to `true`. */
|
|
61
|
-
readonly extraApprovalForUnattributedChanges?: false;
|
|
62
|
-
}
|
|
63
|
-
export interface RepositoryRulesetStatusChecksRule extends NonNullable<UpstreamRules['requiredStatusChecks']> {
|
|
64
|
-
readonly doNotEnforceOnCreate?: boolean;
|
|
65
|
-
}
|
|
66
|
-
export interface RepositoryRulesetRules extends Omit<UpstreamRules, 'pullRequest' | 'requiredStatusChecks'> {
|
|
67
|
-
readonly pullRequest?: RepositoryRulesetPullRequestRule;
|
|
68
|
-
/** `undefined`: no opinion (refused if the live ruleset already has one — see
|
|
69
|
-
* repository-ruleset-guards.ts). `false`: declare its ABSENCE — removes a live one. */
|
|
70
|
-
readonly requiredStatusChecks?: RepositoryRulesetStatusChecksRule | false;
|
|
71
|
-
}
|
|
72
|
-
export interface RepositoryRulesetProps extends Omit<RulesetProps, 'rules'> {
|
|
73
|
-
readonly rules?: RepositoryRulesetRules;
|
|
74
|
-
}
|
|
46
|
+
import type { RepositoryRulesetProps } from './repository-ruleset-props.ts';
|
|
47
|
+
export type { RepositoryRulesetProps, RepositoryRulesetPullRequestRule, RepositoryRulesetReviewerRule, RepositoryRulesetRules, RepositoryRulesetStatusChecksRule, } from './repository-ruleset-props.ts';
|
|
75
48
|
export interface RepositoryRulesetAttributes {
|
|
76
49
|
readonly rulesetId: number;
|
|
77
50
|
readonly nodeId: string | undefined;
|
|
@@ -85,5 +58,4 @@ export declare const RepositoryRuleset: import("alchemy").ResourceClass<Reposito
|
|
|
85
58
|
export declare const attrsOf: (r: RulesetRecord) => RepositoryRulesetAttributes;
|
|
86
59
|
export declare const repositoryRulesetHandlers: Provider.ProviderService<RepositoryRuleset, GitHubCredentials, never, never, GitHubCredentials, GitHubCredentials, never, never, never>;
|
|
87
60
|
export declare const RepositoryRulesetProvider: () => import("effect/Layer").Layer<Provider.Provider<RepositoryRuleset>, never, GitHubCredentials>;
|
|
88
|
-
export {};
|
|
89
61
|
//# sourceMappingURL=repository-ruleset.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository-ruleset.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"repository-ruleset.d.ts","sourceRoot":"","sources":["../../src/github/repository-ruleset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAQ7C,OAAO,EAAE,KAAK,aAAa,EAAe,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAG5E,YAAY,EACV,sBAAsB,EACtB,gCAAgC,EAChC,6BAA6B,EAC7B,sBAAsB,EACtB,iCAAiC,GAClC,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,0BAA0B,EAC1B,sBAAsB,EACtB,2BAA2B,EAC3B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAE5B,CAAC;AAEH,eAAO,MAAM,OAAO,MAAO,aAAa,KAAG,2BAMzC,CAAC;AA4CH,eAAO,MAAM,yBAAyB,yIAkEpC,CAAC;AAEH,eAAO,MAAM,yBAAyB,oGAC0B,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal in-memory NetBox REST API for this family's tests — an in-memory `fetch` handed to
|
|
3
|
+
* Effect's real `FetchHttpClient` through its `Fetch` reference, mirroring
|
|
4
|
+
* `../forgejo/fake-forgejo.ts` (itself mirroring `cloudflare/fake-mesh.ts`). Tests exercise
|
|
5
|
+
* distilled's REAL path assembly, JSON encode/decode and status→typed-error matching (`404` →
|
|
6
|
+
* `NotFound`, `403` → `Forbidden`, …) — nothing about that matching is re-implemented here, only
|
|
7
|
+
* the wire responses a scenario needs.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ TEST-ONLY, AND NO NETWORK. No provider imports this file. The base URL is RFC 2606
|
|
10
|
+
* `netbox.example.com`, and the token is a placeholder.
|
|
11
|
+
*/
|
|
12
|
+
import { type Credentials } from '@distilled.cloud/netbox/Credentials';
|
|
13
|
+
import * as Layer from 'effect/Layer';
|
|
14
|
+
export declare const FAKE_BASE = "https://netbox.example.com";
|
|
15
|
+
export declare const FAKE_TOKEN = "placeholder-netbox-token";
|
|
16
|
+
export type Seen = {
|
|
17
|
+
readonly method: string;
|
|
18
|
+
readonly path: string;
|
|
19
|
+
};
|
|
20
|
+
/** NetBox's `{ detail }` failure envelope (403/404) — matching protocol.ts's `errorEnvelope`. */
|
|
21
|
+
export declare const fakeFailure: (status: number, detail: string) => Response;
|
|
22
|
+
/**
|
|
23
|
+
* `route` decides every response; this wrapper only records what was sent and adapts to
|
|
24
|
+
* `globalThis.fetch`'s signature. A test that expects no request at all asserts `seen` is empty.
|
|
25
|
+
*/
|
|
26
|
+
export declare const fakeNetbox: (route: (method: string, url: URL) => Response) => {
|
|
27
|
+
fetch: typeof fetch;
|
|
28
|
+
seen: Seen[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* distilled Credentials + the real FetchHttpClient over the fake — `NetboxOpContext`, what every
|
|
32
|
+
* resource file's exported `spec.fetchLive` needs.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ NOT PAIRED WITH `handlers` (each resource file's `netboxHandlers(spec)` export). `handlers`
|
|
35
|
+
* bakes in `CredentialsFromEnv`, which resolves `NETBOX_URL` / `NETBOX_TOKEN` through Effect's
|
|
36
|
+
* `Config` — whose default provider snapshots `process.env` once and never re-reads it (measured
|
|
37
|
+
* in the forgejo migration, kit PR 180: mutating `process.env` mid-test-run had no effect). So a
|
|
38
|
+
* test cannot point `handlers` at a fake server; it calls `spec.fetchLive`/`spec.attributes`
|
|
39
|
+
* directly instead, with this explicit layer, exercising exactly the same production code
|
|
40
|
+
* without that env seam.
|
|
41
|
+
*/
|
|
42
|
+
export declare const fakeNetboxLayer: (fetchFn: typeof globalThis.fetch, creds?: Layer.Layer<Credentials>) => Layer.Layer<Credentials | import("effect/unstable/http/HttpClient").HttpClient, never, never>;
|
|
43
|
+
//# sourceMappingURL=fake-netbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-netbox.d.ts","sourceRoot":"","sources":["../../src/netbox/fake-netbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,qCAAqC,CAAC;AACpF,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,eAAO,MAAM,SAAS,+BAA+B,CAAC;AACtD,eAAO,MAAM,UAAU,6BAA6B,CAAC;AAErD,MAAM,MAAM,IAAI,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,iGAAiG;AACjG,eAAO,MAAM,WAAW,WAAY,MAAM,UAAU,MAAM,aACnB,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,UAAU,UAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ;;;CAUvE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,YACjB,OAAO,UAAU,CAAC,KAAK,UACzB,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,kGAC+D,CAAC"}
|
package/dist/netbox/index.d.ts
CHANGED
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* ★ The constraint reader IS exported, because a consumer writing its own family needs the same
|
|
9
9
|
* guard — and because a table nobody can reach is a table nobody uses.
|
|
10
10
|
*/
|
|
11
|
-
export { NetboxError, apiBase } from './client.ts';
|
|
12
11
|
export { NETBOX_CONSTRAINTS, bodyViolations, constraintsFor, guardBody, } from './constraint-guard.ts';
|
|
13
12
|
export { type EndpointConstraints, type EndpointKey, type NetboxBody, type ParamConstraint, refusal, violations, } from './constraints.ts';
|
|
14
13
|
export { NETBOX_CONSTRAINTS_DIGEST } from './generated/constraints/index.ts';
|
|
15
14
|
export { NetboxPrefix, NetboxPrefixProvider, type PrefixAttributes, type PrefixProps, type PrefixStatus, } from './prefix.ts';
|
|
16
|
-
export { type NetboxRequirements, type NetboxSpec, netboxHandlers } from './resource.ts';
|
|
15
|
+
export { LOCATE_PAGE, locateOne, type NetboxRequirements, type NetboxSpec, netboxHandlers, soleMatch, } from './resource.ts';
|
|
17
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/netbox/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/netbox/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,OAAO,EACP,UAAU,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAC7E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,WAAW,EACX,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,cAAc,EACd,SAAS,GACV,MAAM,eAAe,CAAC"}
|
package/dist/netbox/index.js
CHANGED
|
@@ -1,59 +1,5 @@
|
|
|
1
|
-
// src/netbox/client.ts
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
4
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
5
|
-
var PATH_PREFIX = "/api";
|
|
6
|
-
var apiBase = () => {
|
|
7
|
-
const raw = process.env["NETBOX_URL"];
|
|
8
|
-
if (raw === undefined || raw.trim() === "") {
|
|
9
|
-
throw new Error("NETBOX_URL is not set. Point it at your NetBox instance, e.g. https://netbox.example.com");
|
|
10
|
-
}
|
|
11
|
-
return raw.trim().replace(/\/$/, "");
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
class NetboxError extends Error {
|
|
15
|
-
status;
|
|
16
|
-
method;
|
|
17
|
-
path;
|
|
18
|
-
constructor(status, method, path, detail) {
|
|
19
|
-
super(`NetBox ${method} ${path} -> ${String(status)}: ${detail}`);
|
|
20
|
-
this.status = status;
|
|
21
|
-
this.method = method;
|
|
22
|
-
this.path = path;
|
|
23
|
-
this.name = "NetboxError";
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
var token = () => Effect.gen(function* () {
|
|
27
|
-
const raw = process.env["NETBOX_TOKEN"];
|
|
28
|
-
if (raw === undefined || raw.trim() === "") {
|
|
29
|
-
return yield* Effect.fail(new NetboxError(0, "GET", "", "NETBOX_TOKEN is unset. Mint a scoped token and export it — never store it in Alchemy props."));
|
|
30
|
-
}
|
|
31
|
-
return raw.trim();
|
|
32
|
-
});
|
|
33
|
-
var netbox = (method, path, body) => Effect.gen(function* () {
|
|
34
|
-
const credential = yield* token();
|
|
35
|
-
const url = `${apiBase()}${PATH_PREFIX}/${path.replace(/^\//, "")}`;
|
|
36
|
-
const client = yield* HttpClient.HttpClient;
|
|
37
|
-
const request = HttpClientRequest.make(method)(url).pipe(HttpClientRequest.setHeaders({
|
|
38
|
-
Accept: "application/json",
|
|
39
|
-
Authorization: `Token ${credential}`
|
|
40
|
-
}), body === undefined ? (self) => self : HttpClientRequest.bodyText(JSON.stringify(body), "application/json"));
|
|
41
|
-
const response = yield* client.execute(request).pipe(Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))));
|
|
42
|
-
if (response.status < 200 || response.status >= 300) {
|
|
43
|
-
const detail = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
44
|
-
return yield* Effect.fail(new NetboxError(response.status, method, path, detail.slice(0, 300)));
|
|
45
|
-
}
|
|
46
|
-
const text = yield* response.text.pipe(Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))));
|
|
47
|
-
return text === "" ? undefined : JSON.parse(text);
|
|
48
|
-
});
|
|
49
|
-
var soleMatch = (rows, describe) => {
|
|
50
|
-
if (rows.length > 1) {
|
|
51
|
-
throw new Error(`${describe} matched ${String(rows.length)} NetBox objects. An object's identity must be ` + "unique; narrow it (the VRF, the site, the tenant) rather than taking the first row.");
|
|
52
|
-
}
|
|
53
|
-
return rows[0];
|
|
54
|
-
};
|
|
55
1
|
// src/netbox/constraint-guard.ts
|
|
56
|
-
import * as
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
57
3
|
|
|
58
4
|
// src/netbox/constraints.ts
|
|
59
5
|
var lengthOf = (value) => [...value].length;
|
|
@@ -141,16 +87,17 @@ var constraintsFor = (key) => {
|
|
|
141
87
|
return table;
|
|
142
88
|
};
|
|
143
89
|
var bodyViolations = (key, body, presence) => violations(constraintsFor(key), body, { presence });
|
|
144
|
-
var guardBody = (key, body, presence) =>
|
|
90
|
+
var guardBody = (key, body, presence) => Effect.suspend(() => {
|
|
145
91
|
if (key === undefined)
|
|
146
|
-
return
|
|
92
|
+
return Effect.void;
|
|
147
93
|
const found = bodyViolations(key, body, presence);
|
|
148
|
-
return found.length === 0 ?
|
|
94
|
+
return found.length === 0 ? Effect.void : Effect.die(new Error(refusal(key, found)));
|
|
149
95
|
});
|
|
150
96
|
// src/netbox/prefix.ts
|
|
151
97
|
import { Resource } from "alchemy";
|
|
152
98
|
import * as Provider from "alchemy/Provider";
|
|
153
|
-
import * as
|
|
99
|
+
import * as ipam from "@distilled.cloud/netbox/ipam";
|
|
100
|
+
import * as Effect3 from "effect/Effect";
|
|
154
101
|
|
|
155
102
|
// src/netbox/prefix-form.ts
|
|
156
103
|
var DEFAULT_STATUS = "active";
|
|
@@ -186,27 +133,27 @@ var prefixMatches = (attributes, props) => {
|
|
|
186
133
|
|
|
187
134
|
// src/netbox/resource.ts
|
|
188
135
|
import { isResolved } from "alchemy/Diff";
|
|
189
|
-
import
|
|
190
|
-
|
|
136
|
+
import { CredentialsFromEnv } from "@distilled.cloud/netbox/Credentials";
|
|
137
|
+
import * as Effect2 from "effect/Effect";
|
|
191
138
|
var LOCATE_PAGE = 20;
|
|
192
|
-
var
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
139
|
+
var soleMatch = (rows, describe) => {
|
|
140
|
+
if (rows.length > 1) {
|
|
141
|
+
throw new Error(`${describe} matched ${String(rows.length)} NetBox objects. An object's identity must be ` + "unique; narrow it (the VRF, the site, the tenant) rather than taking the first row.");
|
|
142
|
+
}
|
|
143
|
+
return rows[0];
|
|
144
|
+
};
|
|
145
|
+
var locateOne = (list, describe, identifies) => list.pipe(Effect2.map((page) => {
|
|
146
|
+
if (page.count > LOCATE_PAGE) {
|
|
147
|
+
throw new Error(`${describe}: the locate filter matched ${String(page.count)} rows, more than the ` + `${String(LOCATE_PAGE)}-row page this reads. That is not a natural key — narrow the ` + "filter rather than paging through candidates.");
|
|
148
|
+
}
|
|
149
|
+
const rows = identifies === undefined ? page.results : page.results.filter(identifies);
|
|
150
|
+
return soleMatch(rows, describe);
|
|
151
|
+
}));
|
|
196
152
|
var netboxOperations = (spec) => {
|
|
197
|
-
const
|
|
198
|
-
if (list !== undefined && list.count > LOCATE_PAGE) {
|
|
199
|
-
throw new Error(`${spec.describe(props)}: the locate filter matched ${String(list.count)} rows, more ` + `than the ${String(LOCATE_PAGE)}-row page this reads. That is not a natural key — ` + "narrow the filter rather than paging through candidates.");
|
|
200
|
-
}
|
|
201
|
-
const rows = list?.results ?? [];
|
|
202
|
-
const narrow = spec.identifies;
|
|
203
|
-
return soleMatch(narrow === undefined ? rows : rows.filter((row) => narrow(row, props)), spec.describe(props));
|
|
204
|
-
}));
|
|
205
|
-
const objectPath = (live) => `${spec.collection}/${String(live["id"])}`;
|
|
206
|
-
const read = (props) => fetchLive(props).pipe(Effect3.map((live) => live === undefined ? undefined : spec.attributes(live, props)));
|
|
153
|
+
const read = (props) => spec.fetchLive(props).pipe(Effect2.map((live) => live === undefined ? undefined : spec.attributes(live, props)));
|
|
207
154
|
return {
|
|
208
155
|
read,
|
|
209
|
-
diff: (news, output) =>
|
|
156
|
+
diff: (news, output) => Effect2.gen(function* () {
|
|
210
157
|
if (output === undefined || !isResolved(news))
|
|
211
158
|
return;
|
|
212
159
|
const live = yield* read(news);
|
|
@@ -214,46 +161,47 @@ var netboxOperations = (spec) => {
|
|
|
214
161
|
return { action: "update" };
|
|
215
162
|
if (spec.matches(live, news))
|
|
216
163
|
return { action: "noop" };
|
|
217
|
-
return spec.
|
|
164
|
+
return spec.update === undefined ? { action: "replace" } : { action: "update" };
|
|
218
165
|
}),
|
|
219
|
-
reconcile: (news) =>
|
|
220
|
-
const before = yield* fetchLive(news);
|
|
166
|
+
reconcile: (news) => Effect2.gen(function* () {
|
|
167
|
+
const before = yield* spec.fetchLive(news);
|
|
221
168
|
if (before === undefined) {
|
|
222
169
|
const body = spec.createBody(news);
|
|
223
170
|
yield* guardBody(spec.endpoint.create, body, true);
|
|
224
|
-
yield*
|
|
225
|
-
} else if (spec.
|
|
171
|
+
yield* spec.create(news, body);
|
|
172
|
+
} else if (spec.update !== undefined) {
|
|
226
173
|
const mapped = spec.attributes(before, news);
|
|
227
174
|
if (mapped !== undefined && !spec.matches(mapped, news)) {
|
|
228
|
-
const body = spec.
|
|
175
|
+
const body = spec.update.body(news);
|
|
229
176
|
yield* guardBody(spec.endpoint.update, body, false);
|
|
230
177
|
if (Object.keys(body).length > 0) {
|
|
231
|
-
yield*
|
|
178
|
+
yield* spec.update.call(news, before, body);
|
|
232
179
|
}
|
|
233
180
|
}
|
|
234
181
|
}
|
|
235
182
|
const after = yield* read(news);
|
|
236
183
|
if (after === undefined) {
|
|
237
|
-
return yield*
|
|
184
|
+
return yield* Effect2.die(new Error(`${spec.describe(news)}: the write returned no error but the object is still ` + "absent. NetBox returns the object it wrote — read back rather than trusting the " + "status code."));
|
|
238
185
|
}
|
|
239
186
|
return after;
|
|
240
187
|
}),
|
|
241
|
-
destroy: (olds) =>
|
|
242
|
-
const live = yield* fetchLive(olds);
|
|
188
|
+
destroy: (olds) => Effect2.gen(function* () {
|
|
189
|
+
const live = yield* spec.fetchLive(olds);
|
|
243
190
|
if (live === undefined)
|
|
244
191
|
return;
|
|
245
|
-
yield*
|
|
192
|
+
yield* spec.destroy(olds, live);
|
|
246
193
|
})
|
|
247
194
|
};
|
|
248
195
|
};
|
|
249
196
|
var netboxHandlers = (spec) => {
|
|
250
197
|
const ops = netboxOperations(spec);
|
|
198
|
+
const withCredentials = (effect) => Effect2.provide(effect, CredentialsFromEnv);
|
|
251
199
|
return {
|
|
252
|
-
list: () =>
|
|
253
|
-
read: ({ olds }) => ops.read(olds),
|
|
254
|
-
diff: ({ news, output }) => ops.diff(news, output),
|
|
255
|
-
reconcile: ({ news }) => ops.reconcile(news),
|
|
256
|
-
delete: ({ olds }) => ops.destroy(olds)
|
|
200
|
+
list: () => Effect2.succeed([]),
|
|
201
|
+
read: ({ olds }) => withCredentials(ops.read(olds)),
|
|
202
|
+
diff: ({ news, output }) => withCredentials(ops.diff(news, output)),
|
|
203
|
+
reconcile: ({ news }) => withCredentials(ops.reconcile(news)),
|
|
204
|
+
delete: ({ olds }) => withCredentials(ops.destroy(olds))
|
|
257
205
|
};
|
|
258
206
|
};
|
|
259
207
|
|
|
@@ -281,51 +229,55 @@ var fk = (value) => {
|
|
|
281
229
|
var NetboxPrefix = Resource("Netbox.Prefix", {
|
|
282
230
|
defaultRemovalPolicy: "retain"
|
|
283
231
|
});
|
|
284
|
-
var
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
vrf: fk(live["vrf"])
|
|
300
|
-
};
|
|
301
|
-
},
|
|
302
|
-
collection: "ipam/prefixes",
|
|
232
|
+
var attributesOf = (live, props) => ({
|
|
233
|
+
comments: text(live.comments),
|
|
234
|
+
description: text(live.description),
|
|
235
|
+
isPool: live.is_pool === true,
|
|
236
|
+
markUtilized: live.mark_utilized === true,
|
|
237
|
+
prefix: live.prefix || props.prefix,
|
|
238
|
+
prefixId: live.id,
|
|
239
|
+
status: choice(live.status),
|
|
240
|
+
tenant: fk(live.tenant),
|
|
241
|
+
vlan: fk(live.vlan),
|
|
242
|
+
vrf: fk(live.vrf)
|
|
243
|
+
});
|
|
244
|
+
var spec = {
|
|
245
|
+
attributes: attributesOf,
|
|
246
|
+
create: (_props, body) => ipam.createIpamPrefix({ body }),
|
|
303
247
|
createBody: prefixBody,
|
|
248
|
+
destroy: (_props, live) => ipam.ipamPrefixesDestroy({ id: live.id }),
|
|
304
249
|
describe: (props) => `ipam/prefixes ${props.prefix}`,
|
|
305
250
|
endpoint: {
|
|
306
251
|
create: "netbox:POST /api/ipam/prefixes/",
|
|
307
252
|
update: "netbox:PATCH /api/ipam/prefixes/{id}/"
|
|
308
253
|
},
|
|
309
|
-
|
|
310
|
-
locate: (props) => [["prefix", props.prefix]],
|
|
254
|
+
fetchLive: (props) => locateOne(ipam.listIpamPrefixes({ limit: LOCATE_PAGE, prefix: [props.prefix] }), `ipam/prefixes ${props.prefix}`, (row) => fk(row.vrf) === props.vrf),
|
|
311
255
|
matches: prefixMatches,
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
256
|
+
update: {
|
|
257
|
+
body: prefixBody,
|
|
258
|
+
call: (_props, live, body) => ipam.updateIpamPrefixesPartial({
|
|
259
|
+
...body,
|
|
260
|
+
id: live.id
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var handlers = netboxHandlers(spec);
|
|
265
|
+
var NetboxPrefixProvider = () => Provider.effect(NetboxPrefix, Effect3.succeed(NetboxPrefix.Provider.of(handlers)));
|
|
315
266
|
export {
|
|
267
|
+
LOCATE_PAGE,
|
|
316
268
|
NETBOX_CONSTRAINTS,
|
|
317
269
|
NETBOX_CONSTRAINTS_DIGEST,
|
|
318
|
-
NetboxError,
|
|
319
270
|
NetboxPrefix,
|
|
320
271
|
NetboxPrefixProvider,
|
|
321
|
-
apiBase,
|
|
322
272
|
bodyViolations,
|
|
323
273
|
constraintsFor,
|
|
324
274
|
guardBody,
|
|
275
|
+
locateOne,
|
|
325
276
|
netboxHandlers,
|
|
326
277
|
refusal,
|
|
278
|
+
soleMatch,
|
|
327
279
|
violations
|
|
328
280
|
};
|
|
329
281
|
|
|
330
|
-
//# debugId=
|
|
282
|
+
//# debugId=546DA12AD639183464756E2164756E21
|
|
331
283
|
//# sourceMappingURL=index.js.map
|