@homeflare/alchemy 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +25 -1
  2. package/dist/litellm/client.d.ts +76 -0
  3. package/dist/litellm/client.d.ts.map +1 -0
  4. package/dist/litellm/credentials.d.ts +48 -0
  5. package/dist/litellm/credentials.d.ts.map +1 -0
  6. package/dist/litellm/fake-litellm.d.ts +36 -0
  7. package/dist/litellm/fake-litellm.d.ts.map +1 -0
  8. package/dist/litellm/fake-stack.d.ts +30 -0
  9. package/dist/litellm/fake-stack.d.ts.map +1 -0
  10. package/dist/litellm/generated/pass-through.d.ts +71 -0
  11. package/dist/litellm/generated/pass-through.d.ts.map +1 -0
  12. package/dist/litellm/index.d.ts +13 -0
  13. package/dist/litellm/index.d.ts.map +1 -0
  14. package/dist/litellm/index.js +270 -0
  15. package/dist/litellm/index.js.map +14 -0
  16. package/dist/litellm/pass-through-endpoint.d.ts +94 -0
  17. package/dist/litellm/pass-through-endpoint.d.ts.map +1 -0
  18. package/dist/litellm/pass-through-form.d.ts +40 -0
  19. package/dist/litellm/pass-through-form.d.ts.map +1 -0
  20. package/dist/litellm/providers.d.ts +16 -0
  21. package/dist/litellm/providers.d.ts.map +1 -0
  22. package/dist/paperless/client.d.ts +37 -0
  23. package/dist/paperless/client.d.ts.map +1 -0
  24. package/dist/paperless/constraint-guard.d.ts +19 -0
  25. package/dist/paperless/constraint-guard.d.ts.map +1 -0
  26. package/dist/paperless/constraints.d.ts +50 -0
  27. package/dist/paperless/constraints.d.ts.map +1 -0
  28. package/dist/paperless/credentials.d.ts +40 -0
  29. package/dist/paperless/credentials.d.ts.map +1 -0
  30. package/dist/paperless/custom-field.d.ts +75 -0
  31. package/dist/paperless/custom-field.d.ts.map +1 -0
  32. package/dist/paperless/document-type.d.ts +30 -0
  33. package/dist/paperless/document-type.d.ts.map +1 -0
  34. package/dist/paperless/errors.d.ts +49 -0
  35. package/dist/paperless/errors.d.ts.map +1 -0
  36. package/dist/paperless/fake-paperless.d.ts +39 -0
  37. package/dist/paperless/fake-paperless.d.ts.map +1 -0
  38. package/dist/paperless/generated/constraints/index.d.ts +13 -0
  39. package/dist/paperless/generated/constraints/index.d.ts.map +1 -0
  40. package/dist/paperless/generated/constraints/paperless-custom_fields.d.ts +21 -0
  41. package/dist/paperless/generated/constraints/paperless-custom_fields.d.ts.map +1 -0
  42. package/dist/paperless/generated/constraints/paperless-document_types.d.ts +21 -0
  43. package/dist/paperless/generated/constraints/paperless-document_types.d.ts.map +1 -0
  44. package/dist/paperless/generated/constraints/paperless-storage_paths.d.ts +21 -0
  45. package/dist/paperless/generated/constraints/paperless-storage_paths.d.ts.map +1 -0
  46. package/dist/paperless/generated/constraints/paperless-tags.d.ts +21 -0
  47. package/dist/paperless/generated/constraints/paperless-tags.d.ts.map +1 -0
  48. package/dist/paperless/generated/types/custom-fields.d.ts +23 -0
  49. package/dist/paperless/generated/types/custom-fields.d.ts.map +1 -0
  50. package/dist/paperless/generated/types/document-types.d.ts +39 -0
  51. package/dist/paperless/generated/types/document-types.d.ts.map +1 -0
  52. package/dist/paperless/generated/types/index.d.ts +10 -0
  53. package/dist/paperless/generated/types/index.d.ts.map +1 -0
  54. package/dist/paperless/generated/types/storage-paths.d.ts +31 -0
  55. package/dist/paperless/generated/types/storage-paths.d.ts.map +1 -0
  56. package/dist/paperless/generated/types/tags.d.ts +37 -0
  57. package/dist/paperless/generated/types/tags.d.ts.map +1 -0
  58. package/dist/paperless/index.d.ts +19 -0
  59. package/dist/paperless/index.d.ts.map +1 -0
  60. package/dist/paperless/index.js +524 -0
  61. package/dist/paperless/index.js.map +26 -0
  62. package/dist/paperless/matching-locate.d.ts +33 -0
  63. package/dist/paperless/matching-locate.d.ts.map +1 -0
  64. package/dist/paperless/matching-types.d.ts +60 -0
  65. package/dist/paperless/matching-types.d.ts.map +1 -0
  66. package/dist/paperless/matching.d.ts +61 -0
  67. package/dist/paperless/matching.d.ts.map +1 -0
  68. package/dist/paperless/providers.d.ts +15 -0
  69. package/dist/paperless/providers.d.ts.map +1 -0
  70. package/dist/paperless/storage-path.d.ts +32 -0
  71. package/dist/paperless/storage-path.d.ts.map +1 -0
  72. package/dist/paperless/tag.d.ts +47 -0
  73. package/dist/paperless/tag.d.ts.map +1 -0
  74. package/dist/postgres/connection.d.ts +50 -0
  75. package/dist/postgres/connection.d.ts.map +1 -0
  76. package/dist/postgres/database-attrs.d.ts +79 -0
  77. package/dist/postgres/database-attrs.d.ts.map +1 -0
  78. package/dist/postgres/database-sql.d.ts +73 -0
  79. package/dist/postgres/database-sql.d.ts.map +1 -0
  80. package/dist/postgres/database.d.ts +63 -0
  81. package/dist/postgres/database.d.ts.map +1 -0
  82. package/dist/postgres/errors.d.ts +86 -0
  83. package/dist/postgres/errors.d.ts.map +1 -0
  84. package/dist/postgres/fake-sql.d.ts +22 -0
  85. package/dist/postgres/fake-sql.d.ts.map +1 -0
  86. package/dist/postgres/index.d.ts +16 -0
  87. package/dist/postgres/index.d.ts.map +1 -0
  88. package/dist/postgres/index.js +238 -0
  89. package/dist/postgres/index.js.map +15 -0
  90. package/dist/postgres/providers.d.ts +14 -0
  91. package/dist/postgres/providers.d.ts.map +1 -0
  92. package/dist/proxmox/index.js.map +2 -2
  93. package/dist/proxmox/zfs-pool.d.ts +10 -2
  94. package/dist/proxmox/zfs-pool.d.ts.map +1 -1
  95. package/dist/release/archive.d.ts +6 -2
  96. package/dist/release/archive.d.ts.map +1 -1
  97. package/dist/release/binary-form.d.ts +14 -9
  98. package/dist/release/binary-form.d.ts.map +1 -1
  99. package/dist/release/binary-pins.d.ts +17 -0
  100. package/dist/release/binary-pins.d.ts.map +1 -0
  101. package/dist/release/catalog.d.ts.map +1 -1
  102. package/dist/release/index.js +90 -40
  103. package/dist/release/index.js.map +8 -7
  104. package/dist/release/tar.d.ts +9 -2
  105. package/dist/release/tar.d.ts.map +1 -1
  106. package/docs/litellm.md +110 -0
  107. package/docs/paperless.md +115 -0
  108. package/docs/peers.md +8 -1
  109. package/docs/postgres.md +125 -0
  110. package/docs/release-binary-catalogs.md +40 -4
  111. package/docs/release-binary-refusals.md +22 -8
  112. package/docs/release-binary-upstream.md +33 -15
  113. package/package.json +14 -1
  114. package/src/litellm/client.ts +195 -0
  115. package/src/litellm/credentials.ts +73 -0
  116. package/src/litellm/fake-litellm.ts +114 -0
  117. package/src/litellm/fake-stack.ts +100 -0
  118. package/src/litellm/generated/pass-through.ts +73 -0
  119. package/src/litellm/index.ts +42 -0
  120. package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +66 -0
  121. package/src/litellm/pass-through-endpoint-replace.test.ts +84 -0
  122. package/src/litellm/pass-through-endpoint.test.ts +208 -0
  123. package/src/litellm/pass-through-endpoint.ts +258 -0
  124. package/src/litellm/pass-through-form.test.ts +128 -0
  125. package/src/litellm/pass-through-form.ts +136 -0
  126. package/src/litellm/providers.ts +19 -0
  127. package/src/paperless/client.ts +88 -0
  128. package/src/paperless/constraint-guard.ts +50 -0
  129. package/src/paperless/constraints.test.ts +74 -0
  130. package/src/paperless/constraints.ts +111 -0
  131. package/src/paperless/credentials.ts +69 -0
  132. package/src/paperless/custom-field.test.ts +74 -0
  133. package/src/paperless/custom-field.ts +95 -0
  134. package/src/paperless/document-type.ts +74 -0
  135. package/src/paperless/errors.ts +62 -0
  136. package/src/paperless/fake-paperless.ts +156 -0
  137. package/src/paperless/generated/constraints/index.ts +23 -0
  138. package/src/paperless/generated/constraints/paperless-custom_fields.ts +29 -0
  139. package/src/paperless/generated/constraints/paperless-document_types.ts +32 -0
  140. package/src/paperless/generated/constraints/paperless-storage_paths.ts +34 -0
  141. package/src/paperless/generated/constraints/paperless-tags.ts +34 -0
  142. package/src/paperless/generated/types/custom-fields.ts +23 -0
  143. package/src/paperless/generated/types/document-types.ts +30 -0
  144. package/src/paperless/generated/types/index.ts +9 -0
  145. package/src/paperless/generated/types/storage-paths.ts +31 -0
  146. package/src/paperless/generated/types/tags.ts +37 -0
  147. package/src/paperless/index.ts +56 -0
  148. package/src/paperless/matching-identity.test.ts +194 -0
  149. package/src/paperless/matching-locate.ts +104 -0
  150. package/src/paperless/matching-types.ts +61 -0
  151. package/src/paperless/matching.test.ts +128 -0
  152. package/src/paperless/matching.ts +239 -0
  153. package/src/paperless/providers.ts +27 -0
  154. package/src/paperless/storage-path.ts +78 -0
  155. package/src/paperless/tag.ts +91 -0
  156. package/src/postgres/connection.ts +62 -0
  157. package/src/postgres/database-attrs.ts +87 -0
  158. package/src/postgres/database-bigint-serialization.test.ts +140 -0
  159. package/src/postgres/database-lifecycle.test.ts +113 -0
  160. package/src/postgres/database-provenance.test.ts +118 -0
  161. package/src/postgres/database-refusals.test.ts +135 -0
  162. package/src/postgres/database-sql.ts +174 -0
  163. package/src/postgres/database.ts +193 -0
  164. package/src/postgres/errors.ts +131 -0
  165. package/src/postgres/fake-sql.ts +123 -0
  166. package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +26 -0
  167. package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +67 -0
  168. package/src/postgres/index.ts +30 -0
  169. package/src/postgres/providers.ts +16 -0
  170. package/src/proxmox/zfs-pool.ts +10 -2
  171. package/src/release/archive.ts +10 -4
  172. package/src/release/binary-form.ts +24 -76
  173. package/src/release/binary-pins.ts +95 -0
  174. package/src/release/binary-root.test.ts +122 -0
  175. package/src/release/catalog.ts +10 -2
  176. package/src/release/tar-root.test.ts +114 -0
  177. package/src/release/tar.ts +78 -12
