| 1 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 2 | import type * as Config from "effect/Config"; |
| 3 | import * as Context from "effect/Context"; |
| 4 | import * as Duration from "effect/Duration"; |
| 5 | import type * as Redacted from "effect/Redacted"; |
| 6 | import * 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 | */ |
| 20 | export 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 | }); |
| 36 | export 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 | */ |
| 44 | export 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 | }); |
| 58 | export 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 | */ |
| 64 | export type WorkerOverrides = Omit< |
| 65 | Cloudflare.WorkerProps, |
| 66 | "main" | "env" | "crons" |
| 67 | >; |
| 68 | |
| 69 | export 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 | */ |
| 83 | export 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}. */ |
| 92 | export const SETTINGS_ENV = "PKG_SETTINGS"; |
| 93 | |
| 94 | /** Worker env var the GitHub App id is bound under. */ |
| 95 | export const APP_ID_ENV = "PKG_GITHUB_APP_ID"; |
| 96 | |
| 97 | /** Worker secret the GitHub App private key PEM is bound under. */ |
| 98 | export const PRIVATE_KEY_ENV = "PKG_GITHUB_APP_PRIVATE_KEY"; |
| 99 | |
| 100 | export const DEFAULT_TTL = Duration.weeks(1); |
| 101 | export const DEFAULT_CRON = "0 * * * *"; |
| 102 | export const DEFAULT_API_URL = "https://api.github.com"; |
| 103 | |
| 104 | /** Marker that identifies the sticky install comment. */ |
| 105 | export const COMMENT_MARKER = "<!-- pkg-preview-comment -->"; |
| 106 | |
| 107 | /** Uploaded tarballs no tag points at are deleted once older than this. */ |
| 108 | export const ORPHAN_GRACE = Duration.hours(24); |
| 109 | |
| 110 | /** Tags tied to pull requests are re-checked when due within this window. */ |
| 111 | export const SWEEP_LOOKAHEAD = Duration.hours(2); |
| 112 | |
| 113 | /** Content-addressed tarballs, keyed `<encoded name>/<sha256>.tgz`. */ |
| 114 | export const Bucket = Cloudflare.R2.Bucket("Bucket", { forceDestroy: true }); |
| 115 | |
| 116 | export 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. |
| 122 | const 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. */ |
| 128 | export 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 | */ |
| 134 | export const Cache = Cloudflare.KV.Namespace("Cache"); |