1import * as Cloudflare from "alchemy/Cloudflare";
2import type * as Config from "effect/Config";
3import * as Context from "effect/Context";
4import * as Duration from "effect/Duration";
5import type * as Redacted from "effect/Redacted";
6import * as Schema from "effect/Schema";
7
8/**
9 * What the registry Worker binds: its three resources, the settings binding
10 * that carries the policy into the isolate, and the credential bindings.
11 * Shared by `Registry.ts` (which declares the Worker) and the runtime
12 * modules in this folder.
13 */
14
15/**
16 * Registry policy. The `Type` side is what the Worker runs against
17 * (`Duration`, `ByteSize`); the `Encoded` side is plain JSON, which is how
18 * the policy crosses into the isolate as a Worker binding.
19 */
20export const Policy = Schema.Struct({
21 /**
22 * Repositories allowed to publish, as `owner/name`. A publication may
23 * contain any package; every package gets the commit, short commit,
24 * `branch:` and `pr:` tags of the run that produced it.
25 */
26 repos: Schema.Array(Schema.String),
27 /**
28 * How long a publication lives: from the push for branch publications,
29 * from close or merge for pull request publications. Publishing the same
30 * content again refreshes the clock.
31 */
32 ttl: Schema.DurationFromMillis,
33 /** Upper bound on a single tarball. Absent means unlimited. */
34 maxPackageSize: Schema.optionalKey(Schema.ByteSizeFromNumber),
35});
36export type Policy = typeof Policy.Type;
37
38/**
39 * Everything the Worker needs that is plain data. Encoded into the
40 * {@link SETTINGS_ENV} binding at deploy time and decoded back inside the
41 * isolate, so the deploy-time and runtime views of the registry are the
42 * same value by construction.
43 */
44export const Settings = Schema.Struct({
45 policy: Policy,
46 /**
47 * Hostname to package scope. Requests on an aliased host resolve an
48 * unscoped name under that scope, so `pkg.distilled.cloud/core/<sha>`
49 * serves `@distilled.cloud/core`.
50 */
51 aliases: Schema.Record(Schema.String, Schema.String),
52 /** Cron expression for the expiry sweep. */
53 cron: Schema.String,
54 github: Schema.Struct({
55 apiUrl: Schema.String,
56 }),
57});
58export type Settings = typeof Settings.Type;
59
60/**
61 * Worker props the registry forwards verbatim. `main`, `env`, and `crons`
62 * are owned by the registry itself.
63 */
64export type WorkerOverrides = Omit<
65 Cloudflare.WorkerProps,
66 "main" | "env" | "crons"
67>;
68
69export interface GitHubCredentials {
70 /** The App id. Read from the deploy environment, bound to the Worker. */
71 readonly appId: Config.Config<string>;
72 /** The App's private key PEM. Bound to the Worker as a secret. */
73 readonly privateKey: Config.Config<Redacted.Redacted<string>>;
74}
75
76/**
77 * The resolved registry configuration, provided to the Worker's Init phase.
78 * At plan time it is the value `PkgRegistry(id, options)` built from the
79 * caller's options; inside the isolate it is rebuilt from the Worker's
80 * bindings (see `RegistryConfigFromEnv` in `Registry.ts`). Either way the
81 * handler code sees one shape.
82 */
83export class RegistryConfig extends Context.Service<
84 RegistryConfig,
85 Settings & {
86 readonly worker: WorkerOverrides;
87 readonly github: Settings["github"] & GitHubCredentials;
88 }
89>()("@alchemy.run/pkg/RegistryConfig") {}
90
91/** Worker `json` binding carrying the encoded {@link Settings}. */
92export const SETTINGS_ENV = "PKG_SETTINGS";
93
94/** Worker env var the GitHub App id is bound under. */
95export const APP_ID_ENV = "PKG_GITHUB_APP_ID";
96
97/** Worker secret the GitHub App private key PEM is bound under. */
98export const PRIVATE_KEY_ENV = "PKG_GITHUB_APP_PRIVATE_KEY";
99
100export const DEFAULT_TTL = Duration.weeks(1);
101export const DEFAULT_CRON = "0 * * * *";
102export const DEFAULT_API_URL = "https://api.github.com";
103
104/** Marker that identifies the sticky install comment. */
105export const COMMENT_MARKER = "<!-- pkg-preview-comment -->";
106
107/** Uploaded tarballs no tag points at are deleted once older than this. */
108export const ORPHAN_GRACE = Duration.hours(24);
109
110/** Tags tied to pull requests are re-checked when due within this window. */
111export const SWEEP_LOOKAHEAD = Duration.hours(2);
112
113/** Content-addressed tarballs, keyed `<encoded name>/<sha256>.tgz`. */
114export const Bucket = Cloudflare.R2.Bucket("Bucket", { forceDestroy: true });
115
116export const tarballKey = (name: string, sha256: string) =>
117 `${encodeURIComponent(name)}/${sha256}.tgz`;
118
119// The migrations ship inside this package. `import.meta.url` is a file URL
120// during plan/deploy and absent or opaque inside the isolate, where the
121// resource declaration is only evaluated for its binding.
122const migrations =
123 typeof import.meta.url === "string" && import.meta.url.startsWith("file:")
124 ? decodeURIComponent(new URL("../../migrations", import.meta.url).pathname)
125 : undefined;
126
127/** Publications, tags, and tarball bookkeeping. */
128export const Database = Cloudflare.D1.Database("Database", { migrations });
129
130/**
131 * Resolved runs and installation tokens, shared by every isolate of the
132 * Worker. Everything in it is re-derivable from GitHub; see `KVCache.ts`.
133 */
134export const Cache = Cloudflare.KV.Namespace("Cache");