// el-secrets — Secrets management for el-ui. // // A Secret is a value with a redacted display contract. Source plugins // fetch by key from env, Vault, AWS Secrets Manager, or in-memory. // `SecretsResolver` composes a chain and resolves required keys at startup. // ── Errors ─────────────────────────────────────────────────────────────────── let SECRET_ERR_MISSING: String = "secret.missing" let SECRET_ERR_SOURCE: String = "secret.source_error" let SECRET_ERR_PERMISSION: String = "secret.permission_denied" // ── Source kinds ───────────────────────────────────────────────────────────── let SRC_ENV: String = "env" let SRC_VAULT: String = "vault" let SRC_AWS_SECRETS: String = "aws_secrets" let SRC_IN_MEMORY: String = "in_memory" // ── Source descriptor ─────────────────────────────────────────────────────── type SecretsSource { kind: String // env | vault | aws_secrets | in_memory config_json: String // source-specific (e.g. {"prefix":"EL_SECRET_"} or {"path":"secret/data/app"}) } fn source_env(prefix: String) -> SecretsSource { { "kind": "env", "config_json": "{\"prefix\":\"" + prefix + "\"}" } } fn source_vault(addr: String, token: String, mount: String) -> SecretsSource { let cfg: String = "{\"addr\":\"" + addr + "\",\"token\":\"" + token + "\",\"mount\":\"" + mount + "\"}" { "kind": "vault", "config_json": cfg } } fn source_aws(region: String) -> SecretsSource { { "kind": "aws_secrets", "config_json": "{\"region\":\"" + region + "\"}" } } fn source_in_memory() -> SecretsSource { { "kind": "in_memory", "config_json": "{\"map\":{}}" } } // ── Source lookup ─────────────────────────────────────────────────────────── // // Each source kind has its own fetch fn. `source_get` dispatches by kind. fn source_get(src: SecretsSource, key: String) -> String { if str_eq(src.kind, "env") { let prefix: String = json_get(src.config_json, "prefix") return env(prefix + str_to_upper(str_replace(key, ".", "_"))) } if str_eq(src.kind, "vault") { let addr: String = json_get(src.config_json, "addr") let token: String = json_get(src.config_json, "token") let mount: String = json_get(src.config_json, "mount") return vault_kv_get_at(addr, token, mount, key) } if str_eq(src.kind, "aws_secrets") { let region: String = json_get(src.config_json, "region") return aws_secrets_get(region, key) } if str_eq(src.kind, "in_memory") { let map: String = json_get(src.config_json, "map") return json_get(map, key) } "" } fn in_memory_insert(src: SecretsSource, key: String, value: String) -> SecretsSource { let map: String = json_get(src.config_json, "map") let map = json_set(map, key, "\"" + value + "\"") let cfg = json_set(src.config_json, "map", map) { "kind": src.kind, "config_json": cfg } } // ── Secret — redacted wrapper ──────────────────────────────────────────── // // A Secret stores its raw value behind a tag. `secret_display` always // returns `[REDACTED]`. `secret_expose` returns the actual string. // Serializers should call `secret_display` by default. type Secret { tag: String // marker: "secret" raw: String // the actual value — never log this directly } fn secret_new(value: String) -> Secret { { "tag": "secret", "raw": value } } fn secret_display(s: Secret) -> String { "[REDACTED]" } fn secret_expose(s: Secret) -> String { s.raw } // Disposable read-once guard — like Rust's `SecretGuard`. Consumes the secret // once and zeroes it. Useful at boundaries: read JWT signing key once, hand to // the signing routine, never readable again. type SecretGuard { consumed: Bool raw: String } fn guard_new(s: Secret) -> SecretGuard { { "consumed": false, "raw": s.raw } } fn guard_take(g: SecretGuard) -> String { if g.consumed { return "" } g.raw } // ── SecretsResolver — builder ─────────────────────────────────────────────── // // Sources are tried in order. First non-empty hit wins. // `require(key)` marks a key as required at startup; `resolve()` fails if any // required key is unresolved. type SecretsResolver { sources_json: String // JSON array of SecretsSource required_json: String // JSON array of required keys } fn resolver_new() -> SecretsResolver { { "sources_json": "[]", "required_json": "[]" } } fn resolver_source(r: SecretsResolver, src: SecretsSource) -> SecretsResolver { { "sources_json": json_array_push(r.sources_json, json_encode(src)), "required_json": r.required_json } } fn resolver_require(r: SecretsResolver, key: String) -> SecretsResolver { { "sources_json": r.sources_json, "required_json": json_array_push(r.required_json, "\"" + key + "\"") } } // Resolved bag. Keys -> Secret. Failure if any required key is missing. type ResolvedSecrets { map_json: String // JSON: key -> raw value errors: String // JSON array of missing required keys } fn resolver_resolve(r: SecretsResolver) -> ResolvedSecrets { let resolved: String = "{}" let errors: String = "[]" let n: Int = json_array_len(r.required_json) let i: Int = 0 while i < n { let key: String = json_array_get(r.required_json, i) let value: String = lookup_chain(r.sources_json, key) if str_eq(value, "") { let errors = json_array_push(errors, "\"" + key + "\"") } if !str_eq(value, "") { let resolved = json_set(resolved, key, "\"" + value + "\"") } let i = i + 1 } { "map_json": resolved, "errors": errors } } fn lookup_chain(sources_json: String, key: String) -> String { let n: Int = json_array_len(sources_json) let i: Int = 0 while i < n { let src_json: String = json_array_get(sources_json, i) let src: SecretsSource = { "kind": json_get(src_json, "kind"), "config_json": json_get(src_json, "config_json") } let v: String = source_get(src, key) if !str_eq(v, "") { return v } let i = i + 1 } "" } fn resolved_require(r: ResolvedSecrets, key: String) -> Secret { let raw: String = json_get(r.map_json, key) secret_new(raw) } // ── Entry — smoke test ────────────────────────────────────────────────────── let src: SecretsSource = source_in_memory() let src = in_memory_insert(src, "jwt.key", "test-key-for-testing-only") let resolver: SecretsResolver = resolver_source(resolver_new(), src) let resolver = resolver_require(resolver, "jwt.key") let resolved: ResolvedSecrets = resolver_resolve(resolver) let jwt: Secret = resolved_require(resolved, "jwt.key") println("[el-secrets] jwt.key = " + secret_display(jwt))