@@ -6,8 +6,15 @@ export type TarContents = {
6
6
  /** Every entry name, in archive order — so a refusal can say what the archive did hold. */
7
7
  readonly names: readonly string[];
8
8
  };
9
- /** A push parser: `push` each chunk of the unzipped stream, then `finish`. Either may throw. */
10
- export declare const tarReader: (wanted: ReadonlySet<string>) => {
9
+ /**
10
+ * A push parser: `push` each chunk of the unzipped stream, then `finish`. Either may throw.
11
+ *
12
+ * @param root When given, exactly one typeflag-'5' entry named `<root>/`, size 0, is accepted and
13
+ * dropped rather than refused as a directory; every OTHER entry name must begin with the SEGMENT
14
+ * `<root>/` (a `<root>-evil/x` sibling does not) and is reported, matched and kept under that
15
+ * prefix stripped off. Omitted, behaviour is byte-identical to a reader with no root at all.
16
+ */
17
+ export declare const tarReader: (wanted: ReadonlySet<string>, root?: string) => {
11
18
  finish: () => TarContents;
12
19
  push: (chunk: Uint8Array) => void;
13
20
  };
@@ -1 +1 @@
1
- {"version":3,"file":"tar.d.ts","sourceRoot":"","sources":["../../src/release/tar.ts"],"names":[],"mappings":"AAqFA,0CAA0C;AAC1C,eAAO,MAAM,WAAW,UAAW,SAAS,UAAU,EAAE,KAAG,UAQ1D,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,mGAAmG;IACnG,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClD,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,CAAC;AAEF,gGAAgG;AAChG,eAAO,MAAM,SAAS,WAAY,WAAW,CAAC,MAAM,CAAC;kBAoFhC,WAAW;kBAlCT,UAAU,KAAG,IAAI;CAwCvC,CAAC"}
1
+ {"version":3,"file":"tar.d.ts","sourceRoot":"","sources":["../../src/release/tar.ts"],"names":[],"mappings":"AAoGA,0CAA0C;AAC1C,eAAO,MAAM,WAAW,UAAW,SAAS,UAAU,EAAE,KAAG,UAQ1D,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,mGAAmG;IACnG,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClD,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,WAAY,WAAW,CAAC,MAAM,CAAC,SAAS,MAAM;kBA6H/C,WAAW;kBAlCT,UAAU,KAAG,IAAI;CA2CvC,CAAC"}
@@ -0,0 +1,110 @@
1
+ # LiteLLM — `@homeflare/alchemy/litellm`
2
+
3
+ `LiteLLM.PassThroughEndpoint` declares one row of LiteLLM's `/config/pass_through_endpoint`
4
+ family: a route on the proxy that forwards requests to an upstream target, generated from
5
+ LiteLLM **1.100.0**'s own OpenAPI document (`codegen/manifest.json`'s `litellm-openapi` entry —
6
+ vendor code at tag `v1.100.0`, dumped by the vendor's own `gen-api-types.mjs`, cross-checked
7
+ byte-identically against the tag's committed `schema.d.ts`; NOT a live read, because the reference
8
+ proxy's `/openapi.json` could not be reached when this was walked — see the manifest entry's note).
9
+
10
+ ## What is DB-backed and API-managed, versus config-file only
11
+
12
+ Measured at 1.100.0 (`schema.prisma`, `pass_through_endpoints.py`, and the mini's own
13
+ `settings.yaml` `store_model_in_db: true`):
14
+
15
+ | Family | Managed by this resource? |
16
+ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
17
+ | Pass-through endpoints | Yes — this package |
18
+ | Models, virtual keys, teams, credentials, access groups | DB-backed, API-managed, but no kit resource yet |
19
+ | `router_settings`, `litellm_settings`, `general_settings.master_key`/`database_url`/`store_model_in_db` | config-file only — read-only through the API |
20
+ | A `config.yaml` pass-through entry (`is_from_config: true`) | Read-only through this resource — see Refusals |
21
+
22
+ ## Credentials
23
+
24
+ Reads `LITELLM_PROXY_URL` and `LITELLM_PROXY_API_KEY` at call time — LiteLLM's own variable
25
+ names, the ones its `litellm` CLI reads (`litellm/proxy/client/cli/main.py` at the tag). No
26
+ default URL. The key must be LiteLLM's master key or another `PROXY_ADMIN` key:
27
+ `update_config_general_settings`, the function every mutation goes through, is `PROXY_ADMIN`
28
+ only. Never a prop — see [docs/credentials.md](./credentials.md) for the house rule.
29
+
30
+ ## Refusals
31
+
32
+ This resource refuses a plan rather than writing something it cannot safely undo:
33
+
34
+ - **A path already held by an `is_from_config` row.** A DB row on the same path overrides the
35
+ config file at runtime, so creating one would silently take over a route the operator declared
36
+ in `config.yaml`. Change the config file instead, or pick a different path.
37
+ - **A DB row LiteLLM did not give an `id`.** LiteLLM identifies some pass-through endpoints by
38
+ path alone, for backwards compatibility. Update and delete are both id-keyed, so this resource
39
+ cannot address a row like that — it is refused rather than silently adopted into a lifecycle
40
+ that cannot actually manage it.
41
+ - **A literal secret in a forwarded header.** `Authorization`, `x-api-key` and
42
+ `cf-aig-authorization` must contain LiteLLM's own `os.environ/NAME` reference form
43
+ (`set_env_variables_in_header`, `pass_through_endpoints.py` at the tag), never a literal value —
44
+ a house rule (not a vendor one), because a missing variable on the proxy host would otherwise
45
+ leave the literal string in the header with nothing in the API able to detect it.
46
+
47
+ A foreign DB row on the same path that LiteLLM DID give an id is `Unowned`: `read` finds it and
48
+ the plan needs `--adopt` before it is bound.
49
+
50
+ ## Nullable fields: `timeout`, `methods`, `guardrails`
51
+
52
+ LiteLLM's update route merges the parsed body with `exclude_none` (measured at 1.100.0), so it
53
+ can never clear a field that is already set — sending neither `null` nor omitting it changes
54
+ anything already stored. Clearing one of these three fields is therefore planned as a
55
+ **replace**: delete the row, then create it again without the field. There is a brief window
56
+ with the route absent, and it exists because the alternative — create-before-delete — would put
57
+ two rows on the same path, which this resource never allows.
58
+
59
+ ## The whole-field write, and why every mutation is serialised
60
+
61
+ Every pass-through endpoint lives in ONE `general_settings.pass_through_endpoints` list. Create,
62
+ update and delete are each a read-modify-write of that WHOLE list. `client.ts` wraps every
63
+ mutating call in a per-base-URL `Effect` semaphore, so two of THIS PROCESS's own calls never
64
+ race each other — two endpoints declared in the same deploy both survive.
65
+
66
+ ⛔ **What the semaphore does not cover.** It serialises this package's own writes within one
67
+ process. It cannot serialise against the LiteLLM UI, another stack, or a second concurrent
68
+ `alchemy deploy` — the field has no ETag and no vendor-side locking. `reconcile` reads the row
69
+ back after every write rather than trusting the call that just returned, so a lost update is
70
+ visible on the NEXT plan (as drift) even though it cannot be prevented on this one.
71
+
72
+ ## Example
73
+
74
+ Estate values — the account id, the gateway id, the upstream key's variable name — are never kit
75
+ constants; they are props your own stack supplies. Placeholders below, per the TypeSafe plugin's
76
+ `ai-gateway.md` reference (Cloudflare's `/accounts/{account_id}/ai/run`, a Workers-AI-scoped
77
+ Bearer token, `cf-aig-gateway-id` and `cf-aig-no-wholesale: true`):
78
+
79
+ ```ts
80
+ import { LiteLLMPassThroughEndpoint, litellmProviders } from '@homeflare/alchemy/litellm';
81
+
82
+ export class AiGateway extends LiteLLMPassThroughEndpoint('AiGateway', {
83
+ path: '/cloudflare-ai',
84
+ target: 'https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/run',
85
+ headers: {
86
+ Authorization: 'os.environ/CF_AI_GATEWAY_TOKEN',
87
+ 'cf-aig-gateway-id': '<GATEWAY_ID>',
88
+ 'cf-aig-no-wholesale': 'true',
89
+ },
90
+ timeout: 60,
91
+ }) {}
92
+ ```
93
+
94
+ ```ts
95
+ // …then provide `litellmProviders()` alongside the stack's other providers.
96
+ ```
97
+
98
+ ## Not covered
99
+
100
+ - **Models, virtual keys, teams, credentials, access groups.** DB-backed and API-managed at
101
+ 1.100.0, but no kit resource yet — deferred; see the unit's own trigger notes.
102
+ - **The Claude OAuth model/key/team slice.** Needs estate answers only Tim can give.
103
+ - **Runtime behaviour of a route forced into the schema for the dashboard.** The byte-identical
104
+ cross-check proves the REQUEST/RESPONSE BODY SHAPES match the tag exactly. It does not prove
105
+ every route the dashboard's generator forces into the schema (`include_in_schema = True`) is
106
+ actually served the same way in production, or that the mini's own `config.yaml`/
107
+ `general_settings` match what `schema.prisma`/`proxy_server.py` assume.
108
+ - **More than one uvicorn worker.** Pass-through routes register themselves on the worker that
109
+ handled the registering request (`add_*_route` on `request.app`); with more than one worker,
110
+ the others' routes are unmeasured by this package.
@@ -0,0 +1,115 @@
1
+ # Paperless — `@homeflare/alchemy/paperless`
2
+
3
+ Four resources — `Paperless.Tag`, `Paperless.DocumentType`, `Paperless.StoragePath`,
4
+ `Paperless.CustomField` — the taxonomy Paperless-ngx writes through Django's internal models,
5
+ create-or-update and never deleted by default. Workflows and mail accounts are a later slice.
6
+
7
+ ## Credentials
8
+
9
+ Two environment variables, read **at call time**, never props:
10
+
11
+ | variable | holds |
12
+ | ----------------- | --------------------------------------------------------- |
13
+ | `PAPERLESS_URL` | the instance origin, e.g. `https://paperless.example.com` |
14
+ | `PAPERLESS_TOKEN` | a scoped Paperless-ngx API token |
15
+
16
+ ⛔ **Neither has a default.** A package that fell back to a loopback address would let a consumer
17
+ who forgot the variable watch every call fail against a machine that is not theirs.
18
+
19
+ ⚠️ **`Authorization: Token <value>`, not `Bearer`** — Paperless-ngx's own `securitySchemes`
20
+ states the "Token" prefix explicitly.
21
+
22
+ ⚠️ **Every call carries `Accept: application/json; version=10`**, pinned to the API version this
23
+ family was walked against (`REST_FRAMEWORK.DEFAULT_VERSION` at v3.1.1). An estate upgrade that
24
+ moves the default cannot silently change what this package receives without that assertion
25
+ failing first.
26
+
27
+ ## Every create carries `owner`, null when undeclared
28
+
29
+ 🔴 **This is the one behaviour that had to be measured, not guessed.**
30
+ `OwnedObjectSerializer.create` (Paperless-ngx `documents/serialisers.py:495-502` at v3.1.1) sets
31
+ the owner to the TOKEN USER whenever the field is absent from the body. Omitting an undeclared
32
+ prop the way you might expect — `Netbox.Prefix`'s pattern for an undeclared foreign key — would
33
+ silently create an object owned by whichever credential ran the deploy: invisible to every other
34
+ user, and a second row beside the estate's `owner: null` one on the next apply. So `Tag`,
35
+ `DocumentType` and `StoragePath` always send `owner: null` unless `owner` is declared.
36
+
37
+ ⛔ `Paperless.CustomField` has **no owner field at all** — its wire body is exactly `{name,
38
+ data_type, extra_data}`, measured against the schema. It is not `OwnedObjectSerializer`-based.
39
+
40
+ ## Adopt is never silent (H1)
41
+
42
+ ★ **`read` answers `Unowned(attrs)` on every match**, never plain attributes. Paperless has no
43
+ ownership marker this package can check — `owner` is the estate's own prop, not proof of who
44
+ created a row — so, per the house's 2026-09-21 decision, identical is not ours. `--adopt` (or
45
+ `.pipe(adopt(true))`) is required to bind an existing tag, document type, storage path or custom
46
+ field, the same posture as any other marker-less API.
47
+
48
+ ## `CustomField.dataType` is create-only
49
+
50
+ ⛔ **A change is refused at plan time, never a PATCH and never a replace.** Changing a custom
51
+ field's data type in Paperless deletes that field's value on every document that carries it — a
52
+ replace (delete + recreate) is exactly as destructive as a PATCH would be, just under a different
53
+ name. Declaring a different `dataType` against a live object fails the plan with both values
54
+ named; retire the old field and create a new one instead.
55
+
56
+ ⚠️ The reconciler this family replaces asserted "Paperless forbids a data_type change" — that
57
+ claim is **unverified** at v3.1.1 (`CustomFieldSerializer.validate` and `CustomFieldViewSet`
58
+ contain no such refusal). This package refuses the change regardless, because the destructive
59
+ side effect is real whether or not the vendor also refuses it.
60
+
61
+ ## Removal is `retain`
62
+
63
+ Every family defaults to `retain` — a tag, document type, storage path or custom field id is
64
+ referenced by every document that carries it, and Paperless does not offer a "detach first"
65
+ option. `delete` is fully implemented; dropping a declaration does not delete the object unless
66
+ the caller opts into `.pipe(RemovalPolicy.destroy())`.
67
+
68
+ ## Locate is by name only before there is state; identity is the stored id after
69
+
70
+ `MatchingModel` (Paperless-ngx `models.py:79-89`) has `UniqueConstraint(name, owner)` plus a
71
+ unique name where owner is null. Before a declaration has ever been deployed — a genuine first
72
+ create, or the engine's own `--adopt` probe — `matching.ts` locates candidates with a
73
+ case-insensitive `name__iexact` filter, wide enough to see every case-variant Paperless holds,
74
+ and then narrows to the exact-case, exact-owner row in this process (the same locate-then-identify
75
+ split `Netbox.Prefix` uses for its VRF). A locate that matches more than the 20-row page it reads
76
+ is refused as "not a natural key".
77
+
78
+ ⛔ **Once a declaration has state, identity is the stored numeric id, not `(name, owner)`.**
79
+ Renaming a tag, or re-owning any of the three owned families, is an ordinary edit — and locating
80
+ by the _new_ name/owner would miss the row this declaration already wrote, PATCHing nothing and
81
+ POSTing a second object under the new name while the original sat there permanently orphaned
82
+ (`list()` always answers `[]` by design, so `nuke` can never find it either). `diff`/`reconcile`
83
+ locate by the id Alchemy's engine already has on hand (`output.id`) instead: a changed name/owner
84
+ becomes an in-place PATCH of that same id — Paperless allows renaming all four types by PATCH, and
85
+ every document keeps its tag/type/path/field because the id never changes. If that id no longer
86
+ exists live (deleted out of band), the plan refuses loudly rather than silently creating a
87
+ replacement; adopt the live object back or drop it from state first.
88
+
89
+ `destroy` locates the same way — by `output.id` once state exists, so a row renamed out of band
90
+ after its last deploy is still found and deleted by id rather than silently missed by a
91
+ now-stale name. Unlike `diff`/`reconcile`, a missing `output.id` at delete time is not a refusal:
92
+ the object being already gone is exactly the idempotent success `delete` reports.
93
+
94
+ ## Generated, not hand-typed
95
+
96
+ `generated/types/*.ts` and `generated/constraints/*.ts` come from `bun codegen/paperless.ts`
97
+ against Paperless-ngx 3.1.1's own served OpenAPI document — see
98
+ [`codegen/paperless.md`](../../../codegen/paperless.md) for the full provenance chain. A prop's
99
+ enum-shaped fields (`matchingAlgorithm`, `dataType`) are typed by indexing into the generated
100
+ request interface (`PaperlessTagRequest['matching_algorithm']`), never retyped by hand.
101
+
102
+ ## Usage
103
+
104
+ ```ts
105
+ import { paperlessProviders, Tag } from '@homeflare/alchemy/paperless';
106
+ import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
107
+
108
+ // PAPERLESS_URL and PAPERLESS_TOKEN in the deploy's environment.
109
+ export default async () => {
110
+ const app = await alchemy('paperless-taxonomy');
111
+ const invoices = await Tag('invoices', { name: 'Invoices', color: '#a6cee3' });
112
+ await app.finalize();
113
+ };
114
+ // wire the stack: Layer.mergeAll(paperlessProviders(), …).pipe(Layer.provide(FetchHttpClient.layer))
115
+ ```
package/docs/peers.md CHANGED
@@ -7,7 +7,7 @@ The install line for `@homeflare/alchemy`, and why every peer on it and every en
7
7
  ```sh
8
8
  bun add @homeflare/alchemy alchemy@2.0.0-beta.79 effect@4.0.0-rc.115 \
9
9
  @effect/platform-node@4.0.0-rc.115 cloudflare@4.5.0 mime@4.1.0 \
10
- @distilled.cloud/cloudflare@1.0.0-rc.12
10
+ @distilled.cloud/cloudflare@1.0.0-rc.12 @effect/sql-pg@4.0.0-rc.115
11
11
  ```
