@homeflare/alchemy 0.10.1 → 0.12.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/README.md +3 -3
- package/dist/caddy/admin-guard.d.ts +1 -1
- package/dist/caddy/index.js.map +4 -4
- package/dist/cloudflare/client.d.ts +1 -1
- package/dist/cloudflare/index.js.map +2 -2
- package/dist/cloudflare/lock-rules.d.ts +1 -1
- package/dist/forgejo/branch-protection-form.d.ts +1 -1
- package/dist/forgejo/branch-protection.d.ts +1 -1
- package/dist/forgejo/index.js.map +10 -10
- package/dist/forgejo/org-actions-secrets.d.ts +1 -1
- package/dist/forgejo/org-label.d.ts +1 -1
- package/dist/forgejo/org-team.d.ts +1 -1
- package/dist/forgejo/repo-webhook.d.ts +1 -1
- package/dist/forgejo/repository.d.ts +2 -2
- package/dist/forgejo/repository.d.ts.map +1 -1
- package/dist/forgejo/resource.d.ts +2 -2
- package/dist/forgejo/team-member.d.ts +1 -1
- package/dist/launchd/index.js.map +2 -2
- package/dist/openbao/auth-role.d.ts +1 -1
- package/dist/openbao/bao-http.d.ts +4 -4
- package/dist/openbao/cloudflare-permission-groups.d.ts +1 -1
- package/dist/openbao/cloudflare-role-form.d.ts +2 -2
- package/dist/openbao/cloudflare-role-form.d.ts.map +1 -1
- package/dist/openbao/cloudflare-roles-expand.d.ts +1 -1
- package/dist/openbao/digest.d.ts +1 -1
- package/dist/openbao/forgejo-bootstrap-parse.d.ts +2 -2
- package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -1
- package/dist/openbao/forgejo-bootstrap.d.ts +1 -1
- package/dist/openbao/index.js +10 -8
- package/dist/openbao/index.js.map +18 -18
- package/dist/openbao/mount.d.ts +1 -1
- package/dist/openbao/pki-role.d.ts +1 -1
- package/dist/openbao/policy.d.ts +2 -2
- package/dist/openbao/proxmox-role-form.d.ts +2 -2
- package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
- package/dist/openbao/proxmox-role.d.ts +10 -10
- package/dist/openbao/ssh-role.d.ts +3 -3
- package/dist/ownership/adopting.d.ts +34 -0
- package/dist/ownership/adopting.d.ts.map +1 -0
- package/dist/ownership/probe.d.ts +15 -5
- package/dist/ownership/probe.d.ts.map +1 -1
- package/dist/ownership/rows.d.ts +12 -0
- package/dist/ownership/rows.d.ts.map +1 -1
- package/dist/ownership/store-fixture.d.ts +10 -0
- package/dist/ownership/store-fixture.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts +2 -2
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/api-token-form.d.ts +2 -2
- package/dist/proxmox/api-token.d.ts +11 -11
- package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
- package/dist/proxmox/ceph-daemon.d.ts +19 -19
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
- package/dist/proxmox/ceph-flag.d.ts +5 -5
- package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
- package/dist/proxmox/ceph-fs.d.ts +16 -16
- package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
- package/dist/proxmox/ceph-osd.d.ts +5 -5
- package/dist/proxmox/ceph-pool-form.d.ts +13 -13
- package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool.d.ts +3 -3
- package/dist/proxmox/client.d.ts +2 -2
- package/dist/proxmox/credentials.d.ts +8 -8
- package/dist/proxmox/credentials.d.ts.map +1 -1
- package/dist/proxmox/fake-pve-lxc.d.ts +8 -0
- package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -1
- package/dist/proxmox/firewall-alias-form.d.ts +1 -1
- package/dist/proxmox/firewall-alias.d.ts +1 -1
- package/dist/proxmox/generated/pbs.d.ts +1 -1
- package/dist/proxmox/generated/pve.d.ts +1 -1
- package/dist/proxmox/group.d.ts +8 -8
- package/dist/proxmox/ha-resource.d.ts +3 -2
- package/dist/proxmox/ha-resource.d.ts.map +1 -1
- package/dist/proxmox/ha-rule-form.d.ts +5 -5
- package/dist/proxmox/ha-rule.d.ts +3 -3
- package/dist/proxmox/index.d.ts +6 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +465 -217
- package/dist/proxmox/index.js.map +63 -58
- package/dist/proxmox/lease-cache.d.ts +4 -4
- package/dist/proxmox/lxc-adoption.d.ts +32 -0
- package/dist/proxmox/lxc-adoption.d.ts.map +1 -0
- package/dist/proxmox/lxc-harness.d.ts +12 -0
- package/dist/proxmox/lxc-harness.d.ts.map +1 -1
- package/dist/proxmox/lxc-props.d.ts +2 -2
- package/dist/proxmox/lxc.d.ts +8 -8
- package/dist/proxmox/lxc.d.ts.map +1 -1
- package/dist/proxmox/members.d.ts +3 -3
- package/dist/proxmox/network-apply-read.d.ts +5 -5
- package/dist/proxmox/network-apply.d.ts +9 -9
- package/dist/proxmox/node-network-form.d.ts +6 -6
- package/dist/proxmox/node-network.d.ts +17 -17
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/dist/proxmox/notification-target.d.ts +3 -2
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
- package/dist/proxmox/pbs-datastore.d.ts +10 -10
- package/dist/proxmox/pbs-prune-job.d.ts +2 -2
- package/dist/proxmox/pbs-sync-job.d.ts +4 -4
- package/dist/proxmox/pbs-verify-job.d.ts +1 -1
- package/dist/proxmox/pool.d.ts +4 -4
- package/dist/proxmox/provision-baseline.d.ts +74 -0
- package/dist/proxmox/provision-baseline.d.ts.map +1 -0
- package/dist/proxmox/provision-bootstrap.d.ts +33 -0
- package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
- package/dist/proxmox/provision-cli-fake.d.ts +38 -0
- package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
- package/dist/proxmox/provision-declare.d.ts +25 -0
- package/dist/proxmox/provision-declare.d.ts.map +1 -0
- package/dist/proxmox/replication-job.d.ts +9 -8
- package/dist/proxmox/replication-job.d.ts.map +1 -1
- package/dist/proxmox/resource.d.ts +2 -2
- package/dist/proxmox/resource.d.ts.map +1 -1
- package/dist/proxmox/sdn-apply-read.d.ts +7 -7
- package/dist/proxmox/sdn-apply.d.ts +1 -1
- package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
- package/dist/proxmox/sdn-subnet.d.ts +5 -5
- package/dist/proxmox/sdn-vnet.d.ts +5 -6
- package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
- package/dist/proxmox/storage.d.ts +4 -3
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts +3 -2
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/proxmox/values.d.ts +1 -1
- package/dist/proxmox/zfs-pool-write.d.ts +1 -1
- package/dist/proxmox/zfs-pool.d.ts +5 -5
- package/dist/talos/credentials.d.ts +1 -1
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/index.js.map +1 -1
- package/docs/adopt-verify.md +21 -5
- package/docs/adopted-deploys.md +5 -3
- package/docs/ownership.md +27 -2
- package/docs/provision-baseline.md +106 -0
- package/docs/proxmox-lxc-adopt.md +83 -0
- package/docs/proxmox-lxc.md +12 -49
- package/docs/proxmox.md +8 -0
- package/package.json +1 -1
- package/src/caddy/admin-guard.ts +1 -1
- package/src/caddy/local-admin.ts +1 -1
- package/src/cloudflare/client.ts +1 -1
- package/src/cloudflare/lock-rules.ts +1 -1
- package/src/forgejo/branch-protection-form.ts +1 -1
- package/src/forgejo/branch-protection.ts +1 -1
- package/src/forgejo/client.ts +1 -1
- package/src/forgejo/org-actions-secrets.ts +1 -1
- package/src/forgejo/org-label.ts +1 -1
- package/src/forgejo/org-team.ts +1 -1
- package/src/forgejo/repo-webhook.ts +1 -1
- package/src/forgejo/repository.ts +2 -2
- package/src/forgejo/resource.ts +2 -2
- package/src/forgejo/team-member.ts +1 -1
- package/src/openbao/README.md +4 -4
- package/src/openbao/auth-role.ts +1 -1
- package/src/openbao/bao-http.test.ts +2 -2
- package/src/openbao/bao-http.ts +6 -6
- package/src/openbao/bao-status.test.ts +1 -1
- package/src/openbao/bao-status.ts +1 -1
- package/src/openbao/cloudflare-group-scope.test.ts +1 -1
- package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
- package/src/openbao/cloudflare-permission-groups.ts +3 -3
- package/src/openbao/cloudflare-policy.ts +1 -1
- package/src/openbao/cloudflare-role-form.ts +2 -2
- package/src/openbao/cloudflare-roles-expand.ts +1 -1
- package/src/openbao/digest.ts +1 -1
- package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
- package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
- package/src/openbao/forgejo-bootstrap.ts +4 -4
- package/src/openbao/kubernetes-role-form.test.ts +5 -5
- package/src/openbao/mount.ts +1 -1
- package/src/openbao/pki-role.ts +1 -1
- package/src/openbao/policy-wire.test.ts +3 -3
- package/src/openbao/policy.ts +2 -2
- package/src/openbao/proxmox-role-form.ts +5 -5
- package/src/openbao/proxmox-role.ts +11 -11
- package/src/openbao/ssh-role.ts +3 -3
- package/src/ownership/adopting.test.ts +63 -0
- package/src/ownership/adopting.ts +46 -0
- package/src/ownership/ownership.test.ts +1 -34
- package/src/ownership/probe.ts +27 -14
- package/src/ownership/rows.ts +8 -2
- package/src/ownership/store-fixture.ts +50 -0
- package/src/proxmox/acl.ts +4 -4
- package/src/proxmox/adopt-noop.test.ts +6 -6
- package/src/proxmox/api-token-form.ts +4 -4
- package/src/proxmox/api-token.ts +13 -13
- package/src/proxmox/ceph-daemon-form.ts +3 -3
- package/src/proxmox/ceph-daemon.ts +20 -20
- package/src/proxmox/ceph-flag.ts +7 -7
- package/src/proxmox/ceph-fs-wire.ts +7 -7
- package/src/proxmox/ceph-fs.ts +18 -18
- package/src/proxmox/ceph-osd-tree.ts +16 -16
- package/src/proxmox/ceph-osd.ts +6 -6
- package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
- package/src/proxmox/ceph-pool-form.ts +13 -13
- package/src/proxmox/ceph-pool.ts +6 -6
- package/src/proxmox/client.test.ts +3 -3
- package/src/proxmox/client.ts +3 -3
- package/src/proxmox/credentials.test.ts +6 -8
- package/src/proxmox/credentials.ts +8 -8
- package/src/proxmox/fake-pve-lxc.ts +13 -1
- package/src/proxmox/firewall-alias-form.ts +1 -1
- package/src/proxmox/firewall-alias.ts +1 -1
- package/src/proxmox/generated/pbs.ts +1 -1
- package/src/proxmox/generated/pve.ts +1 -1
- package/src/proxmox/group.ts +10 -10
- package/src/proxmox/ha-resource.ts +3 -2
- package/src/proxmox/ha-rule-form.ts +5 -5
- package/src/proxmox/ha-rule.ts +3 -3
- package/src/proxmox/index.test.ts +7 -0
- package/src/proxmox/index.ts +14 -0
- package/src/proxmox/lease-cache.test.ts +19 -19
- package/src/proxmox/lease-cache.ts +4 -4
- package/src/proxmox/lxc-adopt.test.ts +16 -23
- package/src/proxmox/lxc-adoption.ts +50 -0
- package/src/proxmox/lxc-create-form.ts +1 -1
- package/src/proxmox/lxc-guards.test.ts +10 -17
- package/src/proxmox/lxc-harness.ts +27 -4
- package/src/proxmox/lxc-judge.test.ts +5 -5
- package/src/proxmox/lxc-lifecycle.ts +1 -1
- package/src/proxmox/lxc-ownership.test.ts +19 -14
- package/src/proxmox/lxc-props.ts +2 -2
- package/src/proxmox/lxc-read.test.ts +3 -3
- package/src/proxmox/lxc-strict-adopt.test.ts +104 -0
- package/src/proxmox/lxc-volume.test.ts +9 -9
- package/src/proxmox/lxc-volume.ts +1 -1
- package/src/proxmox/lxc-wire.ts +1 -1
- package/src/proxmox/lxc.ts +19 -15
- package/src/proxmox/members.test.ts +1 -1
- package/src/proxmox/members.ts +3 -3
- package/src/proxmox/metric-server-otel.ts +2 -2
- package/src/proxmox/network-apply-read.ts +10 -10
- package/src/proxmox/network-apply.ts +10 -10
- package/src/proxmox/node-network-form.ts +7 -7
- package/src/proxmox/node-network.ts +18 -18
- package/src/proxmox/notification-target.ts +3 -2
- package/src/proxmox/pbs-datastore-guard.ts +1 -1
- package/src/proxmox/pbs-datastore.ts +10 -10
- package/src/proxmox/pbs-prune-job.ts +2 -2
- package/src/proxmox/pbs-sync-job.ts +4 -4
- package/src/proxmox/pbs-verify-job.ts +1 -1
- package/src/proxmox/pool.test.ts +2 -2
- package/src/proxmox/pool.ts +4 -4
- package/src/proxmox/provision-baseline.test.ts +87 -0
- package/src/proxmox/provision-baseline.ts +162 -0
- package/src/proxmox/provision-bootstrap.test.ts +132 -0
- package/src/proxmox/provision-bootstrap.ts +135 -0
- package/src/proxmox/provision-cli-fake.ts +217 -0
- package/src/proxmox/provision-declare.test.ts +111 -0
- package/src/proxmox/provision-declare.ts +103 -0
- package/src/proxmox/replication-job.ts +9 -8
- package/src/proxmox/resource.ts +3 -3
- package/src/proxmox/sdn-apply-read.test.ts +16 -16
- package/src/proxmox/sdn-apply-read.ts +7 -7
- package/src/proxmox/sdn-apply.ts +3 -3
- package/src/proxmox/sdn-subnet-form.ts +1 -1
- package/src/proxmox/sdn-subnet.ts +5 -5
- package/src/proxmox/sdn-vnet.ts +5 -6
- package/src/proxmox/storage.ts +4 -3
- package/src/proxmox/user.ts +3 -2
- package/src/proxmox/values.test.ts +2 -2
- package/src/proxmox/values.ts +1 -1
- package/src/proxmox/zfs-pool-write.ts +1 -1
- package/src/proxmox/zfs-pool.ts +6 -6
- package/src/talos/credentials.ts +3 -3
- package/src/talos/values.test.ts +9 -9
- package/src/verify/cli.test.ts +1 -1
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"sources": ["../src/cloudflare/r2-bucket-lock.ts", "../src/cloudflare/client.ts", "../src/cloudflare/lock-rules.ts", "../src/cloudflare/mesh-node.ts", "../src/cloudflare/mesh-node-api.ts", "../src/cloudflare/mesh-node-form.ts", "../src/cloudflare/mesh-node-lifecycle.ts", "../src/cloudflare/mesh-node-token.ts", "../src/cloudflare/providers.ts", "../src/cloudflare/website.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { Unowned } from 'alchemy/AdoptPolicy';\nimport * as Provider from 'alchemy/Provider';\nimport { Resource, type ResourceClass } from 'alchemy/Resource';\n/**\n * `Cloudflare.R2BucketLock` — the lock rule set on one R2 bucket, declared rather than applied by\n * hand. A bucket lock is a RETENTION FLOOR: while a rule covers an object, no API call, no\n * lifecycle rule and no credential can delete it.\n *\n * 🔴 WHY THIS EXISTS AT ALL, AND WHAT IT REPLACES. a consuming stack shipped the retention floor as a\n * documented `wrangler r2 bucket lock add …` step in the runbook, on the measured ground that\n * `alchemy@2.0.0-beta.77` has no lock property anywhere in its R2 namespace. That measurement is\n * still true and it was the wrong conclusion: a step a human runs once is a step an Alchemy plan\n * can never show missing, so removing the floor by hand leaves that stack, its plan and\n * every gate completely green. The operator's rule (2026-09-15) is to cover such a gap with a\n * custom resource over the vendor's SDK, which is what this is.\n *\n * ⛔ THE SDK ALREADY HAD IT, AND THAT IS WORTH SAYING BECAUSE IT DECIDED THE SHAPE. Measured\n * 2026-09-15 in `cloudflare@4.5.0`, `resources/r2/buckets/locks.d.ts`:\n *\n * update(bucketName: string, params: LockUpdateParams, options?): APIPromise<…>\n * get(bucketName: string, params: LockGetParams, options?): APIPromise<LockGetResponse>\n *\n * over `PUT`/`GET /accounts/{account_id}/r2/buckets/{bucket_name}/lock` with the jurisdiction\n * carried as the `cf-r2-jurisdiction` header (`locks.mjs:17` and `:41`). So there is no raw\n * `client.put` fallback here and no path string this package invented — the fallback the rule\n * allows was not needed.\n *\n * ⚠️ THE RULE SET IS A REPLACE, NOT A MERGE. `PUT` takes the WHOLE list, exactly like R2's\n * lifecycle rules — which is the trap a consuming stack records for\n * `homeflare-git`. Declaring this resource on a bucket whose live rules nobody has read would\n * drop them, so `read` fetches them first and `reconcile` refuses to write when they already\n * match.\n */\nimport type Cloudflare from 'cloudflare';\nimport { NotFoundError } from 'cloudflare';\nimport * as Effect from 'effect/Effect';\nimport { CloudflareApi } from './client.ts';\nimport {\n type Jurisdiction,\n type R2LockRule,\n jurisdictionOf,\n rulesEqual,\n toBody,\n} from './lock-rules.ts';\nimport type { Providers } from './providers.ts';\n\nexport interface R2BucketLockProps {\n readonly accountId: string;\n readonly bucketName: string;\n /** ⚠️ Part of the bucket's IDENTITY, not a preference — a bucket in another jurisdiction is another bucket. */\n readonly jurisdiction?: Jurisdiction;\n /**\n * ⛔ REQUIRED, AND ITS EMPTY VALUE IS THE DANGEROUS ONE. `rules: []` means NO FLOOR: it is what\n * `delete` sends to take a lock off deliberately. An optional property would let a declaration\n * that simply forgot it send `[]` and silently unlock the repository, reporting success — the\n * same reasoning `LiteLLM.McpServer.allowedTools` is required for.\n */\n readonly rules: readonly R2LockRule[];\n}\n\nexport type R2BucketLockAttributes = {\n readonly bucketName: string;\n readonly jurisdiction: Jurisdiction;\n readonly rules: readonly R2LockRule[];\n};\n\nexport interface R2BucketLock extends Resource<\n 'Cloudflare.R2BucketLock',\n R2BucketLockProps,\n R2BucketLockAttributes,\n never,\n Providers\n> {}\n\n// ⚠️ EXPLICIT ANNOTATION, REQUIRED BY isolatedDeclarations. This file was internal to a\n// monorepo before it was published; a published package emits its .d.ts from a separate\n// tsc pass, so every exported symbol must state its type rather than infer it.\nexport const R2BucketLock: ResourceClass<R2BucketLock> = Resource<R2BucketLock>(\n 'Cloudflare.R2BucketLock',\n {\n /**\n * ⛔ RETAIN, AND ON THIS RESOURCE IT IS NOT THE ESTATE'S HABIT BUT THE WHOLE POINT. A lock is a\n * safety floor: removing the declaration must not remove the floor, because the state it\n * protects is the only physical backup of the estate's one Postgres. `delete` below is\n * implemented — a provider that cannot delete cannot be tested or corrected — and it is\n * reachable only by an operator running the CLI directly, past a deploy wrapper.\n */\n defaultRemovalPolicy: 'retain',\n },\n);\n\n/**\n * 🔴 A 404 HERE IS \"NOTHING TO ADOPT\", AND THE FIRST PLAN PROVED IT IS THE ORDINARY CASE.\n * Measured 2026-09-15, an alchemy plan --detailed on a bucket that does not\n * exist yet: `404 {\"success\":false,\"errors\":[{\"code\":10006,\"message\":\"The specified bucket does\n * not exist.\"}]}` — raised from `read`, during PLANNING, before anything is created. Alchemy\n * calls `read` for a resource with no prior state, so on every first deploy the lock is asked\n * about a bucket the same plan is about to create. Letting that throw makes the resource\n * impossible to plan; swallowing it with `catchAll` would also hide a 403 from a mis-scoped\n * token, which is the failure this estate spends the most time on. So: 404 → absent, everything\n * else propagates.\n *\n * ⛔ AN EMPTY LIVE RULE SET IS \"NO LOCK\", NOT \"A LOCK WITH NOTHING IN IT\", so it reads as absent\n * and the resource CREATES. The API answers `{}` for a bucket that has never been locked and\n * `{ rules: [] }` for one whose rules were removed; both mean the floor is gone.\n * ⚠️ `Unowned` UNTIL THIS STACK HAS WRITTEN IT. A lock someone applied by hand — which is exactly\n * what the retired runbook step did — must not be silently rewritten by a mistyped declaration;\n * the planner refuses until the resource is piped through `adopt(true)`, which is one line an\n * operator reads in the diff. An adopted rule set that already MATCHES then costs no request at\n * all, because `reconcile` below returns before writing.\n */\nexport const readLock = (\n client: Cloudflare,\n props: { accountId: string; bucketName: string; jurisdiction?: Jurisdiction },\n written: boolean,\n): Effect.Effect<R2BucketLockAttributes | undefined> =>\n Effect.gen(function* () {\n const jurisdiction = jurisdictionOf(props);\n const live = yield* Effect.tryPromise({\n try: () =>\n client.r2.buckets.locks.get(props.bucketName, {\n account_id: props.accountId,\n jurisdiction,\n }),\n catch: (cause) => cause,\n }).pipe(\n Effect.catchIf(\n (cause) => cause instanceof NotFoundError,\n () => Effect.succeed(undefined),\n ),\n Effect.orDie,\n );\n if (live === undefined) return undefined;\n const rules = (live.rules ?? []) as readonly R2LockRule[];\n if (rules.length === 0) return undefined;\n const attributes: R2BucketLockAttributes = {\n bucketName: props.bucketName,\n jurisdiction,\n rules,\n };\n return written ? attributes : Unowned(attributes);\n });\n\n/**\n * ⛔ A NO-OP MAKES NO REQUEST, AND ON A LOCK THAT MATTERS MORE THAN TIDINESS. `PUT` replaces the\n * rule set; re-sending an identical one on every deploy is a write against the only thing\n * standing between this repository and a delete, for no change. `output` is what the last deploy\n * persisted (or what `read` just adopted), so the comparison costs nothing either.\n */\nexport const reconcileLock = (\n client: Cloudflare,\n news: R2BucketLockProps,\n output: R2BucketLockAttributes | undefined,\n): Effect.Effect<R2BucketLockAttributes> =>\n Effect.gen(function* () {\n const jurisdiction = jurisdictionOf(news);\n if (output !== undefined && rulesEqual(output.rules, news.rules)) return output;\n yield* Effect.promise(() =>\n client.r2.buckets.locks.update(news.bucketName, {\n account_id: news.accountId,\n jurisdiction,\n ...toBody(news.rules),\n }),\n );\n return { bucketName: news.bucketName, jurisdiction, rules: news.rules };\n });\n\n/**\n * ⛔ DELETING THIS RESOURCE MEANS PUTTING AN EMPTY RULE SET, WHICH IS UNLOCKING THE BUCKET. It is\n * implemented because the retirement path is real — R2 refuses to empty a bucket while any lock\n * rule is configured, so retiring the repository has to start here — and it is behind\n * `defaultRemovalPolicy: 'retain'` and the estate CLI's refusal of `destroy` precisely because\n * no ordinary deploy should ever reach it.\n */\nexport const deleteLock = (\n client: Cloudflare,\n output: R2BucketLockAttributes,\n accountId: string,\n): Effect.Effect<void> =>\n Effect.gen(function* () {\n yield* Effect.promise(() =>\n client.r2.buckets.locks.update(output.bucketName, {\n account_id: accountId,\n jurisdiction: output.jurisdiction,\n rules: [],\n }),\n );\n });\n\nexport const R2BucketLockProvider = () =>\n Provider.effect(\n R2BucketLock,\n Effect.gen(function* () {\n const client = yield* CloudflareApi;\n return R2BucketLock.Provider.of({\n /**\n * ⛔ EMPTY, AND ALCHEMY'S OWN CONTRACT ASKS FOR EXACTLY THAT. `ProviderService.list`:\n * \"Resources with no native enumeration API (… sub-resources keyed entirely by a parent)\n * should return an empty array rather than throwing.\" There is no list-locks endpoint —\n * a lock is reachable only through the bucket that owns it — so enumerating would mean\n * listing every bucket in the account and GETting each one's lock.\n * ⛔⛔ AND `nuke.singleton` IS WHY THAT IS NOT MERELY UNAVAILABLE BUT UNWANTED. `list`\n * feeds `alchemy unsafe nuke`, which lists and then deletes; `delete` here is an UNLOCK.\n * A working enumeration would hand one command the ability to strip the retention floor\n * off every bucket in the account — the precise event a floor exists to make impossible.\n * `singleton` is the documented word for it: always-present configuration whose delete\n * resets rather than removes.\n */\n list: () => Effect.succeed([]),\n nuke: { singleton: true },\n read: Effect.fn(function* ({ olds, output }) {\n const props = olds ?? undefined;\n if (props === undefined) return undefined;\n return yield* readLock(client, props, output !== undefined);\n }),\n reconcile: Effect.fn(({ news, output }) => reconcileLock(client, news, output)),\n delete: Effect.fn(({ olds, output }) => deleteLock(client, output, olds.accountId)),\n });\n }),\n );\n",
|
|
6
|
-
"/**\n * The Cloudflare REST API as an Effect service, over `cloudflare-typescript`. Codex standard 5:\n * the vendor's SDK, never a hand-rolled client — and never a hand-rolled `fetch` either.\n *\n * ⛔ THE SDK WAS ALREADY IN THE STORE AND DECLARED BY NOBODY. Measured 2026-09-15:\n * `node_modules/.pnpm/cloudflare@4.5.0` exists as a transitive dependency of `alchemy`, and\n * `pnpm ls cloudflare --depth 0` at the root and in every workspace package returns nothing.\n *
|
|
7
|
-
"/**\n * The vocabulary of an R2 lock rule, and the comparison that decides whether to write one.\n *\n * ★ ITS OWN MODULE FOR THE REASON
|
|
6
|
+
"/**\n * The Cloudflare REST API as an Effect service, over `cloudflare-typescript`. Codex standard 5:\n * the vendor's SDK, never a hand-rolled client — and never a hand-rolled `fetch` either.\n *\n * ⛔ THE SDK WAS ALREADY IN THE STORE AND DECLARED BY NOBODY. Measured 2026-09-15:\n * `node_modules/.pnpm/cloudflare@4.5.0` exists as a transitive dependency of `alchemy`, and\n * `pnpm ls cloudflare --depth 0` at the root and in every workspace package returns nothing.\n * `<estate>/local/scripts/vendor-sdk-ratchet.ts` row R2 says exactly that — \"not declared anywhere\n * in the workspace yet … the row adds the dependency once, then converts the TS callers\" — so\n * this package is that row's first half, and the twenty ratcheted offences keep their rows.\n *\n * ⛔ AND IT IS PINNED EXACTLY, NOT `catalog:`. There is no catalog entry for `cloudflare`, and the\n * version that resolves today is the one alchemy pulls in. Pinning `4.5.0` means a future\n * alchemy bump moves alchemy's copy and not this package's contract — the alternative is a\n * provider whose request shape changes because an unrelated dependency was upgraded.\n *\n * ⚠️ `fetch` IS A SEAM, NOT A CONVENIENCE — the same argument `packages/litellm-client` makes. The\n * SDK hands its custom fetch the URL and init it built, so a test double conforming to\n * `Core.Fetch` exercises the real path assembly, the real body serialisation and the real header\n * set. A test that stubbed `client.r2.buckets.locks.update` would prove only that a stub was\n * called, which is precisely the class of check this estate keeps finding green over nothing.\n */\nimport Cloudflare from 'cloudflare';\nimport * as Config from 'effect/Config';\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\n\n/** What the SDK accepts for `fetch`, restated so tests can name it without importing SDK internals. */\nexport type ClientFetch = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\nexport class CloudflareApi extends Context.Service<CloudflareApi, Cloudflare>()(\n 'homeflare/CloudflareApi',\n) {}\n\n/**\n * ⚠️ THE FAKE FETCH IS THE ONLY REASON THIS TAKES OPTIONS AT ALL. Everything else the SDK reads\n * from the environment the estate CLI already populates.\n */\nexport const makeClient = (options: { apiToken: string; fetch?: ClientFetch }): Cloudflare =>\n new Cloudflare({\n apiToken: options.apiToken,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n\n/**\n * ⛔ FAIL CLOSED ON THE VALUE, NEVER ON THE VARIABLE EXISTING. AGENTS.md \"the four that cost the\n * most\" §4: a denied OpenBao path renders the template EMPTY rather than erroring, and every\n * Cloudflare call then answers 403 exactly like a revoked token — which sends the operator to\n * Cloudflare to rotate a credential that was fine. Your deploy wrapper should write\n * CLOUDFLARE_API_TOKEN from the stack's declared role, short-lived and scoped.\n */\nconst MISSING_TOKEN =\n 'CLOUDFLARE_API_TOKEN is not set. Mint a short-lived, scoped token and export it before ' +\n 'role named in estate.stack.ts; running `alchemy` directly skips that and there is no default.';\n\nconst EMPTY_TOKEN =\n 'CLOUDFLARE_API_TOKEN is set but EMPTY. That is a denied or missing OpenBao grant on the ' +\n 'stack’s role, not a rotated token — re-mint it rather than rotating anything at Cloudflare.';\n\n/**\n * ⛔ A LAYER'S ERROR CHANNEL MUST BE `never`, so a ConfigError becomes a defect carrying a sentence\n * rather than a type error pushed up into `Stack`. Same decision, same words, as `LiteLLMLive`.\n */\nexport const CloudflareApiLive = Layer.effect(\n CloudflareApi,\n Effect.gen(function* () {\n const token = yield* Config.Redacted('CLOUDFLARE_API_TOKEN').pipe(\n Effect.catchTag('ConfigError', () => Effect.die(new Error(MISSING_TOKEN))),\n );\n const value = Redacted.value(token);\n if (value.length === 0) return yield* Effect.die(new Error(EMPTY_TOKEN));\n return makeClient({ apiToken: value });\n }),\n);\n",
|
|
7
|
+
"/**\n * The vocabulary of an R2 lock rule, and the comparison that decides whether to write one.\n *\n * ★ ITS OWN MODULE FOR THE REASON `<estate>/litellm/src/equal.ts` IS ONE: the comparison is the\n * riskiest function in the provider and it deserves to be readable and testable without the\n * lifecycle around it. An equality that returned true for everything would make every reconcile\n * a no-op and the resource would silently never write anything — which looks exactly like a\n * converged stack.\n */\n/** Jurisdictions the API accepts, as the SDK declares them. */\nexport type Jurisdiction = 'default' | 'eu' | 'fedramp';\n\n/**\n * One lock rule. ⚠️ `prefix` OMITTED MEANS THE WHOLE BUCKET — the SDK documents an empty prefix as\n * the way to scope a rule to every object, and omitting the key is how that is spelled on the wire.\n */\nexport type R2LockRule = {\n readonly id: string;\n readonly enabled: boolean;\n readonly prefix?: string;\n readonly condition:\n | { readonly type: 'Age'; readonly maxAgeSeconds: number }\n | { readonly type: 'Date'; readonly date: string }\n | { readonly type: 'Indefinite' };\n};\n\n/** ⚠️ `undefined` and `'default'` are the same jurisdiction; normalise so a diff never sees both. */\nexport const jurisdictionOf = (props: { readonly jurisdiction?: Jurisdiction }): Jurisdiction =>\n props.jurisdiction ?? 'default';\n\n/**\n * ⛔ FIELD BY FIELD, IN ORDER, AND NOT `JSON.stringify` OF EITHER SIDE. The API returns rules in\n * its own key order and omits `prefix` where this repo may spell it `''`; a stringify compare\n * would report drift on every plan and PUT an identical body each deploy. Order across the list\n * IS compared, because the rule set is a replace and the list is what gets sent.\n */\nexport function rulesEqual(a: readonly R2LockRule[], b: readonly R2LockRule[]): boolean {\n if (a.length !== b.length) return false;\n return a.every((left, index) => {\n const right = b[index];\n if (right === undefined) return false;\n if (left.id !== right.id || left.enabled !== right.enabled) return false;\n if ((left.prefix ?? '') !== (right.prefix ?? '')) return false;\n const [lc, rc] = [left.condition, right.condition];\n if (lc.type !== rc.type) return false;\n if (lc.type === 'Age') return rc.type === 'Age' && lc.maxAgeSeconds === rc.maxAgeSeconds;\n if (lc.type === 'Date') return rc.type === 'Date' && lc.date === rc.date;\n return true;\n });\n}\n\n/**\n * The body the SDK sends. Split out so the test can assert the wire shape against one function.\n *\n * ⚠️ IT TAKES THE RULES, NOT THE PROPS, so this module names nothing from the resource — the\n * comparison and the vocabulary stay readable without the lifecycle around them.\n */\nexport const toBody = (rules: readonly R2LockRule[]): { rules: R2LockRule[] } => ({\n rules: rules.map((rule) => ({ ...rule })),\n});\n",
|
|
8
8
|
"/**\n * `Cloudflare.MeshNode` — a Cloudflare Mesh node (a `warp_connector` object), declared without\n * its token ever touching Alchemy state. Usage and the enrolment step: docs/mesh-node.md.\n *\n * 🔴 WHY THIS EXISTS ALONGSIDE `Cloudflare.Tunnel.WarpConnector`. A Mesh node that fronts a\n * private service needs a node record in the graph, so the Gateway rules and DNS records that\n * name it deploy together. Alchemy beta.79's WarpConnector would put the node token — the\n * credential that lets any Linux host join the account's Mesh as that node — into plaintext\n * state on every read, and it cannot create an HA node. See mesh-node-form.ts.\n *\n * ★ REMOVAL POLICY: `retain` BY DEFAULT — see the ★ on `MeshNode` below, and what it does to an\n * `ha` replace in mesh-node-lifecycle.ts.\n */\nimport { Credentials } from '@distilled.cloud/cloudflare/Credentials';\nimport * as Cloudflare from 'alchemy/Cloudflare';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport { Resource, type ResourceClass } from 'alchemy/Resource';\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport {\n type MeshNodeAttributes,\n type MeshNodeProps,\n diffMeshNode,\n validateMeshNode,\n} from './mesh-node-form.ts';\nimport { deleteMeshNode, readMeshNode, reconcileMeshNode } from './mesh-node-lifecycle.ts';\nimport type { Providers } from './providers.ts';\n\nexport interface MeshNode extends Resource<\n 'Cloudflare.MeshNode',\n MeshNodeProps,\n MeshNodeAttributes,\n never,\n Providers\n> {}\n\n/**\n * ★ `defaultRemovalPolicy: 'retain'`, LIKE EVERY KIT RESOURCE WHOSE DELETION BREAKS ITS CONSUMERS\n * (the `Bao.*` families, `R2BucketLock`, the Proxmox storage families; the reasoning is written\n * once in proxmox/resource.ts). Deleting a node cuts every enrolled replica off the Mesh, and\n * nothing brings it back: a new node has a new id, a new token and a new Mesh IP per replica, so\n * each host must be re-enrolled by hand and every Gateway rule or allow-list that pins the old\n * Mesh IP breaks. So dropping the declaration, or `alchemy destroy`, leaves the node live.\n * ⚠️ IT ALSO SKIPS THE OLD NODE'S DELETE IN A REPLACE (Apply.ts, `deleteOldGenerations` and the\n * GC's `retainOldGeneration`, beta.79). A same-name `ha` change therefore REFUSES, deleting\n * nothing, until that deploy opts in with `.pipe(RemovalPolicy.destroy())`; a create-first\n * replace leaves the old node live and unmanaged. Both measured in mesh-node-policy.test.ts.\n * ⛔ `delete` STAYS FULLY IMPLEMENTED. Retain is Terraform's `prevent_destroy`, not a stub: it runs\n * the moment a declaration opts in with `.pipe(RemovalPolicy.destroy())`.\n */\nexport const MeshNode: ResourceClass<MeshNode> = Resource<MeshNode>('Cloudflare.MeshNode', {\n defaultRemovalPolicy: 'retain',\n});\n\n/** What every call below needs: the account, credentials and HTTP — Alchemy's own trio. */\nexport type MeshNodeServices =\n | Cloudflare.CloudflareEnvironment\n | Credentials\n | HttpClient.HttpClient;\n\n/** The account id, resolved the way every `alchemy/Cloudflare` provider resolves it. */\nconst accountId = Effect.gen(function* () {\n const resolved = yield* yield* Cloudflare.CloudflareEnvironment;\n return resolved.accountId;\n});\n\n/**\n * ★ `Provider.effect`, CAPTURING THE THREE SERVICES AT BUILD. `Provider.succeed` (what Alchemy's\n * own providers use) leaves each handler to find CloudflareEnvironment and Credentials in the\n * engine's context at call time — which works for `Cloudflare.providers()` only because it\n * `provideMerge`s them into the stack. Capturing them here keeps this provider correct whether\n * or not the stack also merges `Cloudflare.providers()`.\n */\nexport const MeshNodeProvider = () =>\n Provider.effect(\n MeshNode,\n Effect.gen(function* () {\n const services = Context.make(\n Cloudflare.CloudflareEnvironment,\n yield* Cloudflare.CloudflareEnvironment,\n ).pipe(\n Context.add(Credentials, yield* Credentials),\n Context.add(HttpClient.HttpClient, yield* HttpClient.HttpClient),\n );\n const run = <A, E>(effect: Effect.Effect<A, E, MeshNodeServices>) =>\n Effect.provideContext(effect, services);\n\n return MeshNode.Provider.of({\n stables: ['id', 'accountId', 'ha'],\n /**\n * ⛔ EMPTY, AND `nuke.skip`. Alchemy's own `Cloudflare.Tunnel.WarpConnector` already lists\n * every `warp_connector` in the account for `alchemy unsafe nuke`; a second listing here\n * would make nuke delete each node twice. `read` does not depend on this.\n */\n list: () => Effect.succeed([]),\n nuke: { skip: true },\n /**\n * ⛔ VALIDATE AT PLAN TIME, NOT ONLY IN `reconcile`. An `ha` replace is delete-first: the\n * engine deletes the old node BEFORE the new generation's reconcile runs, so a\n * declaration refused there would leave no node at all. launchd's job-preflight.ts\n * refuses at plan time for the same reason.\n */\n diff: ({ news, output }) =>\n run(\n Effect.gen(function* () {\n const invalid = isResolved(news) ? validateMeshNode(news) : undefined;\n if (invalid !== undefined) return yield* Effect.fail(invalid);\n return diffMeshNode(news, output, yield* accountId);\n }),\n ),\n read: ({ olds, output }) =>\n run(Effect.flatMap(accountId, (account) => readMeshNode(account, olds, output))),\n reconcile: ({ news, output }) =>\n run(Effect.flatMap(accountId, (account) => reconcileMeshNode(account, news, output))),\n delete: ({ output }) => run(deleteMeshNode(output)),\n });\n }),\n );\n",
|
|
9
9
|
"/**\n * The `warp_connector` calls a Cloudflare Mesh node needs, and nothing else.\n *\n * ★ A MESH NODE IS A `warp_connector` OBJECT. Cloudflare's Mesh docs create one with\n * `POST /accounts/{account_id}/warp_connector` and fetch its enrolment token from\n * `GET …/warp_connector/{id}/token` (developers.cloudflare.com/mesh/get-started, read\n * 2026-09-21). There is no separate \"mesh node\" endpoint.\n *\n * ★ OVER `@distilled.cloud/cloudflare`, THE SDK ALCHEMY'S OWN CLOUDFLARE PROVIDERS USE, not the\n * `cloudflare` npm SDK the R2 lock uses. Measured 2026-09-21: `cloudflare@4.5.0`'s\n * `WARPConnectorCreateParams` has only `account_id` and `name`, so it cannot create an HA node.\n * distilled `1.0.0-rc.12` (the version alchemy@2.0.0-beta.79 pins) has `ha?: boolean` on\n * `CreateTunnelWarpConnectorRequest`, typed errors (`TunnelNotFound` code 1002 / 404,\n * `DuplicateTunnelName` code 1013), and resolves credentials and the account exactly the way\n * `Cloudflare.Tunnel.WarpConnector` and `Cloudflare.Gateway.Rule` do — so the node and the Gateway\n * rules that pin its Mesh IP always land in the same account.\n *\n * ⛔ NO FUNCTION IN THIS FILE READS THE NODE TOKEN, and `observe` copies named fields only. The\n * HA docs say the create response carries a `token` field; distilled's decoder copies only the\n * fields in its schema (protocol.ts, `getProps(outputAst)`), and `observe` below would drop it\n * again if it ever got through. The one token read lives in mesh-node-token.ts, on demand.\n */\nimport * as zeroTrust from '@distilled.cloud/cloudflare/zero-trust';\nimport * as Data from 'effect/Data';\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\n\n/** `inactive` (never run), `degraded`, `healthy` or `down` — Cloudflare's own four. */\nexport type MeshNodeStatus = 'inactive' | 'degraded' | 'healthy' | 'down';\n\n/** A live node as this package sees it. ⛔ There is no token field here, on purpose. */\nexport interface ObservedNode {\n readonly id: string;\n readonly name: string;\n readonly status: MeshNodeStatus | undefined;\n}\n\n/** A refusal or failure, carrying the sentence an operator needs. Never a token. */\nexport class MeshNodeError extends Data.TaggedError('MeshNodeError')<{\n readonly message: string;\n}> {}\n\ntype Raw = {\n readonly id?: string | null;\n readonly name?: string | null;\n readonly status?: string | null;\n readonly deletedAt?: string | null;\n};\n\nconst STATUSES: readonly string[] = ['inactive', 'degraded', 'healthy', 'down'];\n\n/**\n * ⚠️ A DELETED NODE IS NOT A NODE. Cloudflare keeps deleted tunnels (every response carries\n * `deleted_at`, and the list takes `is_deleted`), so a `GET` by id can answer for one. Treating\n * that as live would \"adopt\" a tombstone and PATCH it.\n */\nexport const observe = (raw: Raw): ObservedNode | undefined => {\n if (typeof raw.id !== 'string' || raw.id.length === 0) return undefined;\n if (raw.deletedAt !== undefined && raw.deletedAt !== null) return undefined;\n const status =\n typeof raw.status === 'string' && STATUSES.includes(raw.status)\n ? (raw.status as MeshNodeStatus)\n : undefined;\n return { id: raw.id, name: raw.name ?? '', status };\n};\n\n/** The node with this id, or `undefined` when it is gone (404 / code 1002, or soft-deleted). */\nexport const getNode = (accountId: string, nodeId: string) =>\n zeroTrust.getTunnelWarpConnector({ accountId, tunnelId: nodeId }).pipe(\n Effect.map(observe),\n Effect.catchTag('TunnelNotFound', () => Effect.succeed(undefined)),\n );\n\n/**\n * The live node with exactly this name, or `undefined`.\n *\n * ★ `name` is also sent as the list filter, but the match is re-checked here: the filter's\n * exactness is not documented, and a prefix match would adopt the wrong node.\n * ⛔ Two live matches is refused rather than resolved. Names are unique per account (the create\n * answers code 1013 otherwise), so two means something this package does not understand.\n */\nexport const findNodeByName = (accountId: string, name: string) =>\n zeroTrust.listTunnelWarpConnectors.items({ accountId, name, isDeleted: false }).pipe(\n Stream.map(observe),\n Stream.filter((node): node is ObservedNode => node !== undefined && node.name === name),\n Stream.runCollect,\n Effect.flatMap((chunk) => {\n const matches = Array.from(chunk);\n if (matches.length <= 1) return Effect.succeed(matches[0]);\n const ids = matches.map((node) => node.id).join(', ');\n return Effect.fail(\n new MeshNodeError({\n message: `Mesh node name \"${name}\" matches ${matches.length} live nodes (${ids}). Names are unique per account, so refusing to guess which one is meant.`,\n }),\n );\n }),\n );\n\n/**\n * ⛔ `ha` IS SENT ON CREATE AND NOWHERE ELSE. Cloudflare's HA page: \"High availability is set at\n * node creation time and cannot be changed afterward.\" The PATCH body (`PatchTunnelWarpConnector\n * Request`) has only `name` and `tunnelSecret`.\n */\nexport const createNode = (accountId: string, name: string, ha: boolean) =>\n zeroTrust.createTunnelWarpConnector({ accountId, name, ha }).pipe(\n Effect.flatMap((raw) => {\n const node = observe(raw);\n return node === undefined\n ? Effect.fail(\n new MeshNodeError({ message: `Creating Mesh node \"${name}\" returned no id.` }),\n )\n : Effect.succeed(node);\n }),\n );\n\n/** ★ A rename is a PATCH: same id, same token, same enrolled replicas and Mesh IP. */\nexport const renameNode = (accountId: string, nodeId: string, name: string) =>\n zeroTrust\n .patchTunnelWarpConnector({ accountId, tunnelId: nodeId, name })\n .pipe(Effect.map((raw) => observe(raw) ?? { id: nodeId, name, status: undefined }));\n\n/** Idempotent: a node already gone (404 / code 1002) is a successful delete. */\nexport const deleteNode = (accountId: string, nodeId: string) =>\n zeroTrust.deleteTunnelWarpConnector({ accountId, tunnelId: nodeId }).pipe(\n Effect.asVoid,\n Effect.catchTag('TunnelNotFound', () => Effect.void),\n );\n",
|
|
10
10
|
"/**\n * `Cloudflare.MeshNode` — its props, its attributes, and the pure `diff` that decides between\n * update and replace.\n *\n * ⛔ THE ATTRIBUTES NEVER HOLD THE NODE TOKEN. That is the whole reason this resource exists:\n * Alchemy beta.79's `Cloudflare.Tunnel.WarpConnector` calls `getTunnelWarpConnectorToken` in\n * `toAttributes`, so EVERY read, reconcile and list fetches the token, and Alchemy state (which\n * is not encrypted) stores it — `Redacted` hides it from logs, not from the state store. It also\n * has no `ha` prop. The token is a runtime credential: fetch it on demand with\n * `fetchMeshNodeToken` (mesh-node-token.ts) and write it straight to the host.\n * ★ A BONUS OF NOT READING IT: the token endpoint needs a Write permission (\"Cloudflare One\n * Connectors Write\", per its API reference, read 2026-09-21), so a plan over this resource can\n * run on a read-only token. WarpConnector's cannot.\n */\nimport type { Diff } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport { MeshNodeError, type MeshNodeStatus } from './mesh-node-api.ts';\n\nexport interface MeshNodeProps {\n /**\n * The node's name in the account — and its identity when adopting (names are unique per\n * account). ★ Changing it RENAMES the node in place (`PATCH`): the id, the token, the\n * enrolled replicas and their Mesh IPs all survive, so a rename never re-enrols a host.\n */\n readonly name: string;\n /**\n * High availability (active-passive replicas sharing one node token).\n *\n * ⛔ CREATE-ONLY, AND REQUIRED FOR THAT REASON. Cloudflare's Mesh HA page: \"High availability\n * is set at node creation time and cannot be changed afterward.\" The PATCH body carries no\n * `ha`. So a change REPLACES the node — a new id, a new token, a new Mesh IP per replica —\n * and a default would silently fix an irreversible choice. The API's own default is `false`;\n * the dashboard's is `true`. Say which one you mean.\n * ★ A NODE WITH NO ROUTES — A DOOR — IS `ha: false`, AND A SECOND DOOR IS A SECOND NODE. HA\n * fails over the CIDR routes a node advertises; \"Nodes without routes do not benefit from HA\n * failover\", because the node's Mesh IP is tied to the individual replica (same page, \"When to\n * use\" and \"Considerations\", read 2026-09-21). After a failover the promoted replica answers\n * on its own Mesh IP, so nothing that dials a door's first replica follows it. Declare the\n * second door as its own `MeshNode` (own name, token, Mesh IP) and have callers list both.\n * ⚠️ `ha` NEEDS MASQUE on the node's device profile (same page).\n */\n readonly ha: boolean;\n}\n\nexport interface MeshNodeAttributes {\n /** The node's UUID (the `warp_connector` tunnel id). Pass it to `fetchMeshNodeToken`. */\n readonly id: string;\n /** The account the provider environment resolved when the node was written. */\n readonly accountId: string;\n readonly name: string;\n /** As of the last read or write; it moves on its own as replicas connect and drop. */\n readonly status: MeshNodeStatus | undefined;\n /**\n * The value the node was CREATED with (or declared with when adopted — see mesh-node.md).\n * ⚠️ No documented read returns it: `GET /warp_connector/{id}` has no `ha` field (API\n * reference, read 2026-09-21), so drift here cannot be observed, only prevented.\n */\n readonly ha: boolean;\n}\n\n/** ⛔ Fail closed on a declaration the API would accept but nobody meant. */\nexport const validateMeshNode = (props: MeshNodeProps): MeshNodeError | undefined => {\n if (typeof props.name !== 'string' || props.name.trim().length === 0) {\n return new MeshNodeError({ message: 'MeshNode `name` must be a non-empty string.' });\n }\n if (props.name !== props.name.trim()) {\n return new MeshNodeError({\n message: `MeshNode name \"${props.name}\" has leading or trailing whitespace; adoption matches names exactly.`,\n });\n }\n if (typeof props.ha !== 'boolean') {\n return new MeshNodeError({\n message: 'MeshNode `ha` must be true or false. It is create-only, so it has no default.',\n });\n }\n return undefined;\n};\n\n/**\n * update or replace, from the declaration and what was last written.\n *\n * - Account changed → `replace`, create-first: names are unique per ACCOUNT, so the two\n * generations cannot collide.\n * - `ha` changed → `replace`. ⛔ `deleteFirst` when the name stays the same: the new node needs\n * the name the old one holds, so create-first would fail with code 1013 (or, worse, a\n * create path that \"converged\" on the old node would record an `ha` it does not have).\n * Create-first when the name changes in the same edit, because then nothing collides.\n * ⚠️ UNDER THE DEFAULT `retain` (mesh-node.ts) the engine skips the old node's delete in both\n * orders: delete-first then REFUSES at the create (mesh-node-lifecycle.ts) until the deploy\n * opts in with `RemovalPolicy.destroy()`, and create-first leaves the old node live, unmanaged.\n * The answer here is the same under both policies: `diff` is not handed the policy (Provider.ts,\n * its input), and delete-first is exactly what lets the opted-in deploy reuse the name.\n * - Name changed alone → `update` (a PATCH rename).\n * - Otherwise → no opinion; the engine compares props.\n *\n * ★ An unresolved `ha` gives no answer here; `reconcile` re-checks it once resolved and refuses\n * rather than updating across a create-only change. An unresolved `name` next to a changed `ha`\n * counts as \"unchanged\", which picks the delete-first order that cannot collide.\n */\nexport const diffMeshNode = (\n news: Input<MeshNodeProps>,\n output: MeshNodeAttributes | undefined,\n accountId: string,\n): Diff | undefined => {\n if (output === undefined) return undefined;\n if (output.accountId !== accountId) return { action: 'replace' };\n const fields = news as { readonly name?: unknown; readonly ha?: unknown };\n const name = typeof fields.name === 'string' ? fields.name : undefined;\n if (typeof fields.ha === 'boolean' && fields.ha !== output.ha) {\n const sameName = name === undefined || name === output.name;\n return sameName ? { action: 'replace', deleteFirst: true } : { action: 'replace' };\n }\n if (name !== undefined && name !== output.name) return { action: 'update' };\n return undefined;\n};\n",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The vocabulary of an R2 lock rule, and the comparison that decides whether to write one.
|
|
3
3
|
*
|
|
4
|
-
* ★ ITS OWN MODULE FOR THE REASON
|
|
4
|
+
* ★ ITS OWN MODULE FOR THE REASON `<estate>/litellm/src/equal.ts` IS ONE: the comparison is the
|
|
5
5
|
* riskiest function in the provider and it deserves to be readable and testable without the
|
|
6
6
|
* lifecycle around it. An equality that returned true for everything would make every reconcile
|
|
7
7
|
* a no-op and the resource would silently never write anything — which looks exactly like a
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Create/PATCH bodies for `Forgejo.BranchProtection` — extracted for the 250-line cap.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json` — create posts
|
|
5
5
|
* `CreateBranchProtectionOption`; update sends `EditBranchProtectionOption` fields only.
|
|
6
6
|
*/
|
|
7
7
|
import type { BranchProtectionProps } from './branch-protection.ts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.BranchProtection` — one rule at `/repos/{owner}/{repo}/branch_protections/{name}`.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection;
|
|
5
5
|
* read/update/delete use the branch/rule name in the path segment `{name}`. Response carries
|
|
6
6
|
* `rule_name` (preferred) and deprecated `branch_name`.
|
|
7
7
|
*
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/forgejo/branch-protection.ts", "../src/forgejo/branch-protection-form.ts", "../src/forgejo/resource.ts", "../src/forgejo/client.ts", "../src/forgejo/values.ts", "../src/forgejo/org-actions-secrets.ts", "../src/forgejo/org-label.ts", "../src/forgejo/org-team.ts", "../src/forgejo/repo-webhook.ts", "../src/forgejo/repo-webhook-form.ts", "../src/forgejo/repository.ts", "../src/forgejo/team-member.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `Forgejo.BranchProtection` — one rule at `/repos/{owner}/{repo}/branch_protections/{name}`.\n *\n * ★ READ OFF
|
|
6
|
-
"/**\n * Create/PATCH bodies for `Forgejo.BranchProtection` — extracted for the 250-line cap.\n *\n * ★ READ OFF
|
|
7
|
-
"/**\n * One shape for every Forgejo object — the same four operations, different path and JSON body.\n *\n * ⛔ THE ALTERNATIVE IS COPYING THE SAME HANDLERS PER RESOURCE. Repositories, org labels, teams,\n * runners, branch protection and package settings are all create / read / update / delete over\n * `/api/v1` with JSON bodies. Hand-rolling the fourth copy is where someone drops the read-back\n * or the isResolved guard and the plan lies.\n *\n * ★ SO THE FOUR OPERATIONS LIVE HERE, ONCE. A new Forgejo object should be a spec plus ~forty\n * lines, not a hundred and thirty.\n *\n * ⚠️ GITEA ANSWERS WITH THE OBJECT DIRECTLY — NOT `{\"data\": ...}`. See client.ts. reconcile still\n * READS BACK after every write rather than trusting the status code alone.\n *\n * ★ WHY `Forgejo.Repository` DECLARES `defaultRemovalPolicy: 'retain'`, WRITTEN ONCE HERE.\n * Alchemy's own `GitHub.Repository` defaults to retain \"because their contents are\n * irreplaceable\". A git repository is the most irreplaceable object on this estate. `retain`\n * means the engine skips `provider.delete` when a resource is orphaned unless the caller opted\n * into `.pipe(RemovalPolicy.destroy())`. The `delete` handler is FULLY IMPLEMENTED anyway — a\n * stub that silently does nothing lies to whoever reads the plan.\n */\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { ForgejoError, forgejo } from './client.ts';\n\n/**\n * ★ WHAT EVERY OPERATION HERE NEEDS FROM THE RUNTIME — and it is NOT what the first draft said.\n *\n * These resources declared `ChildProcessSpawner`, copied from
|
|
8
|
-
"/**\n * One authenticated call to the Forgejo (Gitea) API at `/api/v1`.\n *\n * ★ PLAIN `fetch`, NO TLS ESCAPE HATCH — your Forgejo host serves a real certificate on :443,\n * and the local daemon on :3000 is loopback. There is no `rejectUnauthorized` switch to leave\n * on by accident elsewhere.\n *\n * ⚠️ GITEA DOES NOT WRAP ANSWERS IN `{\"data\": ...}` THE WAY PVE DOES. MEASURED 2026-09-13 on\n * the live daemon: `GET /api/v1/version` returns `{\"version\":\"16.0.3\"}`; `GET /api/v1/user`\n * returns the user object; `GET /api/v1/orgs/HomeFlare/repos` returns a JSON array. A caller\n * that unwraps a non-existent envelope sees `undefined` and calls a healthy object \"absent\".\n *\n * ⛔ THE TOKEN IS NEVER A PROP. Read `FORGEJO_TOKEN` from the environment at call time only.\n * Alchemy persists attributes unencrypted and this estate's state store is Postgres dumped\n * nightly — nothing stored may be a credential.\n */\nimport * as Effect from 'effect/Effect';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';\n\nconst PATH_PREFIX = '/api/v1';\n\n/**\n * The Forgejo instance, from `FORGEJO_URL`. Trailing slash stripped.\n *\n * ⛔ NO DEFAULT HOSTNAME. This used to fall back to the author's own instance, which is\n * fine inside one estate and wrong in a published package: a consumer who forgets the\n * variable would silently point at someone else's server rather than being told.\n * ⚠️ Read at CALL time, never at module scope — a value captured at import is pinned to\n * whatever the first isolate saw.\n */\nexport const apiBase = (): string => {\n const raw = process.env['FORGEJO_URL'];\n if (raw === undefined || raw.trim() === '') {\n throw new Error(\n 'FORGEJO_URL is not set. Point it at your Forgejo instance, e.g. https://git.example.com',\n );\n }\n return raw.trim().replace(/\\/$/, '');\n};\n\nconst token = (): Effect.Effect<string, ForgejoError> =>\n Effect.gen(function* () {\n const raw = process.env['FORGEJO_TOKEN'];\n if (raw === undefined || raw.trim() === '') {\n return yield* Effect.fail(\n new ForgejoError(\n 0,\n 'GET',\n '',\n 'FORGEJO_TOKEN is unset. Mint a scoped token and export it — never store it in Alchemy props.',\n ),\n );\n }\n return raw.trim();\n });\n\nexport class ForgejoError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n detail: string,\n ) {\n super(`Forgejo ${method} ${path} -> ${String(status)}: ${detail}`);\n this.name = 'ForgejoError';\n }\n}\n\n/**\n * ⚠️ `Authorization: token <value>`, NOT `Bearer`. Gitea docs and MEASURED behaviour on this\n * estate: Bearer is treated as OAuth2 and rejected; `token` is the documented API-key form.\n */\nconst authorization = (value: string) => ({ Authorization: `token ${value}` });\n\n/** One JSON call. Returns the parsed body, or `undefined` when the response is empty (DELETE). */\nexport const forgejo = <T>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: Record<string, unknown>,\n) =>\n Effect.gen(function* () {\n const credential = yield* token();\n const url = `${apiBase()}${PATH_PREFIX}/${path.replace(/^\\//, '')}`;\n /**\n * ★ THROUGH `HttpClient`, NOT THE GLOBAL `fetch` — the idiom Alchemy's own providers follow\n * (src/Hetzner/Providers.ts builds on `FetchHttpClient.layer`; there is no bare fetch in its\n * Docker, Kubernetes or GitHub providers). It buys a typed error channel, interruption when\n * a plan is cancelled, the runtime's tracing, and a client a test can replace without\n * monkey-patching a global.
|
|
5
|
+
"/**\n * `Forgejo.BranchProtection` — one rule at `/repos/{owner}/{repo}/branch_protections/{name}`.\n *\n * ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection;\n * read/update/delete use the branch/rule name in the path segment `{name}`. Response carries\n * `rule_name` (preferred) and deprecated `branch_name`.\n *\n * ⛔ `defaultRemovalPolicy: 'retain'` — dropping a protection rule from the stack must not silently\n * unprotect a branch; opt into `.pipe(RemovalPolicy.destroy())` to DELETE.\n *\n * ⛔ TOKEN NEEDS `write:repository` TO CREATE OR PATCH — get needs `read:repository`.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { branchProtectionForm } from './branch-protection-form.ts';\nimport { type ForgejoRequirements, forgejoHandlers } from './resource.ts';\nimport { bool, int, stringArray, text } from './values.ts';\n\nexport interface BranchProtectionProps {\n owner: string;\n repo: string;\n /** Branch or rule name — path key and `rule_name` on the wire. */\n branchName: string;\n requiredApprovals?: number;\n enablePush?: boolean;\n enableStatusCheck?: boolean;\n requireSignedCommits?: boolean;\n dismissStaleApprovals?: boolean;\n ignoreStaleApprovals?: boolean;\n applyToAdmins?: boolean;\n statusCheckContexts?: string[];\n}\n\nexport interface BranchProtectionAttributes {\n owner: string;\n repo: string;\n branchName: string;\n requiredApprovals: number;\n enablePush: boolean;\n enableStatusCheck: boolean;\n requireSignedCommits: boolean;\n dismissStaleApprovals: boolean;\n ignoreStaleApprovals: boolean;\n applyToAdmins: boolean;\n statusCheckContexts: string[];\n}\n\nexport interface ForgejoBranchProtection extends Resource<\n 'Forgejo.BranchProtection',\n BranchProtectionProps,\n BranchProtectionAttributes,\n never,\n ForgejoRequirements\n> {}\n\nexport const ForgejoBranchProtection = Resource<ForgejoBranchProtection>(\n 'Forgejo.BranchProtection',\n { defaultRemovalPolicy: 'retain' },\n);\n\nconst handlers = forgejoHandlers<BranchProtectionProps, BranchProtectionAttributes>({\n attributes: (live, props) => {\n const ruleName = text(live['rule_name'], text(live['branch_name']));\n if (ruleName === '') return undefined;\n return {\n applyToAdmins: bool(live['apply_to_admins']),\n branchName: props.branchName,\n dismissStaleApprovals: bool(live['dismiss_stale_approvals']),\n enablePush: bool(live['enable_push'], true),\n enableStatusCheck: bool(live['enable_status_check']),\n ignoreStaleApprovals: bool(live['ignore_stale_approvals']),\n owner: props.owner,\n repo: props.repo,\n requireSignedCommits: bool(live['require_signed_commits']),\n requiredApprovals: int(live['required_approvals']),\n statusCheckContexts: stringArray(live['status_check_contexts']),\n };\n },\n collection: (props) => `repos/${props.owner}/${props.repo}/branch_protections`,\n createForm: branchProtectionForm,\n matches: (attributes, props) =>\n attributes.requiredApprovals === (props.requiredApprovals ?? 0) &&\n attributes.enablePush === (props.enablePush ?? true) &&\n attributes.enableStatusCheck === (props.enableStatusCheck ?? false) &&\n attributes.requireSignedCommits === (props.requireSignedCommits ?? false) &&\n attributes.dismissStaleApprovals === (props.dismissStaleApprovals ?? false) &&\n attributes.ignoreStaleApprovals === (props.ignoreStaleApprovals ?? false) &&\n attributes.applyToAdmins === (props.applyToAdmins ?? false) &&\n (props.statusCheckContexts === undefined ||\n stringArray(props.statusCheckContexts).join('\\0') ===\n attributes.statusCheckContexts.join('\\0')),\n path: (props) => `repos/${props.owner}/${props.repo}/branch_protections/${props.branchName}`,\n updateForm: branchProtectionForm,\n});\n\nexport const ForgejoBranchProtectionProvider = () =>\n Provider.effect(\n ForgejoBranchProtection,\n Effect.succeed(ForgejoBranchProtection.Provider.of(handlers)),\n );\n",
|
|
6
|
+
"/**\n * Create/PATCH bodies for `Forgejo.BranchProtection` — extracted for the 250-line cap.\n *\n * ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json` — create posts\n * `CreateBranchProtectionOption`; update sends `EditBranchProtectionOption` fields only.\n */\nimport type { BranchProtectionProps } from './branch-protection.ts';\n\nexport const branchProtectionForm = (props: BranchProtectionProps) => ({\n ...(props.applyToAdmins === undefined ? {} : { apply_to_admins: props.applyToAdmins }),\n ...(props.dismissStaleApprovals === undefined\n ? {}\n : { dismiss_stale_approvals: props.dismissStaleApprovals }),\n ...(props.enablePush === undefined ? {} : { enable_push: props.enablePush }),\n ...(props.enableStatusCheck === undefined\n ? {}\n : { enable_status_check: props.enableStatusCheck }),\n ...(props.ignoreStaleApprovals === undefined\n ? {}\n : { ignore_stale_approvals: props.ignoreStaleApprovals }),\n ...(props.requireSignedCommits === undefined\n ? {}\n : { require_signed_commits: props.requireSignedCommits }),\n ...(props.requiredApprovals === undefined ? {} : { required_approvals: props.requiredApprovals }),\n ...(props.statusCheckContexts === undefined\n ? {}\n : { status_check_contexts: props.statusCheckContexts }),\n rule_name: props.branchName,\n});\n",
|
|
7
|
+
"/**\n * One shape for every Forgejo object — the same four operations, different path and JSON body.\n *\n * ⛔ THE ALTERNATIVE IS COPYING THE SAME HANDLERS PER RESOURCE. Repositories, org labels, teams,\n * runners, branch protection and package settings are all create / read / update / delete over\n * `/api/v1` with JSON bodies. Hand-rolling the fourth copy is where someone drops the read-back\n * or the isResolved guard and the plan lies.\n *\n * ★ SO THE FOUR OPERATIONS LIVE HERE, ONCE. A new Forgejo object should be a spec plus ~forty\n * lines, not a hundred and thirty.\n *\n * ⚠️ GITEA ANSWERS WITH THE OBJECT DIRECTLY — NOT `{\"data\": ...}`. See client.ts. reconcile still\n * READS BACK after every write rather than trusting the status code alone.\n *\n * ★ WHY `Forgejo.Repository` DECLARES `defaultRemovalPolicy: 'retain'`, WRITTEN ONCE HERE.\n * Alchemy's own `GitHub.Repository` defaults to retain \"because their contents are\n * irreplaceable\". A git repository is the most irreplaceable object on this estate. `retain`\n * means the engine skips `provider.delete` when a resource is orphaned unless the caller opted\n * into `.pipe(RemovalPolicy.destroy())`. The `delete` handler is FULLY IMPLEMENTED anyway — a\n * stub that silently does nothing lies to whoever reads the plan.\n */\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { ForgejoError, forgejo } from './client.ts';\n\n/**\n * ★ WHAT EVERY OPERATION HERE NEEDS FROM THE RUNTIME — and it is NOT what the first draft said.\n *\n * These resources declared `ChildProcessSpawner`, copied from <estate>/proxmox, where it is there\n * because a PVE credential is minted by shelling out to `bao`. Nothing in this package shells\n * out: the token comes from `FORGEJO_TOKEN` in the environment. Declaring a service you never\n * use is not harmless — it makes a stack provide a layer for nothing, and it hides the one\n * service you DO need.\n *\n * What is actually required is `HttpClient`, for the reason in client.ts: Alchemy's own\n * providers call through Effect's client rather than the global `fetch`.\n */\nexport type ForgejoRequirements = HttpClient.HttpClient;\n\nexport type ForgejoSpec<Props extends object, Attributes> = {\n /** `repos/HomeFlare/homeflare`, `orgs/homeflare/labels/40` — one object. */\n readonly path: (props: Props) => string;\n /** `orgs/HomeFlare/repos`, `orgs/homeflare/labels` — where a new one is POSTed. */\n readonly collection: (props: Props) => string;\n readonly attributes: (live: Record<string, unknown>, props: Props) => Attributes | undefined;\n /** JSON body on create. ⚠️ Gitea takes JSON, not form encoding. */\n readonly createForm: (props: Props) => Record<string, unknown>;\n readonly updateForm?: (props: Props) => Record<string, unknown>;\n readonly matches: (attributes: Attributes, props: Props) => boolean;\n /**\n * When the stable key is not enough for the wire path — org labels are addressed by numeric id.\n * `locate` lists or searches; `wirePath` builds update/delete paths from the live row.\n */\n readonly locate?: (\n props: Props,\n ) => Effect.Effect<Record<string, unknown> | undefined, ForgejoError, ForgejoRequirements>;\n readonly wirePath?: (props: Props, live: Record<string, unknown>) => string;\n /**\n * When create is PUT upsert rather than POST — org actions secrets have no POST and no GET-by-name.\n * `before === undefined` means absent; the callback decides whether to write. Existence-only families\n * noop when `before` is set because the API never returns the value for comparison.\n */\n readonly upsert?: (\n props: Props,\n before: Record<string, unknown> | undefined,\n ) => Effect.Effect<void, ForgejoError, ForgejoRequirements>;\n};\n\n/** ⚠️ Only 404 folds to \"absent\". A 403 is a real failure — folding it would plan a create over an existing object. */\nconst absentOn404 = <A>(io: Effect.Effect<A, ForgejoError, ForgejoRequirements>) =>\n io.pipe(\n Effect.catchIf(\n (cause): cause is ForgejoError => cause instanceof ForgejoError && cause.status === 404,\n () => Effect.succeed(undefined as A),\n ),\n );\n\nexport const forgejoOperations = <Props extends object, Attributes>(\n spec: ForgejoSpec<Props, Attributes>,\n) => {\n const fetchLive = (props: Props) =>\n absentOn404(\n spec.locate !== undefined\n ? spec.locate(props)\n : forgejo<Record<string, unknown>>('GET', spec.path(props)),\n );\n\n const objectPath = (props: Props, live: Record<string, unknown>) =>\n spec.wirePath === undefined ? spec.path(props) : spec.wirePath(props, live);\n\n const read = (props: Props) =>\n fetchLive(props).pipe(\n Effect.map((data) => (data === undefined ? undefined : spec.attributes(data, props))),\n );\n\n return {\n read,\n\n diff: (news: Input<Props>, output: Attributes | undefined) =>\n Effect.gen(function* () {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* read(news);\n if (live === undefined) return { action: 'update' } as const;\n if (spec.matches(live, news)) return { action: 'noop' } as const;\n return spec.updateForm === undefined\n ? ({ action: 'replace' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: (news: Props) =>\n Effect.gen(function* () {\n const before = yield* fetchLive(news);\n if (spec.upsert !== undefined) {\n yield* spec.upsert(news, before);\n } else if (before === undefined) {\n yield* forgejo('POST', spec.collection(news), spec.createForm(news));\n } else if (spec.updateForm !== undefined) {\n const mapped = spec.attributes(before, news);\n if (mapped === undefined || spec.matches(mapped, news)) {\n /* adoption or already converged — no PATCH */\n } else {\n const form = spec.updateForm(news);\n if (Object.keys(form).length > 0) {\n yield* forgejo('PATCH', objectPath(news, before), form);\n }\n }\n }\n const after = yield* read(news);\n if (after === undefined) {\n return yield* Effect.die(\n new Error(\n `${spec.path(news)}: the write returned no error but the object is still absent. ` +\n 'Gitea returns the object directly — read back rather than trusting the status code.',\n ),\n );\n }\n return after;\n }),\n\n destroy: (olds: Props) =>\n Effect.gen(function* () {\n const live = yield* fetchLive(olds);\n if (live === undefined) return;\n yield* forgejo('DELETE', objectPath(olds, live));\n }),\n };\n};\n\n/**\n * The five provider handlers for a spec'd Forgejo object, wired once.\n *\n * ⛔ IT STOPS AT THE HANDLERS AND DOES NOT RETURN THE LAYER — same reasoning as pveHandlers in\n * <estate>/proxmox: wrapping `Provider.effect` here needs casts against Alchemy's `Props<R>`.\n *\n * ⚠️ `list` answers empty — Forgejo index endpoints return the whole org; adoption stays explicit.\n */\nexport const forgejoHandlers = <Props extends object, Attributes extends object>(\n spec: ForgejoSpec<Props, Attributes>,\n) => {\n const ops = forgejoOperations(spec);\n return {\n list: () => Effect.succeed([]),\n read: ({ olds }: { olds: Props }) => ops.read(olds),\n diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>\n ops.diff(news, output),\n reconcile: ({ news }: { news: Props }) => ops.reconcile(news),\n delete: ({ olds }: { olds: Props }) => ops.destroy(olds),\n };\n};\n",
|
|
8
|
+
"/**\n * One authenticated call to the Forgejo (Gitea) API at `/api/v1`.\n *\n * ★ PLAIN `fetch`, NO TLS ESCAPE HATCH — your Forgejo host serves a real certificate on :443,\n * and the local daemon on :3000 is loopback. There is no `rejectUnauthorized` switch to leave\n * on by accident elsewhere.\n *\n * ⚠️ GITEA DOES NOT WRAP ANSWERS IN `{\"data\": ...}` THE WAY PVE DOES. MEASURED 2026-09-13 on\n * the live daemon: `GET /api/v1/version` returns `{\"version\":\"16.0.3\"}`; `GET /api/v1/user`\n * returns the user object; `GET /api/v1/orgs/HomeFlare/repos` returns a JSON array. A caller\n * that unwraps a non-existent envelope sees `undefined` and calls a healthy object \"absent\".\n *\n * ⛔ THE TOKEN IS NEVER A PROP. Read `FORGEJO_TOKEN` from the environment at call time only.\n * Alchemy persists attributes unencrypted and this estate's state store is Postgres dumped\n * nightly — nothing stored may be a credential.\n */\nimport * as Effect from 'effect/Effect';\nimport * as HttpClient from 'effect/unstable/http/HttpClient';\nimport * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';\n\nconst PATH_PREFIX = '/api/v1';\n\n/**\n * The Forgejo instance, from `FORGEJO_URL`. Trailing slash stripped.\n *\n * ⛔ NO DEFAULT HOSTNAME. This used to fall back to the author's own instance, which is\n * fine inside one estate and wrong in a published package: a consumer who forgets the\n * variable would silently point at someone else's server rather than being told.\n * ⚠️ Read at CALL time, never at module scope — a value captured at import is pinned to\n * whatever the first isolate saw.\n */\nexport const apiBase = (): string => {\n const raw = process.env['FORGEJO_URL'];\n if (raw === undefined || raw.trim() === '') {\n throw new Error(\n 'FORGEJO_URL is not set. Point it at your Forgejo instance, e.g. https://git.example.com',\n );\n }\n return raw.trim().replace(/\\/$/, '');\n};\n\nconst token = (): Effect.Effect<string, ForgejoError> =>\n Effect.gen(function* () {\n const raw = process.env['FORGEJO_TOKEN'];\n if (raw === undefined || raw.trim() === '') {\n return yield* Effect.fail(\n new ForgejoError(\n 0,\n 'GET',\n '',\n 'FORGEJO_TOKEN is unset. Mint a scoped token and export it — never store it in Alchemy props.',\n ),\n );\n }\n return raw.trim();\n });\n\nexport class ForgejoError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n detail: string,\n ) {\n super(`Forgejo ${method} ${path} -> ${String(status)}: ${detail}`);\n this.name = 'ForgejoError';\n }\n}\n\n/**\n * ⚠️ `Authorization: token <value>`, NOT `Bearer`. Gitea docs and MEASURED behaviour on this\n * estate: Bearer is treated as OAuth2 and rejected; `token` is the documented API-key form.\n */\nconst authorization = (value: string) => ({ Authorization: `token ${value}` });\n\n/** One JSON call. Returns the parsed body, or `undefined` when the response is empty (DELETE). */\nexport const forgejo = <T>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: Record<string, unknown>,\n) =>\n Effect.gen(function* () {\n const credential = yield* token();\n const url = `${apiBase()}${PATH_PREFIX}/${path.replace(/^\\//, '')}`;\n /**\n * ★ THROUGH `HttpClient`, NOT THE GLOBAL `fetch` — the idiom Alchemy's own providers follow\n * (src/Hetzner/Providers.ts builds on `FetchHttpClient.layer`; there is no bare fetch in its\n * Docker, Kubernetes or GitHub providers). It buys a typed error channel, interruption when\n * a plan is cancelled, the runtime's tracing, and a client a test can replace without\n * monkey-patching a global. <estate>/proxmox/src/resource.ts carries the same note.\n */\n const client = yield* HttpClient.HttpClient;\n /**\n * ⛔ THE CONTENT TYPE GOES ON `bodyText`, NOT IN THE HEADER MAP, AND PUTTING IT IN BOTH IS HOW\n * THIS PACKAGE SHIPPED UNABLE TO WRITE AT ALL.\n *\n * 🔴 THE BUG, MEASURED THE FIRST TIME A WRITE WAS EVER ATTEMPTED. The header map DID carry\n * `Content-Type: application/json` — and `bodyText(s)` with no second argument sets\n * `text/plain` and OVERWRITES it, because it is piped AFTER `setHeaders`. Forgejo answered\n * 422 {\"message\":\"[]: Unsupported Content-Type\"}\n * on `PATCH orgs/homeflare/labels/1`. Every create and every update in every family here was\n * broken, identically, from the day the client was written.\n *\n * ⛔ NOTHING CAUGHT IT BECAUSE NOTHING HAD EVER WRITTEN. The only credential on the shelf was\n * `mcp-read`, which answers 403 before a request body is ever parsed — so read, adopt, diff\n * and plan all worked perfectly and the write path had literally never executed. A provider\n * that has never performed its own writes is not a tested provider, whatever the plan says.\n *\n * ⚠️ `bodyJsonUnsafe` WOULD ALSO WORK. `bodyText` with an explicit type is used instead\n * because it makes the content type visible at the call site, which is the thing that was\n * invisible before.\n */\n const request = HttpClientRequest.make(method)(url).pipe(\n HttpClientRequest.setHeaders({ ...authorization(credential), Accept: 'application/json' }),\n body === undefined\n ? (self) => self\n : HttpClientRequest.bodyText(JSON.stringify(body), 'application/json'),\n );\n const response = yield* client\n .execute(request)\n .pipe(Effect.mapError((cause) => new ForgejoError(0, method, path, String(cause))));\n // ⛔ Status first, body once — a Gitea error body is JSON but a proxy's 502 is not.\n if (response.status < 200 || response.status >= 300) {\n const detail = yield* response.text.pipe(Effect.orElseSucceed(() => ''));\n return yield* Effect.fail(\n new ForgejoError(response.status, method, path, detail.slice(0, 300)),\n );\n }\n // ⚠️ 204 AND AN EMPTY BODY ARE REAL: a label DELETE answers no content, and `json` on an empty\n // body fails. Read text once and parse only when there is something to parse.\n const text = yield* response.text.pipe(\n Effect.mapError((cause) => new ForgejoError(0, method, path, String(cause))),\n );\n // ⚠️ PUT/DELETE on actions secrets and hooks often answer 201/204 with no body — same trap as DELETE.\n return (text === '' ? undefined : JSON.parse(text)) as T;\n });\n",
|
|
9
9
|
"/**\n * Wire coercions shared across Forgejo resources.\n *\n * ★ GITEA/ FORGEJO JSON USES snake_case ON THE WIRE and camelCase in props — each resource\n * translates at the form boundary; these helpers only normalise values for comparison.\n */\n\nexport const text = (value: unknown, fallback = '') =>\n typeof value === 'string' ? value : fallback;\n\n/** Forgejo returns label colours without `#`; declarations may include it. */\nexport const color = (value: unknown) => text(value).replace(/^#/, '').toLowerCase();\n\nexport const bool = (value: unknown, fallback = false) =>\n value === undefined || value === null ? fallback : value === true || value === 1 || value === '1';\n\nexport const int = (value: unknown, fallback = 0) => {\n if (typeof value === 'number' && Number.isFinite(value)) return Math.trunc(value);\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number.parseInt(value, 10);\n if (Number.isFinite(parsed)) return parsed;\n }\n return fallback;\n};\n\n/** Wire arrays compared in sorted order — Gitea reshuffles nothing but callers should not depend on order. */\nexport const stringArray = (value: unknown) =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string').toSorted()\n : [];\n\nexport const stringRecord = (value: unknown): Record<string, string> => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return {};\n const out: Record<string, string> = {};\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry === 'string') out[key] = entry;\n }\n return out;\n};\n\nexport const recordEqual = (left: Record<string, string>, right: Record<string, string>) => {\n const leftKeys = Object.keys(left).toSorted();\n const rightKeys = Object.keys(right).toSorted();\n if (leftKeys.length !== rightKeys.length) return false;\n return leftKeys.every((key, index) => rightKeys[index] === key && left[key] === right[key]);\n};\n\n/** Hook `config` without `secret` — the value must never land in Alchemy attributes. */\nexport const hookConfigPublic = (value: unknown) => {\n const config = stringRecord(value);\n const { secret: _secret, ...rest } = config;\n return rest;\n};\n\n/** Env key for a write-only org actions secret — value read at PUT time only, never stored. */\nexport const orgSecretEnvKey = (org: string, name: string) =>\n `FORGEJO_ORG_SECRET_${org}_${name}`.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();\n\n/** Env key for a write-only repo webhook HMAC secret — omitted from attributes and props. */\nexport const hookSecretEnvKey = (owner: string, repo: string, name: string) =>\n `FORGEJO_HOOK_SECRET_${owner}_${repo}_${name}`.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase();\n",
|
|
10
|
-
"/**\n * `Forgejo.OrgSecret` — org-level Actions secret metadata at `/orgs/{org}/actions/secrets/{name}`.\n *\n * ★ CHOICE: AUTHORED WITH WRITE-ONLY VALUE VIA ENV, NOT REFUSED. The OpenAPI in\n *
|
|
11
|
-
"/**\n * `Forgejo.OrgLabel` — one org-level issue label shared across every repository in the org.\n *\n * ★ RETIRES `
|
|
12
|
-
"/**\n * `Forgejo.OrgTeam` — one organization team under `/orgs/{org}/teams`.\n *\n * ★ READ OFF
|
|
13
|
-
"/**\n * `Forgejo.RepoWebhook` — one repository webhook under `/repos/{owner}/{repo}/hooks`.\n *\n * ★ READ OFF
|
|
10
|
+
"/**\n * `Forgejo.OrgSecret` — org-level Actions secret metadata at `/orgs/{org}/actions/secrets/{name}`.\n *\n * ★ CHOICE: AUTHORED WITH WRITE-ONLY VALUE VIA ENV, NOT REFUSED. The OpenAPI in\n * `<estate>/mcp-servers/docs/api/upstream/forgejo.json` shows `Secret` returns only `name` and\n * `created_at` — never the `data` field — so attributes stay metadata-only. Create/update is PUT\n * with body `{ data }`, which does not fit POST/PATCH; this family uses the `upsert` seam on\n * `ForgejoSpec` instead of hand-rolling provider handlers.\n *\n * ⛔ THE SECRET VALUE IS NEVER A PROP OR AN ATTRIBUTE. It is read from `FORGEJO_ORG_SECRET_{org}_{name}`\n * (see orgSecretEnvKey) at PUT time when the name is absent from the list. Once present, reconcile\n * noops — the API cannot confirm the value matches, and re-PUT on every deploy would rotate\n * unintentionally. Rotate by destroy-then-create or a manual PUT outside Alchemy.\n *\n * ⚠️ PUT ANSWERS 201/204 WITH NO BODY — client.ts treats empty responses as `undefined`; read-back\n * uses the list endpoint, not the PUT response.\n *\n * ⛔ TOKEN NEEDS `write:organization` TO PUT — list needs `read:organization`.\n *\n * ⛔ THE FILE NAME IS PLURAL ON PURPOSE AND MUST STAY THAT WAY. `.gitignore:198` carries\n * `*-secret.*` — the rule that stops `client-secret.json` and friends being committed — and it\n * matches a SOURCE file called `org-secret.ts` just as happily. This family was first authored\n * under that name: it existed on disk, typechecked, passed oxlint, was imported by alchemy.run.ts\n * and appeared in NO `git status`. It would have been committed as a broken import for everyone\n * else, with the local tree green the whole time. `scripts/husky/ignored-imports.ts` is the gate\n * that now catches the class; this comment is for whoever is tempted to rename it back.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { ForgejoError, forgejo } from './client.ts';\nimport { type ForgejoRequirements, forgejoHandlers } from './resource.ts';\nimport { orgSecretEnvKey, text } from './values.ts';\n\nexport interface OrgSecretProps {\n org: string;\n /** Secret name — path key; value lives in env, not here. */\n name: string;\n}\n\nexport interface OrgSecretAttributes {\n org: string;\n name: string;\n createdAt: string;\n}\n\nexport interface ForgejoOrgSecret extends Resource<\n 'Forgejo.OrgSecret',\n OrgSecretProps,\n OrgSecretAttributes,\n never,\n ForgejoRequirements\n> {}\n\nexport const ForgejoOrgSecret = Resource<ForgejoOrgSecret>('Forgejo.OrgSecret');\n\nconst secretData = (org: string, name: string) =>\n Effect.gen(function* () {\n const key = orgSecretEnvKey(org, name);\n const raw = process.env[key];\n if (raw === undefined || raw.trim() === '') {\n return yield* Effect.fail(\n new ForgejoError(\n 0,\n 'PUT',\n `orgs/${org}/actions/secrets/${name}`,\n `${key} is unset. Export the secret value there — never as an Alchemy prop.`,\n ),\n );\n }\n return raw.trim();\n });\n\nconst handlers = forgejoHandlers<OrgSecretProps, OrgSecretAttributes>({\n attributes: (live, props) => ({\n createdAt: text(live['created_at']),\n name: props.name,\n org: props.org,\n }),\n collection: (props) => `orgs/${props.org}/actions/secrets/${props.name}`,\n createForm: () => ({}),\n locate: (props) =>\n forgejo<Record<string, unknown>[]>('GET', `orgs/${props.org}/actions/secrets?limit=200`).pipe(\n Effect.map((rows) =>\n Array.isArray(rows) ? rows.find((row) => row['name'] === props.name) : undefined,\n ),\n ),\n matches: () => true,\n path: (props) => `orgs/${props.org}/actions/secrets/${props.name}`,\n upsert: (props, before) =>\n Effect.gen(function* () {\n if (before !== undefined) return;\n const data = yield* secretData(props.org, props.name);\n yield* forgejo('PUT', `orgs/${props.org}/actions/secrets/${props.name}`, { data });\n }),\n});\n\nexport const ForgejoOrgSecretProvider = () =>\n Provider.effect(ForgejoOrgSecret, Effect.succeed(ForgejoOrgSecret.Provider.of(handlers)));\n",
|
|
11
|
+
"/**\n * `Forgejo.OrgLabel` — one org-level issue label shared across every repository in the org.\n *\n * ★ RETIRES the estate's hand-run `labels-reconcile.sh`, which\n * converged the whole `labels.yaml` file in one shot. That script CREATED and PATCHED labels\n * from the spec and REPORTED `EXTRA` labels present live but absent from the file — it never\n * deleted them, because removing a label strips it from every issue that carries it.\n *\n * ★ THIS RESOURCE DOES NOT REMOVE EXTRA LABELS EITHER. Alchemy manages only labels you declare;\n * labels on the org that nobody declared stay untouched — the same boundary as the script's\n * `EXTRA … NOT deleted` lines. Removing a declaration defaults to `retain`, so dropping a label\n * from the stack does not DELETE it unless the caller opts into `.pipe(RemovalPolicy.destroy())`.\n * That matches the script's never-delete posture while still allowing a deliberate destroy.\n *\n * ⚠️ THE WIRE PATH USES NUMERIC ID, NOT NAME — measured on live org labels: PATCH and DELETE are\n * `/orgs/{org}/labels/{id}`. `locate` lists by name; `wirePath` uses the id from that row.\n *\n * ⛔ TOKEN NEEDS `write:organization` FOR RECONCILE — the mcp-read token lacks it (POST 403\n * measured 2026-09-13). Plan/deploy needs a provision-scoped credential in `FORGEJO_TOKEN`.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { forgejo } from './client.ts';\nimport { type ForgejoRequirements, forgejoHandlers } from './resource.ts';\nimport { bool, color, text } from './values.ts';\n\nexport interface OrgLabelProps {\n /** Organization login — `homeflare` / `HomeFlare` must match the live org. */\n org: string;\n /** Label name — primary key for locate; immutable on update. */\n name: string;\n /** Six-digit hex, with or without `#`. */\n color: string;\n description?: string;\n exclusive?: boolean;\n}\n\nexport interface OrgLabelAttributes {\n org: string;\n name: string;\n labelId: number;\n color: string;\n description: string;\n exclusive: boolean;\n}\n\nexport interface ForgejoOrgLabel extends Resource<\n 'Forgejo.OrgLabel',\n OrgLabelProps,\n OrgLabelAttributes,\n never,\n ForgejoRequirements\n> {}\n\nexport const ForgejoOrgLabel = Resource<ForgejoOrgLabel>('Forgejo.OrgLabel', {\n defaultRemovalPolicy: 'retain',\n});\n\nconst handlers = forgejoHandlers<OrgLabelProps, OrgLabelAttributes>({\n attributes: (live, props) => {\n const id = live['id'];\n if (typeof id !== 'number') return undefined;\n return {\n color: color(live['color']),\n description: text(live['description']),\n exclusive: bool(live['exclusive']),\n labelId: id,\n name: props.name,\n org: props.org,\n };\n },\n collection: (props) => `orgs/${props.org}/labels`,\n createForm: (props) => ({\n color: color(props.color),\n description: props.description ?? '',\n exclusive: props.exclusive ?? false,\n name: props.name,\n }),\n locate: (props) =>\n forgejo<Record<string, unknown>[]>('GET', `orgs/${props.org}/labels?limit=200`).pipe(\n Effect.map((rows) =>\n Array.isArray(rows) ? rows.find((row) => row['name'] === props.name) : undefined,\n ),\n ),\n matches: (attributes, props) =>\n attributes.color === color(props.color) &&\n attributes.description === (props.description ?? '') &&\n attributes.exclusive === (props.exclusive ?? false),\n path: (props) => `orgs/${props.org}/labels/${props.name}`,\n updateForm: (props) => ({\n color: color(props.color),\n description: props.description ?? '',\n exclusive: props.exclusive ?? false,\n }),\n wirePath: (props, live) => `orgs/${props.org}/labels/${String(live['id'])}`,\n});\n\nexport const ForgejoOrgLabelProvider = () =>\n Provider.effect(ForgejoOrgLabel, Effect.succeed(ForgejoOrgLabel.Provider.of(handlers)));\n",
|
|
12
|
+
"/**\n * `Forgejo.OrgTeam` — one organization team under `/orgs/{org}/teams`.\n *\n * ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json` (Forgejo 16.x OpenAPI): create is\n * `POST /orgs/{org}/teams`; read/update/delete use numeric id at `/teams/{id}` — not the name.\n *\n * ⚠️ PATCH AND DELETE USE `/teams/{id}` — same seam as org labels. `locate` lists by name;\n * `wirePath` carries the id from that row.\n *\n * ⛔ TOKEN NEEDS `write:organization` TO CREATE OR PATCH — read list/get needs `read:organization`.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { forgejo } from './client.ts';\nimport { type ForgejoRequirements, forgejoHandlers } from './resource.ts';\nimport { bool, recordEqual, stringArray, stringRecord, text } from './values.ts';\n\nexport type TeamPermission = 'read' | 'write' | 'admin';\n\nexport interface OrgTeamProps {\n org: string;\n /** Team name — primary locate key; sent on every PATCH body. */\n name: string;\n description?: string;\n permission?: TeamPermission;\n units?: string[];\n unitsMap?: Record<string, string>;\n includesAllRepositories?: boolean;\n canCreateOrgRepo?: boolean;\n}\n\nexport interface OrgTeamAttributes {\n org: string;\n name: string;\n teamId: number;\n description: string;\n permission: TeamPermission;\n units: string[];\n unitsMap: Record<string, string>;\n includesAllRepositories: boolean;\n canCreateOrgRepo: boolean;\n}\n\nexport interface ForgejoOrgTeam extends Resource<\n 'Forgejo.OrgTeam',\n OrgTeamProps,\n OrgTeamAttributes,\n never,\n ForgejoRequirements\n> {}\n\nexport const ForgejoOrgTeam = Resource<ForgejoOrgTeam>('Forgejo.OrgTeam');\n\nconst teamForm = (props: OrgTeamProps) => ({\n can_create_org_repo: props.canCreateOrgRepo ?? false,\n description: props.description ?? '',\n includes_all_repositories: props.includesAllRepositories ?? false,\n name: props.name,\n ...(props.permission === undefined ? {} : { permission: props.permission }),\n ...(props.units === undefined ? {} : { units: props.units }),\n ...(props.unitsMap === undefined ? {} : { units_map: props.unitsMap }),\n});\n\nconst handlers = forgejoHandlers<OrgTeamProps, OrgTeamAttributes>({\n attributes: (live, props) => {\n const id = live['id'];\n if (typeof id !== 'number') return undefined;\n const permission = text(live['permission'], 'read');\n if (permission !== 'read' && permission !== 'write' && permission !== 'admin') return undefined;\n return {\n canCreateOrgRepo: bool(live['can_create_org_repo']),\n description: text(live['description']),\n includesAllRepositories: bool(live['includes_all_repositories']),\n name: props.name,\n org: props.org,\n permission,\n teamId: id,\n units: stringArray(live['units']),\n unitsMap: stringRecord(live['units_map']),\n };\n },\n collection: (props) => `orgs/${props.org}/teams`,\n createForm: teamForm,\n locate: (props) =>\n forgejo<Record<string, unknown>[]>('GET', `orgs/${props.org}/teams?limit=200`).pipe(\n Effect.map((rows) =>\n Array.isArray(rows) ? rows.find((row) => row['name'] === props.name) : undefined,\n ),\n ),\n matches: (attributes, props) =>\n attributes.description === (props.description ?? '') &&\n attributes.permission === (props.permission ?? 'read') &&\n attributes.includesAllRepositories === (props.includesAllRepositories ?? false) &&\n attributes.canCreateOrgRepo === (props.canCreateOrgRepo ?? false) &&\n (props.units === undefined ||\n stringArray(props.units).join('\\0') === attributes.units.join('\\0')) &&\n (props.unitsMap === undefined || recordEqual(attributes.unitsMap, props.unitsMap)),\n path: (props) => `orgs/${props.org}/teams/${props.name}`,\n updateForm: teamForm,\n wirePath: (_props, live) => `teams/${String(live['id'])}`,\n});\n\nexport const ForgejoOrgTeamProvider = () =>\n Provider.effect(ForgejoOrgTeam, Effect.succeed(ForgejoOrgTeam.Provider.of(handlers)));\n",
|
|
13
|
+
"/**\n * `Forgejo.RepoWebhook` — one repository webhook under `/repos/{owner}/{repo}/hooks`.\n *\n * ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection; read/update/delete\n * use numeric hook id at `/repos/{owner}/{repo}/hooks/{id}`. `locate` lists and matches on `name`.\n *\n * ⛔ NO WEBHOOK `secret` IN PROPS OR ATTRIBUTES. Optional HMAC material is env-only at write time\n * (`FORGEJO_HOOK_SECRET_*` — see repo-webhook-form.ts). `hookConfigPublic` strips `secret` on read.\n *\n * ⛔ TOKEN NEEDS `write:repository` TO CREATE OR PATCH — list/get needs `read:repository`.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { forgejo } from './client.ts';\nimport { createHookForm, hookUrl, updateHookForm } from './repo-webhook-form.ts';\nimport { type ForgejoRequirements, forgejoHandlers } from './resource.ts';\nimport { bool, hookConfigPublic, stringArray, text } from './values.ts';\n\nexport type RepoWebhookType =\n | 'dingtalk'\n | 'discord'\n | 'feishu'\n | 'forgejo'\n | 'gitea'\n | 'gogs'\n | 'msteams'\n | 'packagist'\n | 'slack'\n | 'telegram'\n | 'wechatwork';\n\nexport interface RepoWebhookProps {\n owner: string;\n repo: string;\n /** Stable locate key — also sent as Hook.name on create. */\n name: string;\n type: RepoWebhookType;\n url: string;\n events: string[];\n contentType?: 'json' | 'form';\n active?: boolean;\n branchFilter?: string;\n authorizationHeader?: string;\n}\n\nexport interface RepoWebhookAttributes {\n owner: string;\n repo: string;\n name: string;\n hookId: number;\n type: RepoWebhookType;\n url: string;\n contentType: string;\n events: string[];\n active: boolean;\n branchFilter: string;\n authorizationHeader: string;\n}\n\nexport interface ForgejoRepoWebhook extends Resource<\n 'Forgejo.RepoWebhook',\n RepoWebhookProps,\n RepoWebhookAttributes,\n never,\n ForgejoRequirements\n> {}\n\nexport const ForgejoRepoWebhook = Resource<ForgejoRepoWebhook>('Forgejo.RepoWebhook');\n\nconst handlers = forgejoHandlers<RepoWebhookProps, RepoWebhookAttributes>({\n attributes: (live, props) => {\n const id = live['id'];\n if (typeof id !== 'number') return undefined;\n const config = hookConfigPublic(live['config']);\n const type = text(live['type']);\n return {\n active: bool(live['active'], true),\n authorizationHeader: text(live['authorization_header']),\n branchFilter: text(live['branch_filter']),\n contentType: text(config['content_type'], text(live['content_type'], 'json')),\n events: stringArray(live['events']),\n hookId: id,\n name: props.name,\n owner: props.owner,\n repo: props.repo,\n type: type as RepoWebhookType,\n url: hookUrl(live),\n };\n },\n collection: (props) => `repos/${props.owner}/${props.repo}/hooks`,\n createForm: createHookForm,\n locate: (props) =>\n forgejo<Record<string, unknown>[]>(\n 'GET',\n `repos/${props.owner}/${props.repo}/hooks?limit=200`,\n ).pipe(\n Effect.map((rows) =>\n Array.isArray(rows)\n ? rows.find((row) => text(row['name']) === props.name || hookUrl(row) === props.url)\n : undefined,\n ),\n ),\n matches: (attributes, props) =>\n attributes.type === props.type &&\n attributes.url === props.url &&\n attributes.active === (props.active ?? true) &&\n attributes.contentType === (props.contentType ?? 'json') &&\n attributes.branchFilter === (props.branchFilter ?? '') &&\n attributes.authorizationHeader === (props.authorizationHeader ?? '') &&\n stringArray(props.events).join('\\0') === attributes.events.join('\\0'),\n path: (props) => `repos/${props.owner}/${props.repo}/hooks/${props.name}`,\n updateForm: updateHookForm,\n wirePath: (props, live) => `repos/${props.owner}/${props.repo}/hooks/${String(live['id'])}`,\n});\n\nexport const ForgejoRepoWebhookProvider = () =>\n Provider.effect(ForgejoRepoWebhook, Effect.succeed(ForgejoRepoWebhook.Provider.of(handlers)));\n",
|
|
14
14
|
"/**\n * JSON bodies for `Forgejo.RepoWebhook` — split to keep repo-webhook.ts under the line cap.\n *\n * ⛔ `config.secret` IS NEVER A PROP. When present it is read from `FORGEJO_HOOK_SECRET_*` at\n * create/update time only (see hookSecretEnvKey in values.ts). Alchemy attributes are Postgres\n * plaintext — a webhook HMAC secret must not be stored there.\n */\nimport type { RepoWebhookProps } from './repo-webhook.ts';\nimport { hookSecretEnvKey, stringRecord, text } from './values.ts';\n\nexport const hookUrl = (live: Record<string, unknown>) =>\n text(live['url']) || text(stringRecord(live['config'])['url']);\n\nexport const hookConfigForm = (props: RepoWebhookProps, includeSecret: boolean) => {\n const config: Record<string, string> = {\n content_type: props.contentType ?? 'json',\n url: props.url,\n };\n if (includeSecret) {\n const key = hookSecretEnvKey(props.owner, props.repo, props.name);\n const secret = process.env[key]?.trim();\n if (secret !== undefined && secret !== '') config['secret'] = secret;\n }\n return config;\n};\n\nexport const createHookForm = (props: RepoWebhookProps) => ({\n active: props.active ?? true,\n ...(props.authorizationHeader === undefined\n ? {}\n : { authorization_header: props.authorizationHeader }),\n ...(props.branchFilter === undefined ? {} : { branch_filter: props.branchFilter }),\n config: hookConfigForm(props, true),\n events: props.events,\n name: props.name,\n type: props.type,\n});\n\nexport const updateHookForm = (props: RepoWebhookProps) => ({\n active: props.active ?? true,\n ...(props.authorizationHeader === undefined\n ? {}\n : { authorization_header: props.authorizationHeader }),\n ...(props.branchFilter === undefined ? {} : { branch_filter: props.branchFilter }),\n config: hookConfigForm(props, false),\n events: props.events,\n});\n",
|
|
15
|
-
"/**\n * `Forgejo.Repository` — an organization-owned git repository.\n *\n * ★ MEASURED 2026-09-13 against the live daemon (`GET /api/v1/repos
|
|
16
|
-
"/**\n * `Forgejo.TeamMember` — one user in one organization team.\n *\n * ★ WHY IT EXISTS: forgejo-provision, the service account the Forgejo engine mints as, belongs to the\n * HomeFlare Owners team — Tim, 2026-09-14. Membership is what its tokens can reach; the role's\n * scopes only cap it (
|
|
15
|
+
"/**\n * `Forgejo.Repository` — an organization-owned git repository.\n *\n * ★ MEASURED 2026-09-13 against the live daemon (`GET /api/v1/repos/<org>/<repo>`):\n * responses are flat JSON with `private`, `has_issues`, `default_branch`, etc. Create is\n * `POST /orgs/{org}/repos`; read/update/delete use `/repos/{owner}/{repo}`.\n *\n * ⛔ `defaultRemovalPolicy: 'retain'` — history, issues and packages in a repo are irreplaceable.\n * Opt into deletion with `.pipe(RemovalPolicy.destroy())`; `delete` below is fully implemented.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { type ForgejoRequirements, forgejoHandlers } from './resource.ts';\nimport { bool, text } from './values.ts';\n\nexport interface RepositoryProps {\n /** Organization login — the owner segment in `<org>/<repo>`. */\n org: string;\n /** Repository name. Changing it is a replace, not an update. */\n name: string;\n description?: string;\n private?: boolean;\n hasIssues?: boolean;\n hasWiki?: boolean;\n hasProjects?: boolean;\n /** Only applied when the repository already has at least one branch. */\n defaultBranch?: string;\n /** Create an initial commit with a README when the repository is new. */\n autoInit?: boolean;\n}\n\nexport interface RepositoryAttributes {\n org: string;\n name: string;\n repoId: number;\n description: string;\n private: boolean;\n hasIssues: boolean;\n hasWiki: boolean;\n hasProjects: boolean;\n defaultBranch: string;\n empty: boolean;\n}\n\nexport interface ForgejoRepository extends Resource<\n 'Forgejo.Repository',\n RepositoryProps,\n RepositoryAttributes,\n never,\n ForgejoRequirements\n> {}\n\nexport const ForgejoRepository = Resource<ForgejoRepository>('Forgejo.Repository', {\n defaultRemovalPolicy: 'retain',\n});\n\nconst handlers = forgejoHandlers<RepositoryProps, RepositoryAttributes>({\n attributes: (live, props) => {\n const id = live['id'];\n if (typeof id !== 'number') return undefined;\n return {\n defaultBranch: text(live['default_branch'], 'main'),\n description: text(live['description']),\n empty: bool(live['empty']),\n hasIssues: bool(live['has_issues'], true),\n hasProjects: bool(live['has_projects'], true),\n hasWiki: bool(live['has_wiki'], true),\n name: props.name,\n org: props.org,\n private: bool(live['private'], true),\n repoId: id,\n };\n },\n collection: (props) => `orgs/${props.org}/repos`,\n createForm: (props) => ({\n auto_init: props.autoInit ?? false,\n description: props.description ?? '',\n has_issues: props.hasIssues ?? true,\n has_projects: props.hasProjects ?? true,\n has_wiki: props.hasWiki ?? true,\n name: props.name,\n private: props.private ?? true,\n }),\n /**\n * ⛔ AN UNDECLARED FIELD IS NEITHER COMPARED NOR SENT — the rule every other family in this\n * estate follows (storage.ts, backup-job.ts, and all four Pbs.* families say it in those\n * words), and this file was the exception.\n *\n * 🔴 WHAT THE EXCEPTION DID. Five fields were compared against a DEFAULT rather than skipped:\n * `description ?? ''`, and `?? true` for private, hasIssues, hasWiki and hasProjects. So a\n * declaration that simply did not mention `hasWiki` asserted \"the wiki is ON\", and one that\n * did not mention `description` asserted \"the description is EMPTY\" — and `updateForm` sent\n * both. Declaring a repo you only wanted to rename would have wiped its description and\n * switched features on, reported as a plain `update`. Only `defaultBranch` was written\n * correctly, on the line below, which is what the other five now copy.\n *\n * ⚠️ THE PRICE, STATED: dropping a line no longer resets a field. Clear one out of band in the\n * Forgejo UI or with a direct PATCH. It costs no forever-diff, because an undeclared field is\n * not compared either.\n *\n * ⚠️ `private` IS THE ONE WORTH DECLARING ANYWAY ON EVERY REPO. Leaving it undeclared now means\n * Alchemy will not notice a repository being made public. That is the correct trade for a\n * generic family — undeclared is unmanaged, not assumed — but it is a real gap, so say it\n * explicitly in each declaration rather than relying on a default that no longer exists.\n */\n matches: (attributes, props) =>\n (props.description === undefined || attributes.description === props.description) &&\n (props.private === undefined || attributes.private === props.private) &&\n (props.hasIssues === undefined || attributes.hasIssues === props.hasIssues) &&\n (props.hasWiki === undefined || attributes.hasWiki === props.hasWiki) &&\n (props.hasProjects === undefined || attributes.hasProjects === props.hasProjects) &&\n (props.defaultBranch === undefined || attributes.defaultBranch === props.defaultBranch),\n path: (props) => `repos/${props.org}/${props.name}`,\n updateForm: (props) => ({\n ...(props.defaultBranch === undefined ? {} : { default_branch: props.defaultBranch }),\n ...(props.description === undefined ? {} : { description: props.description }),\n ...(props.hasIssues === undefined ? {} : { has_issues: props.hasIssues }),\n ...(props.hasProjects === undefined ? {} : { has_projects: props.hasProjects }),\n ...(props.hasWiki === undefined ? {} : { has_wiki: props.hasWiki }),\n ...(props.private === undefined ? {} : { private: props.private }),\n }),\n});\n\nexport const ForgejoRepositoryProvider = () =>\n Provider.effect(ForgejoRepository, Effect.succeed(ForgejoRepository.Provider.of(handlers)));\n",
|
|
16
|
+
"/**\n * `Forgejo.TeamMember` — one user in one organization team.\n *\n * ★ WHY IT EXISTS: forgejo-provision, the service account the Forgejo engine mints as, belongs to the\n * HomeFlare Owners team — Tim, 2026-09-14. Membership is what its tokens can reach; the role's\n * scopes only cap it (<estate>/openbao/src/forgejo-bootstrap.ts).\n *\n * ★ READ OFF THE LIVE 16.0.3 SWAGGER (127.0.0.1:3000/swagger.v1.json, 2026-09-14): GET, PUT and\n * DELETE `/teams/{id}/members/{username}` answer 200, 204 and 204, and 404 when absent. The team\n * is addressed by numeric id, so it is found by name first — the same seam as org-team.ts.\n *\n * ⚠️ EXISTENCE IS THE WHOLE OBJECT. Nothing about a membership can be updated, so `matches` is\n * always true; a different team or user is a different resource id in alchemy.run.ts.\n *\n * ⚠️ THE TEAM ITSELF IS ONLY LOCATED HERE, NEVER MANAGED. A missing team fails the plan by name\n * instead of folding into \"absent\", because a PUT can add a member but cannot create a team.\n *\n * ⛔ TOKEN NEEDS `write:organization` TO ADD OR REMOVE, and an identity allowed to manage the team.\n */\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { ForgejoError, forgejo } from './client.ts';\nimport { type ForgejoRequirements, forgejoHandlers } from './resource.ts';\n\nexport interface TeamMemberProps {\n org: string;\n /** Team name as Forgejo shows it, e.g. `Owners`. */\n team: string;\n username: string;\n}\n\nexport interface TeamMemberAttributes {\n org: string;\n team: string;\n teamId: number;\n username: string;\n}\n\nexport interface ForgejoTeamMember extends Resource<\n 'Forgejo.TeamMember',\n TeamMemberProps,\n TeamMemberAttributes,\n never,\n ForgejoRequirements\n> {}\n\nexport const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember');\n\n/** Where `locate` leaves the team id on the live row, for `attributes` and `wirePath`. */\nconst TEAM_ID = '__team_id';\n\nconst teamIdOf = (props: TeamMemberProps) =>\n Effect.gen(function* () {\n const path = `orgs/${props.org}/teams?limit=200`;\n const rows = yield* forgejo<Record<string, unknown>[]>('GET', path);\n const id = (Array.isArray(rows) ? rows : []).find((row) => row['name'] === props.team)?.['id'];\n if (typeof id !== 'number') {\n return yield* Effect.fail(\n new ForgejoError(0, 'GET', path, `no team named ${props.team} in org ${props.org}`),\n );\n }\n return id;\n });\n\nconst memberPath = (id: number, props: TeamMemberProps) =>\n `teams/${String(id)}/members/${props.username}`;\n\nconst handlers = forgejoHandlers<TeamMemberProps, TeamMemberAttributes>({\n attributes: (live, props) => {\n const id = live[TEAM_ID];\n const login = live['login'];\n if (typeof id !== 'number' || typeof login !== 'string') return undefined;\n if (login.toLowerCase() !== props.username.toLowerCase()) return undefined;\n return { org: props.org, team: props.team, teamId: id, username: props.username };\n },\n collection: (props) => `orgs/${props.org}/teams`,\n createForm: () => ({}),\n locate: (props) =>\n Effect.gen(function* () {\n const id = yield* teamIdOf(props);\n const user = yield* forgejo<Record<string, unknown>>('GET', memberPath(id, props));\n return user === undefined ? undefined : { ...user, [TEAM_ID]: id };\n }),\n matches: () => true,\n path: (props) => `orgs/${props.org}/teams/${props.team}/members/${props.username}`,\n upsert: (props, before) =>\n before !== undefined\n ? Effect.void\n : Effect.flatMap(teamIdOf(props), (id) =>\n Effect.asVoid(forgejo('PUT', memberPath(id, props))),\n ),\n wirePath: (props, live) => memberPath(Number(live[TEAM_ID]), props),\n});\n\nexport const ForgejoTeamMemberProvider = () =>\n Provider.effect(ForgejoTeamMember, Effect.succeed(ForgejoTeamMember.Provider.of(handlers)));\n"
|
|
17
17
|
],
|
|
18
18
|
"mappings": "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",
|
|
19
19
|
"debugId": "09F62B41760BE1CD64756E2164756E21",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `Forgejo.OrgSecret` — org-level Actions secret metadata at `/orgs/{org}/actions/secrets/{name}`.
|
|
3
3
|
*
|
|
4
4
|
* ★ CHOICE: AUTHORED WITH WRITE-ONLY VALUE VIA ENV, NOT REFUSED. The OpenAPI in
|
|
5
|
-
*
|
|
5
|
+
* `<estate>/mcp-servers/docs/api/upstream/forgejo.json` shows `Secret` returns only `name` and
|
|
6
6
|
* `created_at` — never the `data` field — so attributes stay metadata-only. Create/update is PUT
|
|
7
7
|
* with body `{ data }`, which does not fit POST/PATCH; this family uses the `upsert` seam on
|
|
8
8
|
* `ForgejoSpec` instead of hand-rolling provider handlers.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.OrgLabel` — one org-level issue label shared across every repository in the org.
|
|
3
3
|
*
|
|
4
|
-
* ★ RETIRES `
|
|
4
|
+
* ★ RETIRES the estate's hand-run `labels-reconcile.sh`, which
|
|
5
5
|
* converged the whole `labels.yaml` file in one shot. That script CREATED and PATCHED labels
|
|
6
6
|
* from the spec and REPORTED `EXTRA` labels present live but absent from the file — it never
|
|
7
7
|
* deleted them, because removing a label strips it from every issue that carries it.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.OrgTeam` — one organization team under `/orgs/{org}/teams`.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json` (Forgejo 16.x OpenAPI): create is
|
|
5
5
|
* `POST /orgs/{org}/teams`; read/update/delete use numeric id at `/teams/{id}` — not the name.
|
|
6
6
|
*
|
|
7
7
|
* ⚠️ PATCH AND DELETE USE `/teams/{id}` — same seam as org labels. `locate` lists by name;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.RepoWebhook` — one repository webhook under `/repos/{owner}/{repo}/hooks`.
|
|
3
3
|
*
|
|
4
|
-
* ★ READ OFF
|
|
4
|
+
* ★ READ OFF `<estate>/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection; read/update/delete
|
|
5
5
|
* use numeric hook id at `/repos/{owner}/{repo}/hooks/{id}`. `locate` lists and matches on `name`.
|
|
6
6
|
*
|
|
7
7
|
* ⛔ NO WEBHOOK `secret` IN PROPS OR ATTRIBUTES. Optional HMAC material is env-only at write time
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Forgejo.Repository` — an organization-owned git repository.
|
|
3
3
|
*
|
|
4
|
-
* ★ MEASURED 2026-09-13 against the live daemon (`GET /api/v1/repos
|
|
4
|
+
* ★ MEASURED 2026-09-13 against the live daemon (`GET /api/v1/repos/<org>/<repo>`):
|
|
5
5
|
* responses are flat JSON with `private`, `has_issues`, `default_branch`, etc. Create is
|
|
6
6
|
* `POST /orgs/{org}/repos`; read/update/delete use `/repos/{owner}/{repo}`.
|
|
7
7
|
*
|
|
@@ -12,7 +12,7 @@ import { Resource } from 'alchemy';
|
|
|
12
12
|
import * as Provider from 'alchemy/Provider';
|
|
13
13
|
import { type ForgejoRequirements } from './resource.ts';
|
|
14
14
|
export interface RepositoryProps {
|
|
15
|
-
/** Organization login — the owner segment in
|
|
15
|
+
/** Organization login — the owner segment in `<org>/<repo>`. */
|
|
16
16
|
org: string;
|
|
17
17
|
/** Repository name. Changing it is a replace, not an update. */
|
|
18
18
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/forgejo/repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,mBAAmB,EAAmB,MAAM,eAAe,CAAC;AAG1E,MAAM,WAAW,eAAe;IAC9B,
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/forgejo/repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,KAAK,mBAAmB,EAAmB,MAAM,eAAe,CAAC;AAG1E,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,KAAK,EACL,mBAAmB,CACpB;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAE5B,CAAC;AAqEH,eAAO,MAAM,yBAAyB,uIACuD,CAAC"}
|
|
@@ -5,7 +5,7 @@ import { ForgejoError } from './client.ts';
|
|
|
5
5
|
/**
|
|
6
6
|
* ★ WHAT EVERY OPERATION HERE NEEDS FROM THE RUNTIME — and it is NOT what the first draft said.
|
|
7
7
|
*
|
|
8
|
-
* These resources declared `ChildProcessSpawner`, copied from
|
|
8
|
+
* These resources declared `ChildProcessSpawner`, copied from <estate>/proxmox, where it is there
|
|
9
9
|
* because a PVE credential is minted by shelling out to `bao`. Nothing in this package shells
|
|
10
10
|
* out: the token comes from `FORGEJO_TOKEN` in the environment. Declaring a service you never
|
|
11
11
|
* use is not harmless — it makes a stack provide a layer for nothing, and it hides the one
|
|
@@ -54,7 +54,7 @@ export declare const forgejoOperations: <Props extends object, Attributes>(spec:
|
|
|
54
54
|
* The five provider handlers for a spec'd Forgejo object, wired once.
|
|
55
55
|
*
|
|
56
56
|
* ⛔ IT STOPS AT THE HANDLERS AND DOES NOT RETURN THE LAYER — same reasoning as pveHandlers in
|
|
57
|
-
*
|
|
57
|
+
* <estate>/proxmox: wrapping `Provider.effect` here needs casts against Alchemy's `Props<R>`.
|
|
58
58
|
*
|
|
59
59
|
* ⚠️ `list` answers empty — Forgejo index endpoints return the whole org; adoption stays explicit.
|
|
60
60
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ★ WHY IT EXISTS: forgejo-provision, the service account the Forgejo engine mints as, belongs to the
|
|
5
5
|
* HomeFlare Owners team — Tim, 2026-09-14. Membership is what its tokens can reach; the role's
|
|
6
|
-
* scopes only cap it (
|
|
6
|
+
* scopes only cap it (<estate>/openbao/src/forgejo-bootstrap.ts).
|
|
7
7
|
*
|
|
8
8
|
* ★ READ OFF THE LIVE 16.0.3 SWAGGER (127.0.0.1:3000/swagger.v1.json, 2026-09-14): GET, PUT and
|
|
9
9
|
* DELETE `/teams/{id}/members/{username}` answer 200, 204 and 204, and 404 when absent. The team
|