| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Redacted from "effect/Redacted"; |
| 3 | import * as Provider from "./Provider.ts"; |
| 4 | import { Resource } from "./Resource.ts"; |
| 5 | |
| 6 | export interface RandomProps { |
| 7 | /** |
| 8 | * Number of random bytes to generate before hex encoding. |
| 9 | * @default 32 |
| 10 | */ |
| 11 | bytes?: number; |
| 12 | } |
| 13 | |
| 14 | export type Random = Resource< |
| 15 | "Alchemy.Random", |
| 16 | RandomProps, |
| 17 | { |
| 18 | text: Redacted.Redacted<string>; |
| 19 | } |
| 20 | >; |
| 21 | |
| 22 | export const makeRandom = (id: string, props?: RandomProps) => |
| 23 | Random(id, props).pipe(Effect.map((rand) => rand.text)); |
| 24 | |
| 25 | /** |
| 26 | * A deterministic-in-state random secret generator. |
| 27 | * |
| 28 | * The value is generated once on create and then persisted in state so |
| 29 | * subsequent deploys keep the same secret unless the resource is replaced. |
| 30 | */ |
| 31 | export const Random = Resource<Random>("Alchemy.Random"); |
| 32 | |
| 33 | export const RandomProvider = () => |
| 34 | Provider.succeed(Random, { |
| 35 | reconcile: ({ news = {}, output }) => |
| 36 | Effect.sync(() => { |
| 37 | // Observe — there is no remote state. The cached `output.text` is |
| 38 | // the authoritative current value; once minted it is preserved |
| 39 | // across reconciles to keep the secret stable. |
| 40 | if (output?.text) { |
| 41 | return output; |
| 42 | } |
| 43 | |
| 44 | // Ensure — no observed value: mint a fresh random secret and |
| 45 | // return it. The next reconcile will see this in `output` and |
| 46 | // short-circuit above. |
| 47 | const byteLength = news.bytes ?? 32; |
| 48 | const bytes = new Uint8Array(byteLength); |
| 49 | crypto.getRandomValues(bytes); |
| 50 | return { |
| 51 | text: Redacted.make( |
| 52 | Array.from(bytes) |
| 53 | .map((b) => b.toString(16).padStart(2, "0")) |
| 54 | .join(""), |
| 55 | ), |
| 56 | }; |
| 57 | }), |
| 58 | delete: () => Effect.void, |
| 59 | read: ({ output }) => Effect.succeed(output), |
| 60 | // Non-listable: the value is generated client-side with |
| 61 | // `crypto.getRandomValues` and lives only in alchemy state. There is no |
| 62 | // remote service to enumerate, so listing yields nothing. |
| 63 | list: () => Effect.succeed([]), |
| 64 | }); |