12
12
 
13
13
  🔴 **Why, measured 2026-09-16 on 0.1.0 and re-checked 2026-09-17 against Alchemy 78.**
@@ -37,3 +37,10 @@ that followed the 78 README does not drop a required line.
37
37
  subpath would degrade without it — measured 2026-09-16, the subpath does not load at all:
38
38
  `Cannot find package 'cloudflare'`. An optional peer should mean a feature is absent, not
39
39
  that an import fails.
40
+
41
+ ⚠️ **`@effect/sql-pg` (added for `/postgres`, 2026-09-23) is required too, for the same
42
+ reason, not because every subpath imports it.** This package has one flat peer set for the
43
+ whole install, not one per subpath — `peers.test.ts`'s "no peer is marked optional" test
44
+ enforces it. `@effect/sql-pg` is optional on `alchemy`'s own manifest (its module graph
45
+ does not reach `SQL/Postgres` unless a stack imports it); here it stays a plain peer like
46
+ every other one.
@@ -0,0 +1,125 @@
1
+ # PostgreSQL — `@homeflare/alchemy/postgres`
2
+
3
+ `Postgres.Database`: create-and-assert over a self-hosted cluster, walked against PostgreSQL
4
+ 18.6 (`REL_18_6`, commit `724edf9b`). It retires `org.nixos.postgres-reconcile`, a 38-line bash
5
+ loop over 19 databases run against the trust socket.
6
+
7
+ ## Why the kit builds this instead of using upstream
8
+
9
+ Upstream `alchemy@2.0.0-beta.79` has no resource for a database in a self-hosted cluster —
10
+ measured against `v2.0.0-beta.79`'s full tree: it ships vendor-API Postgres resources
11
+ (`Planetscale.Postgres.*`, `Neon.PostgresOrigin`, `Prisma.Postgres`, `Fly.Postgres`,
12
+ `Railway.Postgres`) and the runtime binding `alchemy/SQL/Postgres` over `@effect/sql-pg`, but
13
+ nothing that issues `CREATE DATABASE` against a cluster you run yourself. No
14
+ `@distilled.cloud/postgres` or `@distilled.cloud/postgresql` package exists either (registry
15
+ 404, checked 2026-09-23). This family builds on the same `@effect/sql-pg` `PgClient` upstream's
16
+ own binding uses, so it stays host-independent and portable — CT100's own Postgres (vault
17
+ decision 21) can use it later, and it is shaped to upstream.
18
+
19
+ ## The locked shape
20
+
21
+ Create-and-assert only. `delete` always refuses. There is no `ALTER OWNER` and no `ALTER
22
+ DATABASE` of any kind. There is no password prop — a database carries none in `pg_database`.
23
+
24
+ ## Measured path (2026-09-23)
25
+
26
+ Read `pg_hba_file_rules` and `pg_settings` over the trust socket: `listen_addresses` is
27
+ `10.100.1.1`, `ssl` is `on` (min `TLSv1.2`), and the `hostssl` rules cover only `netbox`,
28
+ `discord`, `litellm`, `teslamate` and one `homeflare-*` app group — **no TCP rule reaches the
29
+ `postgres` maintenance database.** Line 1 of `pg_hba_file_rules` is `local all all trust`. So
30
+ today only a stack that deploys on the mini, over the Unix socket at
31
+ `/opt/homeflare/postgres/sockets` (mode `0755`, owner `tim`), can create a database.
32
+
33
+ **A JS client reaches that socket.** Measured live with `@effect/sql-pg@4.0.0-rc.115` under
34
+ Bun, from scratch space, never committed: `PgClient.layer({ host:
35
+ '/opt/homeflare/postgres/sockets', port: 5432, database: 'postgres', username: 'tim' })`
36
+ connects and `SELECT version()` answers `PostgreSQL 18.6 on aarch64-apple-darwin25.6.0…`. The
37
+ catalog SELECT this family issues answered all 24 live non-template databases. `SHOW
38
+ standard_conforming_strings` is `on` — the assumption `quoteStringLiteral` (`database-sql.ts`)
39
+ rests on.
40
+
41
+ The reconciler being retired, `/nix/store/…-postgres-reconcile.sh`, runs `psql -h
42
+ /opt/homeflare/postgres/sockets -U tim -d postgres`; for each of 19 databases it runs `CREATE
43
+ DATABASE "$db" OWNER tim` when missing and asserts `pg_get_userbyid(datdba) = tim`. It never
44
+ drops anything.
45
+
46
+ ## `name`
47
+
48
+ At most 63 UTF-8 bytes (`NAMEDATALEN` 64, `pg_config_manual.h` line 29), refused at plan.
49
+ Measured at `scansup.c`'s `truncate_identifier`: past that length the server clips the name with
50
+ `pg_mbcliplen` (byte-oriented, multibyte-aware) and raises only a `NOTICE`
51
+ (`ERRCODE_NAME_TOO_LONG`) — never an error. A create would silently succeed against the
52
+ truncated name, and the next plan's `WHERE datname = $1` (the name exactly as declared) would
53
+ find nothing and try to create it again, forever. Refusing at plan is the only way this stays a
54
+ one-time failure instead of an infinite loop.
55
+
56
+ ## Mapping (`database-provenance.test.ts` proves this against the committed fixtures)
57
+
58
+ | prop | `CREATE DATABASE` option | `pg_database` column |
59
+ | ------------------ | ------------------------ | --------------------------------------------------- |
60
+ | `owner` | `OWNER` | `datdba` (via `pg_get_userbyid`) |
61
+ | `encoding` | `ENCODING` | `encoding` |
62
+ | `localeProvider` | `LOCALE_PROVIDER` | `datlocprovider` |
63
+ | `lcCollate` | `LC_COLLATE` | `datcollate` |
64
+ | `lcCtype` | `LC_CTYPE` | `datctype` |
65
+ | `allowConnections` | `ALLOW_CONNECTIONS` | `datallowconn` |
66
+ | `connectionLimit` | `CONNECTION LIMIT` | `datconnlimit` |
67
+ | `isTemplate` | `IS_TEMPLATE` | `datistemplate` |
68
+ | `tablespace` | `TABLESPACE` | `dattablespace` (joined to `pg_tablespace.spcname`) |
69
+
70
+ `datlocprovider` is Postgres's internal 1-byte `"char"` type. Measured live: `@effect/sql-pg`
71
+ decodes it as raw bytes, not text, so the read query maps it with a `CASE` in SQL to the same
72
+ three words `pg_collation.h`'s `collprovider_name` returns for the same codes (`b`→`builtin`,
73
+ `i`→`icu`, `c`→`libc`) before it ever crosses the wire as JSON.
74
+
75
+ ### Excluded, and why
76
+
77
+ | option | reason |
78
+ | ------------------- | ---------------------------------------------------------------------------- |
79
+ | `TEMPLATE` | consumed only at create time to copy from; no column records it |
80
+ | `STRATEGY` | consumed only at create time (WAL_LOG/FILE_COPY); not stored |
81
+ | `LOCALE` | a shorthand for `LC_COLLATE`/`LC_CTYPE`; this family declares those directly |
82
+ | `BUILTIN_LOCALE` | folds into `datcollate`/`datctype` like `LOCALE`, no column of its own |
83
+ | `ICU_LOCALE` | `datlocale` exists, but deferred — all 24 live databases are libc |
84
+ | `ICU_RULES` | stored in `daticurules`, no ICU-provider consumer yet |
85
+ | `COLLATION_VERSION` | for `pg_upgrade`; normally omitted so Postgres computes it |
86
+ | `OID` | for `pg_upgrade`'s internal use only; not something a stack should assert |
87
+
88
+ ## Identifier quoting: why this file does not reuse `alchemy`'s `sql(value)`
89
+
90
+ `CREATE DATABASE` takes no bind parameters for any value, not even `OWNER` — measured at
91
+ `gram.y`: every `createdb_opt_item` value is `NumericOnly`, `opt_boolean_or_string` or the
92
+ keyword `DEFAULT`, none of which reaches `PARAM` (`$n`). Every value in this statement is
93
+ escaped by hand and run through `.unsafe()`.
94
+
95
+ The database `name` is a `ColId` (`gram.y` line 17327) — one token. Effect's own
96
+ `Statement.defaultEscape('"')`, what `sql(value)` compiles to, ALSO rewrites every `.` in a
97
+ value to `"."`, because it is built for a schema-qualified name like `schema.table`. A database
98
+ named `"my.app"` would come out `"my"."app"` — two tokens where the grammar parses one, a
99
+ syntax error this provider would cause. `quoteIdent` (`database-sql.ts`) is the single-token
100
+ form instead: quote, and double an embedded `"`. Nothing else.
101
+
102
+ Every other `WITH` value (`OWNER`, `TABLESPACE`, `ENCODING`, `LOCALE_PROVIDER`, `LC_COLLATE`,
103
+ `LC_CTYPE`) is a string literal in the grammar, not an identifier: `createdb_opt_item`'s
104
+ string branch is `opt_boolean_or_string → NonReservedWord_or_Sconst → NonReservedWord |
105
+ Sconst`. `quoteStringLiteral` wraps in `'` and doubles an embedded `'`; it never emits a
106
+ backslash, so it does not depend on which way `standard_conforming_strings` points (measured
107
+ `on` above, the PG18 default since 9.1).
108
+
109
+ ## House differences from S11/S14
110
+
111
+ `delete` refuses unconditionally (`PostgresDatabaseDropRefused`) and `defaultRemovalPolicy` is
112
+ `retain` — two independent reasons `DROP DATABASE` never runs through this provider. Most
113
+ databases this family will manage are adopted, not created from nothing; a destroy on data is
114
+ Tim's manual act over the socket, never an engine call.
115
+
116
+ ## Ownership
117
+
118
+ A database carries no ownership mark in `pg_database`. Per H1 (the house's Snippet-precedent
119
+ rule for marker-less APIs), `read` always answers `Unowned` for a match — every one of the
120
+ mini's 19 already-live databases needs `adopt(true)` in the consuming stack.
121
+
122
+ ## Unmeasured
123
+
124
+ Whether `ALTER DATABASE … REFRESH COLLATION VERSION` should ever be offered as an explicit,
125
+ separate operation (not part of `reconcile`) is open — nothing in this family calls it.
@@ -112,10 +112,46 @@ alertmanager, blackbox_exporter, node_exporter and postgres_exporter
112
112
  `darwin_arm64` to `darwin_all`), pyroscope and vector. `bao` (OpenBao) is
113
113
  walked — see `OPENBAO_RELEASES` above.
114
114
 
115
- ⚠️ REASONED NOT MEASURED: the Prometheus-family archives wrap their files in a
116
- directory, and `tar.ts` refuses a directory entry (and any PAX header) for the
117
- whole archive. Walking those down includes a reader change, not only a data
118
- set ([release-binary-upstream.md](./release-binary-upstream.md), gap 11).
115
+ ### The Prometheus family's shape, MEASURED 2026-09-23
116
+
117
+ ★ **Now measured, not reasoned.** All four Prometheus-family darwin-arm64
118
+ archives wrap every entry in one directory, so `tar.ts` refused each one
119
+ whole — before this unit, on `entry "<root>/" is a directory`. Walking them
120
+ down needed a reader change, not only a data set: `tarReader(wanted, root)`
121
+ now strips exactly one DECLARED leading directory, pinned as `archive.root`
122
+ (gap 11 below, and tar.ts's own header comment, carry the full detail).
123
+
124
+ Read at `2026-09-23T14:33Z` with `gh api repos/<owner>/<repo>/releases/tags/<tag>`,
125
+ then downloaded to a scratch directory (not executed) and re-hashed:
126
+
127
+ | package (darwin-arm64) | asset | size (B) | `digest` matches | wrapper directory (root) |
128
+ | ------------------------------------------------ | ---------------------------------------------- | ---------- | ----------------- | --------------------------------------- |
129
+ | `prometheus/alertmanager` v0.33.1 | `alertmanager-0.33.1.darwin-arm64.tar.gz` | 37,247,168 | yes (`a94fbe54…`) | `alertmanager-0.33.1.darwin-arm64` |
130
+ | `prometheus/blackbox_exporter` v0.28.0 | `blackbox_exporter-0.28.0.darwin-arm64.tar.gz` | 15,705,022 | yes (`ec6c70cc…`) | `blackbox_exporter-0.28.0.darwin-arm64` |
131
+ | `prometheus/node_exporter` v1.12.1 | `node_exporter-1.12.1.darwin-arm64.tar.gz` | 5,368,643 | yes (`35b246a3…`) | `node_exporter-1.12.1.darwin-arm64` |
132
+ | `prometheus-community/postgres_exporter` v0.20.1 | `postgres_exporter-0.20.1.darwin-arm64.tar.gz` | 10,072,235 | yes (`4dd2b9e7…`) | `postgres_exporter-0.20.1.darwin-arm64` |
133
+
134
+ `tar -tvzf` on each downloaded archive (nothing extracted) showed: entry 1 is
135
+ the wrapper (typeflag `5`, size 0, `runner`/`circleci` owned), then only
136
+ root-level regular files under it — the exporter binary, `LICENSE`, `NOTICE`,
137
+ and for alertmanager also `amtool` and `alertmanager.yml`. No PAX (`x`/`g`) or
138
+ GNU long-name (`L`/`K`) entries in any of the four. Re-run afterward with the
139
+ worktree's OWN `tarReader(wanted, root)` over the same downloaded bytes: every
140
+ archive now parses to completion and returns the named member at its full
141
+ pinned size (e.g. `alertmanager` → 47,058,626 bytes) — see
142
+ `tar-root.test.ts` and `binary-root.test.ts` for the same proof as committed
143
+ fixtures.
144
+
145
+ ⛔ **This unit adds the reader capability and its tests, not a new catalog
146
+ entry.** No `PinnedArchive` for alertmanager, blackbox_exporter,
147
+ node_exporter or postgres_exporter exists here yet — that is a follow-up data
148
+ module (its own walk-down of the checksum format: `sha256sums.txt`, archives
149
+ only, so member digests would be `computed` like OpenBao's) and its own PR.
150
+
151
+ ⚠️ **`vector` is NOT covered by this measurement or this reader change.**
152
+ Vector's queue entry (above) has its own archive shape, unmeasured here; do
153
+ not assume it also wraps in a single directory without walking it down
154
+ separately.
119
155
 
120
156
  ## What this resource is not for
121
157
 
@@ -1,7 +1,7 @@
1
1
  # Release binaries — every refusal
2
2
 
3
3
  Status: active
4
- Verified: 2026-09-22
4
+ Verified: 2026-09-23
5
5
 
6
6
  Everything `Release.Binary` refuses, by where it is refused. The guide is
7
7
  [release-binary.md](./release-binary.md). ★ One page per subject: the guide says
@@ -16,13 +16,15 @@ At plan (the probe and `diff`), before anything is touched: a pin that is
16
16
  missing or not a plain value (an Output pin would be a fetch; a first deploy
17
17
  is never diffed, so reconcile refuses it there, before any host call, from the
18
18
  props as declared); a repo that is not `owner/name`, or whose name is `.` or
19
- `..`; a tag, asset or name that is not one safe path segment; an asset that is
20
- not a `.tar.gz` or `.tgz` (a glob is not a name); a size that is not an
21
- integer from 1 byte to 1 GiB (it is allocated up front); a digest that is not
22
- 64 lower-case hex; a member that is absolute or climbs. Also a relative or
23
- unnormalised directory; a mode that is setuid, setgid, sticky, group- or
24
- world-writable, or not owner-readable and owner-executable; an owner or group
25
- that is not a valid name or id; a new pin at the same path.
19
+ `..`; a tag, asset, name or **root** that is not one safe path segment (`root`
20
+ added 2026-09-23: refuses `''`, `'.'`, `'..'` and anything with a `/`, same as
21
+ a tag or asset); an asset that is not a `.tar.gz` or `.tgz` (a glob is not a
22
+ name); a size that is not an integer from 1 byte to 1 GiB (it is allocated up
23
+ front); a digest that is not 64 lower-case hex; a member that is absolute or
24
+ climbs. Also a relative or unnormalised directory; a mode that is setuid,
25
+ setgid, sticky, group- or world-writable, or not owner-readable and
26
+ owner-executable; an owner or group that is not a valid name or id; a new pin
27
+ at the same path.
26
28
 
27
29
  ⚠️ **Not on a first deploy with `directory: dir.path`.** Alchemy neither
28
30
  probes nor diffs a create whose props hold an Output, so there every check
@@ -51,6 +53,18 @@ header checksum, uses base-256, is neither ustar nor GNU, a lone zero block,
51
53
  data after the end, a truncated archive; the declared member missing; the
52
54
  member's SHA-256 not its pin. Every one of these ends `Nothing was written.`
53
55
 
56
+ **With `archive.root` declared** (tar.ts, 2026-09-23 — the Prometheus family's
57
+ own shape, walked down for this): a second entry named `<root>/`; any entry
58
+ whose first path segment is not exactly `root` — `<root>-evil/x` is a
59
+ different segment, not a prefix match, and is refused the same as a plain
60
+ top-level file beside the wrapper; the `<root>/` entry itself being anything
61
+ but an empty (size-0) directory, including a hard link or symlink at that
62
+ exact name; and the declared root never appearing in the archive at all.
63
+ Every other refusal above — PAX, GNU long-name, `..`, links, devices, a
64
+ duplicate name — still applies to entries _inside_ the root, unchanged.
65
+ **Without `root`,** a directory entry (the wrapper included) is refused
66
+ exactly as it always was: byte-identical to a reader with no root support.
67
+
54
68
  After the write: a file that does not read back as declared (a create is
55
69
  removed). Errors are tagged (`BinaryRefused`, `DownloadFailed`,
56
70
  `ChecksumMismatch`, `ArchiveRefused`) and carry the house refusal sentence.
@@ -116,21 +116,39 @@ that upstream wants it.
116
116
  fails with `BinaryRefused`, and the provider would fail with the tag union
117
117
  instead of `lift()`'s `Error`. `error-channel.test.ts` fails the moment
118
118
  either changes, so this page moves with it.
119
- 11. **The tar reader accepts one measured layout.** Measured 2026-09-22 against
120
- `extractMembers`: macOS's default `tar czf` (bsdtar 3.5.3) writes a PAX `x`
121
- header before every entry (a sub-second `mtime` and the
122
- `com.apple.provenance` xattr), so even a one-file archive is refused whole.
123
- `--no-mac-metadata` still writes them; `--format ustar` passes. A wrapping
124
- directory entry is refused whole. Go's `archive/tar` writing root-level
125
- files passes. The house accepts this on purpose: the Victoria archives are
126
- flat, and homeflare-builds writes plain ustar. ⚠️ REASONED NOT MEASURED:
127
- the Prometheus family's archives (`alertmanager-0.34.1.darwin-arm64.tar.gz`)
128
- wrap their files in a directory, so the queue in
129
- release-binary-catalogs.md needs a reader change, not only a data set.
130
- Generic replacement: skip directory entries, and links that are not the
131
- declared member, because nothing is ever written from an entry name; read
132
- PAX records and refuse only a `path`, `linkpath` or `size` record. The
133
- member digest still guards the bytes.
119
+ 11. **The tar reader accepts two measured layouts: flat, and one declared
120
+ root.** Measured 2026-09-22 against `extractMembers`: macOS's default
121
+ `tar czf` (bsdtar 3.5.3) writes a PAX `x` header before every entry (a
122
+ sub-second `mtime` and the `com.apple.provenance` xattr), so even a
123
+ one-file archive is refused whole. `--no-mac-metadata` still writes them;
124
+ `--format ustar` passes. Go's `archive/tar` writing root-level files
125
+ passes. The house accepts this on purpose: the Victoria archives are flat,
126
+ and homeflare-builds writes plain ustar.
127
+ ★ **MEASURED 2026-09-23, no longer reasoned**: all four Prometheus-family
128
+ darwin-arm64 archives (`alertmanager-0.33.1.darwin-arm64.tar.gz` — the
129
+ estate pins **0.33.1**, not the 0.34.1 this page previously cited —
130
+ `blackbox_exporter-0.28.0`, `node_exporter-1.12.1`,
131
+ `postgres_exporter-0.20.1`) wrap every entry in exactly one directory
132
+ (typeflag `5`, size 0, first) and carry no PAX or GNU long-name entries
133
+ (release-binary-catalogs.md has the full table and commands). The house
134
+ chose the **narrow fix**, not the generic one below: `tarReader(wanted,
135
+ root)` accepts exactly one entry named `<root>/`, requires every other
136
+ name to begin with that exact segment, and strips the prefix before a name
137
+ is matched, listed or checked for a duplicate — refusing a second
138
+ directory entry, a `<root>-evil/x` sibling (a segment match, not a string
139
+ prefix), anything outside the root, and a `<root>/` that is not an empty
140
+ directory. Without `root` the refusal is unchanged: a directory entry, the
141
+ wrapper included, is still refused whole.
142
+ ⚠️ **The generic replacement below is still the more upstreamable shape**
143
+ and was NOT what the house built here: skip directory entries, and links
144
+ that are not the declared member, because nothing is ever written from an
145
+ entry name; read PAX records and refuse only a `path`, `linkpath` or
146
+ `size` record. The member digest still guards the bytes either way. The
147
+ house's narrower `root` prop is easier to review (a vendor's wrapper name
148
+ is pinned, not inferred) but does not reach an archive with no declared
149
+ wrapper, or more than one PAX record kind, the way the generic reader
150
+ would; a PR to upstream would need to pick one, and a house-only
151
+ consumption doesn't have to.
134
152
  12. **One source, one tag alphabet, other prop names.** `releaseUrl` fixes
135
153
  `https://github.com`. Upstream's GitHub family also reaches GitHub
136
154
  Enterprise (`normalizeGitHubBaseUrl`, `GitHub/BaseUrl.ts`). A tag must be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -60,10 +60,18 @@
60
60
  "types": "./dist/netbox/index.d.ts",
61
61
  "default": "./dist/netbox/index.js"
62
62
  },
63
+ "./postgres": {
64
+ "types": "./dist/postgres/index.d.ts",
65
+ "default": "./dist/postgres/index.js"
66
+ },
63
67
  "./linux": {
64
68
  "types": "./dist/linux/index.d.ts",
65
69
  "default": "./dist/linux/index.js"
66
70
  },
71
+ "./paperless": {
72
+ "types": "./dist/paperless/index.d.ts",
73
+ "default": "./dist/paperless/index.js"
74
+ },
67
75
  "./release": {
68
76
  "types": "./dist/release/index.d.ts",
69
77
  "default": "./dist/release/index.js"
@@ -72,6 +80,10 @@
72
80
  "types": "./dist/caddy/index.d.ts",
73
81
  "default": "./dist/caddy/index.js"
74
82
  },
83
+ "./litellm": {
84
+ "types": "./dist/litellm/index.d.ts",
85
+ "default": "./dist/litellm/index.js"
86
+ },
75
87
  "./verify": {
76
88
  "types": "./dist/verify/index.d.ts",
77
89
  "default": "./dist/verify/index.js"
@@ -84,6 +96,7 @@
84
96
  "peerDependencies": {
85
97
  "@distilled.cloud/cloudflare": "1.0.0-rc.12",
86
98
  "@effect/platform-node": "4.0.0-rc.115",
99
+ "@effect/sql-pg": "4.0.0-rc.115",
87
100
  "alchemy": "2.0.0-beta.79",
88
101
  "cloudflare": ">=4.5.0",
89
102
  "effect": "4.0.0-rc.115",