@secrefs/node 0.1.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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/index.cjs +804 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +581 -0
- package/dist/index.d.ts +581 -0
- package/dist/index.js +749 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.cjs +79 -0
- package/dist/parser.cjs.map +1 -0
- package/dist/parser.d.cts +41 -0
- package/dist/parser.d.ts +41 -0
- package/dist/parser.js +60 -0
- package/dist/parser.js.map +1 -0
- package/dist/secrefs.cjs +876 -0
- package/dist/secrefs.cjs.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/aws.ts","../src/providers/base.ts","../src/ttlCache.ts","../src/controlPlaneClient.ts","../src/providers/vault.ts","../src/providers/local.ts","../src/providers/bitwarden.ts","../src/parser.ts","../src/resolver.ts","../src/envFile.ts","../src/index.ts"],"sourcesContent":["import {\n GetSecretValueCommand,\n ListSecretsCommand,\n SecretsManagerClient,\n} from \"@aws-sdk/client-secrets-manager\";\nimport {\n BaseSecretProvider,\n errorMessage,\n extractField,\n type ProviderHealth,\n type SecretFetchRequest,\n} from \"./base.js\";\nimport { TtlCache } from \"../ttlCache.js\";\nimport {\n ControlPlaneClient,\n ControlPlaneRequestError,\n type ControlPlaneCredentialSource,\n type MintedAwsCredentials,\n} from \"../controlPlaneClient.js\";\n\nexport type { ControlPlaneCredentialSource };\n\nexport interface AwsProviderOptions {\n region?: string;\n /** Inject a pre-configured client (primarily for testing) - also wins\n * over `controlPlane` if both are set, since a test that supplies an\n * explicit client wants full control regardless of the mode. */\n client?: SecretsManagerClient;\n /**\n * Sources per-request AWS credentials from a running control plane\n * (docs/control-plane-design.md §7/§10) instead of the ambient default\n * credential chain. Every `fetchOne` call mints a fresh, request-scoped\n * credential via `sts:AssumeRole` on the control plane's side - see\n * `apps/control-plane/src/providers/awsSts.ts`. Mutually exclusive\n * with ambient auth in spirit (not enforced - `client` still wins if\n * both are given, for testing).\n */\n controlPlane?: ControlPlaneCredentialSource;\n /**\n * How long a fetched secret value may be reused, in milliseconds.\n * Defaults to 0 - every expansion re-fetches, so a rotated secret\n * reaches a long-running consumer without a redeploy. Raise it to\n * trade a bounded window of staleness for fewer round trips. See\n * ../ttlCache.ts.\n */\n cacheTtlMs?: number;\n}\n\n/**\n * AWS Secrets Manager provider. Two credential-sourcing modes:\n *\n * - **Ambient (default)**: the AWS SDK v3 default credential provider\n * chain - environment variables, shared config/credentials files,\n * ECS/EC2 instance metadata, or an assumed IAM role. No credentials\n * ever need to live in SecRefs configuration itself. One client is\n * built lazily and reused for the provider's lifetime.\n * - **Control-plane-sourced** (`controlPlane` option): a fresh,\n * request-scoped credential is minted per `fetchOne` call via the\n * control plane's `/v1/credentials/mint`, so a distinct\n * `SecretsManagerClient` is constructed per path rather than reused -\n * each one only ever has the narrow scope that one mint granted.\n *\n * Raw secret values fetched per-path are cached in memory for the lifetime\n * of the provider instance either way, so multiple `#field` references\n * against the same secret only cost one API call (and, in control-plane\n * mode, one mint).\n */\nexport class AwsSecretsManagerProvider extends BaseSecretProvider {\n readonly name = \"aws\";\n\n private readonly explicitClient?: SecretsManagerClient;\n private readonly region?: string;\n private readonly controlPlane?: ControlPlaneCredentialSource;\n private readonly controlPlaneClient?: ControlPlaneClient;\n private ambientClient: SecretsManagerClient | null = null;\n private readonly rawCache: TtlCache<string>;\n\n constructor(options: AwsProviderOptions = {}) {\n super();\n this.explicitClient = options.client;\n this.region = options.region;\n this.controlPlane = options.controlPlane;\n this.rawCache = new TtlCache<string>({ ttlMs: options.cacheTtlMs });\n if (this.controlPlane) {\n this.controlPlaneClient =\n this.controlPlane.client ??\n new ControlPlaneClient({ baseUrl: this.controlPlane.baseUrl, token: this.controlPlane.token });\n }\n }\n\n /** Resolves the `SecretsManagerClient` to use for one `path` - lazily\n * built and reused in ambient mode, freshly minted per call in\n * control-plane mode. `explicitClient` (test injection) always wins. */\n private async clientFor(path: string): Promise<SecretsManagerClient> {\n if (this.explicitClient) return this.explicitClient;\n\n if (this.controlPlane && this.controlPlaneClient) {\n const minted = await this.controlPlaneClient.mintCredential(this.controlPlane.alias, path);\n if (minted.provider !== \"aws\") {\n throw new Error(\n `control plane returned a \"${minted.provider}\" credential for alias \"${this.controlPlane.alias}\", ` +\n `expected \"aws\"`,\n );\n }\n return this.buildClientFromMintedCredentials(minted.credentials);\n }\n\n if (!this.ambientClient) this.ambientClient = new SecretsManagerClient({ region: this.region });\n return this.ambientClient;\n }\n\n private buildClientFromMintedCredentials(credentials: MintedAwsCredentials): SecretsManagerClient {\n return new SecretsManagerClient({\n region: this.region,\n credentials: {\n accessKeyId: credentials.accessKeyId,\n secretAccessKey: credentials.secretAccessKey,\n sessionToken: credentials.sessionToken,\n },\n });\n }\n\n private getRaw(path: string): Promise<string> {\n return this.rawCache.fetch(path, async () => {\n try {\n const client = await this.clientFor(path);\n const response = await client.send(new GetSecretValueCommand({ SecretId: path }));\n if (typeof response.SecretString === \"string\") {\n return response.SecretString;\n }\n if (response.SecretBinary) {\n return Buffer.from(response.SecretBinary as Uint8Array).toString(\"utf8\");\n }\n throw new Error(`secret \"${path}\" has no SecretString or SecretBinary payload`);\n } catch (err) {\n throw new Error(`could not fetch secret \"${path}\": ${errorMessage(err)}`);\n }\n });\n }\n\n async fetchOne(request: SecretFetchRequest): Promise<string> {\n const raw = await this.getRaw(request.path);\n return extractField(raw, request.field, { provider: this.name, path: request.path });\n }\n\n async healthCheck(): Promise<ProviderHealth> {\n try {\n if (this.controlPlane) {\n // A control-plane-sourced provider has no single ambient\n // credential to probe - health here means \"the control plane is\n // reachable and this token is accepted\", checked with a\n // deliberately-unresolvable synthetic path so this never mutates\n // anything or depends on any specific secret existing. A 403\n // (\"no grant authorizes...\") still proves reachability + auth\n // worked; only a network/5xx failure means unhealthy.\n const controlPlaneClient =\n this.controlPlane.client ??\n new ControlPlaneClient({ baseUrl: this.controlPlane.baseUrl, token: this.controlPlane.token });\n try {\n await controlPlaneClient.mintCredential(this.controlPlane.alias, \"__secrefs_health_check__\");\n } catch (err) {\n if (err instanceof ControlPlaneRequestError) {\n return { provider: this.name, ok: true, message: \"control plane reachable\" };\n }\n throw err;\n }\n return { provider: this.name, ok: true };\n }\n\n // A cheap, low-privilege call that proves both network reachability\n // and that the ambient credentials are valid enough to call the API.\n const client = await this.clientFor(\"__secrefs_health_check__\");\n await client.send(new ListSecretsCommand({ MaxResults: 1 }));\n return { provider: this.name, ok: true };\n } catch (err) {\n return { provider: this.name, ok: false, message: errorMessage(err) };\n }\n }\n}\n","/**\n * The provider contract every SecRefs backend (AWS, Vault, local, or a\n * custom one you bring yourself) implements. Providers never log, print,\n * or persist the values they return - that discipline is enforced by the\n * resolver and CLI layers above them, which only ever handle secret values\n * long enough to hand them to `process.env` or a spawned child process.\n */\n\nexport interface SecretFetchRequest {\n /** The provider-specific secret path/id, as written after `sec://<provider>/`. */\n path: string;\n /** Optional dot-notation field to extract from a JSON secret payload. */\n field?: string;\n}\n\nexport interface ProviderHealth {\n provider: string;\n ok: boolean;\n /** Human-readable diagnostic. Never contains secret material. */\n message?: string;\n}\n\nexport interface ISecretProvider {\n readonly name: string;\n\n /** Fetch and resolve a single secret reference to its final string value. */\n fetchOne(request: SecretFetchRequest): Promise<string>;\n\n /**\n * Fetch multiple secret references. Implementations may batch/dedupe\n * against the backend where possible; the default behavior (provided by\n * {@link BaseSecretProvider}) is concurrent individual fetches via\n * `Promise.allSettled`, surfacing the first failure with full context.\n */\n fetchBatch(requests: SecretFetchRequest[]): Promise<string[]>;\n\n /**\n * Lightweight reachability/auth probe used by `secrefs check`. Must never\n * throw for expected failure modes (bad credentials, unreachable host) -\n * those are reported via the returned {@link ProviderHealth}.\n */\n healthCheck(): Promise<ProviderHealth>;\n}\n\nexport class SecretFetchError extends Error {\n constructor(\n public readonly provider: string,\n public readonly path: string,\n cause: unknown,\n ) {\n super(`[${provider}] failed to fetch secret at \"${path}\": ${errorMessage(cause)}`);\n this.name = \"SecretFetchError\";\n }\n}\n\nexport abstract class BaseSecretProvider implements ISecretProvider {\n abstract readonly name: string;\n\n abstract fetchOne(request: SecretFetchRequest): Promise<string>;\n\n async fetchBatch(requests: SecretFetchRequest[]): Promise<string[]> {\n const settled = await Promise.allSettled(requests.map((r) => this.fetchOne(r)));\n return settled.map((result, i) => {\n const request = requests[i];\n if (result.status === \"fulfilled\") {\n return result.value;\n }\n throw new SecretFetchError(this.name, request?.path ?? \"<unknown>\", result.reason);\n });\n }\n\n abstract healthCheck(): Promise<ProviderHealth>;\n}\n\nexport function errorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n return String(err);\n}\n\n/**\n * Extracts a (possibly dot-nested) field from a JSON-encoded secret. If no\n * field is requested, the raw string is returned unchanged. Throws a plain\n * `Error` (never leaking the secret value itself) when the payload isn't\n * valid JSON or the field path doesn't resolve to a value.\n */\nexport function extractField(\n raw: string,\n field: string | undefined,\n context: { provider: string; path: string },\n): string {\n if (!field) return raw;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\n `[${context.provider}] secret at \"${context.path}\" is not JSON, cannot extract field \"${field}\"`,\n );\n }\n\n let current: unknown = parsed;\n for (const part of field.split(\".\")) {\n if (current === null || typeof current !== \"object\") {\n throw new Error(\n `[${context.provider}] field \"${field}\" not found in secret at \"${context.path}\"`,\n );\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n if (current === undefined) {\n throw new Error(\n `[${context.provider}] field \"${field}\" not found in secret at \"${context.path}\"`,\n );\n }\n\n return typeof current === \"object\" ? JSON.stringify(current) : String(current);\n}\n","/**\n * A cache that expires, used by every provider that fetches over the\n * network.\n *\n * The default TTL is **zero** — every read re-fetches. That's deliberate\n * and it's the whole point of the product: a `sec://` reference is a\n * stable name for a value that changes underneath it. A consumer holding\n * the reference is supposed to see a rotated secret without being\n * redeployed, and a cache with no expiry silently breaks exactly that.\n * Before this existed, a long-running process fetched once and held the\n * old value until restart.\n *\n * A non-zero TTL is a real tradeoff, not a mistake: every expansion is a\n * network round trip, so a busy caller may want to trade a bounded window\n * of staleness for latency and API-rate-limit headroom. `ttlMs: 30_000`\n * means \"a rotation reaches me within 30 seconds\" — usually fine, and it\n * should be a decision someone made rather than a default they inherited.\n */\nexport interface TtlCacheOptions {\n /** Milliseconds an entry stays fresh. `0` (default) disables caching\n * entirely - every `fetch` call goes to the source. */\n ttlMs?: number;\n /** Injected in tests so expiry doesn't require real waiting. */\n now?: () => number;\n}\n\ninterface Entry<T> {\n value: Promise<T>;\n storedAt: number;\n}\n\nexport class TtlCache<T> {\n /** Settled values, only populated when a TTL is configured. */\n private readonly entries = new Map<string, Entry<T>>();\n /** Requests currently in flight, tracked separately from `entries`\n * because coalescing and caching are different things: sharing an\n * unsettled request holds no value past the moment it resolves, so it\n * stays correct even with caching fully disabled. */\n private readonly inFlight = new Map<string, Promise<T>>();\n private readonly ttlMs: number;\n private readonly now: () => number;\n\n constructor(options: TtlCacheOptions = {}) {\n this.ttlMs = options.ttlMs ?? 0;\n this.now = options.now ?? Date.now;\n }\n\n /**\n * Returns the cached value for `key` if it's still fresh, otherwise\n * calls `load` and caches that. In-flight promises are shared, so N\n * concurrent expansions of the same reference make one request rather\n * than N even when the TTL is zero - that's request coalescing, not\n * caching, and it doesn't hold a value past its use.\n *\n * A rejected load is evicted rather than remembered, so a transient\n * failure doesn't become a sticky one.\n */\n async fetch(key: string, load: () => Promise<T>): Promise<T> {\n // Always join an in-flight request, whatever the TTL.\n const pendingExisting = this.inFlight.get(key);\n if (pendingExisting) return pendingExisting;\n\n const cached = this.entries.get(key);\n if (cached && this.ttlMs > 0 && this.now() - cached.storedAt < this.ttlMs) {\n return cached.value;\n }\n\n const pending = load();\n this.inFlight.set(key, pending);\n\n try {\n const value = await pending;\n // Only retain past settlement when a TTL was actually asked for.\n if (this.ttlMs > 0) {\n this.entries.set(key, { value: Promise.resolve(value), storedAt: this.now() });\n }\n return value;\n } catch (err) {\n // Never remember a failure - a transient outage shouldn't become\n // a sticky one for the length of the TTL.\n this.entries.delete(key);\n throw err;\n } finally {\n this.inFlight.delete(key);\n }\n }\n\n /** Drops everything - used when a credential changes underneath the\n * cache and anything fetched with the old one is suspect. */\n clear(): void {\n this.entries.clear();\n }\n}\n","/**\n * Thin HTTP client for a running control plane's credential-broker\n * endpoint (docs/control-plane-design.md §7). This is the piece §10\n * flagged as the missing link: every provider that supports\n * control-plane-sourced credentials (AwsSecretsManagerProvider,\n * BitwardenProvider - see their `controlPlane` constructor option)\n * constructs one of these instead of only ever reading ambient env vars.\n *\n * Deliberately just an HTTP wrapper with no retry/backoff/circuit-\n * breaking logic - a mint failure surfaces as a normal rejected promise,\n * same as any other provider fetch failure, and the caller's existing\n * error handling (resolver.ts's Promise.allSettled aggregation) already\n * does the right thing with that.\n */\n\nexport interface MintedAwsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken: string;\n /** ISO-8601 expiration timestamp. */\n expiration: string;\n}\n\nexport interface MintedBitwardenCredentials {\n accessToken: string;\n organizationId?: string;\n /** Explicitly not a TTL promise - see apps/control-plane/src/providers/bitwarden.ts. */\n note: string;\n}\n\nexport type MintCredentialResponse =\n | { provider: \"aws\"; credentials: MintedAwsCredentials }\n | { provider: \"bitwarden\"; credentials: MintedBitwardenCredentials };\n\n/** What a provider's `controlPlane` constructor option needs - shared\n * shape between `AwsSecretsManagerProvider` and `BitwardenProvider` (and\n * any future control-plane-aware provider). */\nexport interface ControlPlaneCredentialSource {\n /** Base URL of a running control plane, e.g. from $SECREFS_CONTROL_PLANE_URL. */\n baseUrl: string;\n /** Bootstrap token or a verified OIDC token, e.g. from $SECREFS_CONTROL_PLANE_TOKEN. */\n token: string;\n /** Which `VaultConnection` alias this provider instance represents -\n * this is what the control plane's RBAC grants are actually scoped\n * against, not the `sec://` alias this provider happens to be\n * registered under (though in practice they're usually the same\n * string). */\n alias: string;\n /** Injected for testing - defaults to a real `ControlPlaneClient`. */\n client?: ControlPlaneClient;\n}\n\nexport interface ControlPlaneClientOptions {\n /** Base URL of a running control plane, e.g. from $SECREFS_CONTROL_PLANE_URL. */\n baseUrl: string;\n /** Bootstrap token or a verified OIDC token, e.g. from $SECREFS_CONTROL_PLANE_TOKEN. */\n token: string;\n /** Injected for testing - defaults to the global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Thrown for a well-formed error response from the control plane (401,\n * 403, 502, ...) - `status` and `message` come straight from its `{ error }`\n * body, so a denial reason (e.g. \"no grant authorizes path...\") reaches\n * the caller verbatim rather than as an opaque HTTP failure. */\nexport class ControlPlaneRequestError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n ) {\n super(message);\n this.name = \"ControlPlaneRequestError\";\n }\n}\n\nexport class ControlPlaneClient {\n private readonly baseUrl: string;\n private readonly token: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(options: ControlPlaneClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n this.token = options.token;\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n /** Authenticates, authorizes, and resolves a credential for `alias`/`path`\n * - see the control plane's `POST /v1/credentials/mint`. Throws\n * `ControlPlaneRequestError` for any non-2xx response. */\n async mintCredential(alias: string, path: string): Promise<MintCredentialResponse> {\n let response: Response;\n try {\n response = await this.fetchImpl(`${this.baseUrl}/v1/credentials/mint`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", authorization: `Bearer ${this.token}` },\n body: JSON.stringify({ alias, path }),\n });\n } catch (err) {\n throw new Error(\n `could not reach control plane at ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as { error?: string };\n throw new ControlPlaneRequestError(\n response.status,\n body.error ?? `control plane returned ${response.status} for alias \"${alias}\" path \"${path}\"`,\n );\n }\n\n return (await response.json()) as MintCredentialResponse;\n }\n}\n","import vaultFactory from \"node-vault\";\nimport { TtlCache } from \"../ttlCache.js\";\nimport {\n BaseSecretProvider,\n errorMessage,\n extractField,\n type ProviderHealth,\n type SecretFetchRequest,\n} from \"./base.js\";\n\nexport interface VaultProviderOptions {\n /** Defaults to $VAULT_ADDR. */\n endpoint?: string;\n /** Defaults to $VAULT_TOKEN. */\n token?: string;\n /** Inject a pre-configured client (primarily for testing). */\n client?: ReturnType<typeof vaultFactory>;\n /** How long a fetched secret may be reused, in ms. Defaults to 0 -\n * every expansion re-fetches, so rotation reaches a long-running\n * consumer without a redeploy. See ../ttlCache.ts. */\n cacheTtlMs?: number;\n}\n\n/**\n * HashiCorp Vault provider supporting both KV v1 and KV v2 secrets engines.\n * Auth is ambient via `VAULT_ADDR`/`VAULT_TOKEN` - point `path` at whatever\n * the Vault HTTP API itself expects (KV v2 mounts include a literal `data/`\n * segment, e.g. `secret/data/stripe`; KV v1 mounts do not).\n *\n * The client is constructed lazily on first use so that simply having a\n * `VaultProvider` in your provider registry doesn't require Vault to be\n * configured if you never actually reference `sec://vault/...`.\n */\nexport class VaultProvider extends BaseSecretProvider {\n readonly name = \"vault\";\n\n private readonly explicitClient?: ReturnType<typeof vaultFactory>;\n private readonly endpoint?: string;\n private readonly token?: string;\n private client: ReturnType<typeof vaultFactory> | null = null;\n private readonly dataCache: TtlCache<Record<string, unknown>>;\n\n constructor(options: VaultProviderOptions = {}) {\n super();\n this.explicitClient = options.client;\n this.endpoint = options.endpoint ?? process.env.VAULT_ADDR;\n this.token = options.token ?? process.env.VAULT_TOKEN;\n this.dataCache = new TtlCache<Record<string, unknown>>({ ttlMs: options.cacheTtlMs });\n }\n\n private getClient(): ReturnType<typeof vaultFactory> {\n if (this.explicitClient) return this.explicitClient;\n if (this.client) return this.client;\n\n if (!this.endpoint) {\n throw new Error(\"VAULT_ADDR is not set (required for sec://vault/... references)\");\n }\n if (!this.token) {\n throw new Error(\"VAULT_TOKEN is not set (required for sec://vault/... references)\");\n }\n\n this.client = vaultFactory({ endpoint: this.endpoint, token: this.token });\n return this.client;\n }\n\n private getData(path: string): Promise<Record<string, unknown>> {\n return this.dataCache.fetch(path, () =>\n this.getClient()\n .read(path)\n .then((response) => {\n const outer = response.data;\n if (outer === undefined || outer === null) {\n throw new Error(`no data returned for path \"${path}\"`);\n }\n // KV v2 responses nest the secret under data.data alongside\n // data.metadata; KV v1 responses put the secret straight in data.\n if (\n typeof outer === \"object\" &&\n \"data\" in (outer as Record<string, unknown>) &&\n \"metadata\" in (outer as Record<string, unknown>)\n ) {\n return (outer as Record<string, unknown>).data as Record<string, unknown>;\n }\n return outer as Record<string, unknown>;\n })\n .catch((err: unknown) => {\n throw new Error(`could not read Vault path \"${path}\": ${errorMessage(err)}`);\n }),\n );\n }\n\n async fetchOne(request: SecretFetchRequest): Promise<string> {\n const data = await this.getData(request.path);\n\n if (!request.field) {\n const keys = Object.keys(data);\n // A single-key secret with no #field requested resolves to that\n // key's raw value directly (e.g. Vault's common { value: \"...\" }\n // convention); anything else is returned as a JSON blob.\n if (keys.length === 1) {\n const only = data[keys[0] as string];\n return typeof only === \"string\" ? only : JSON.stringify(only);\n }\n return JSON.stringify(data);\n }\n\n return extractField(JSON.stringify(data), request.field, {\n provider: this.name,\n path: request.path,\n });\n }\n\n async healthCheck(): Promise<ProviderHealth> {\n try {\n await this.getClient().health();\n return { provider: this.name, ok: true };\n } catch (err) {\n return { provider: this.name, ok: false, message: errorMessage(err) };\n }\n }\n}\n","import { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n BaseSecretProvider,\n type ProviderHealth,\n type SecretFetchRequest,\n extractField,\n} from \"./base.js\";\n\nconst DEFAULT_FILENAME = \".secrefs.local.json\";\n\nexport interface LocalProviderOptions {\n /** Overrides the file path. Defaults to $SECREFS_LOCAL_FILE or ./.secrefs.local.json */\n filePath?: string;\n /** Keep the parsed file in memory instead of re-reading per fetch.\n * Off by default so edits take effect immediately. */\n cacheFile?: boolean;\n}\n\n/**\n * Reads secrets from a gitignored, developer-local JSON file. Intended for\n * local development only - never point this at anything checked into\n * version control. Each top-level key is a secret path; its value is either\n * a plain string (returned as-is when no `#field` is requested) or an\n * object (JSON-stringified, then field-extracted as needed).\n *\n * Example `.secrefs.local.json`:\n * ```json\n * { \"mock-db\": { \"password\": \"hunter2\", \"user\": \"postgres\" } }\n * ```\n */\nexport class LocalProvider extends BaseSecretProvider {\n readonly name = \"local\";\n\n private readonly filePath: string;\n /** Re-read on every fetch. The file is local and tiny, and caching\n * it meant editing it mid-session silently did nothing. */\n private cache: Record<string, unknown> | null = null;\n private readonly cacheFile: boolean;\n\n constructor(options: LocalProviderOptions = {}) {\n super();\n this.filePath =\n options.filePath ??\n process.env.SECREFS_LOCAL_FILE ??\n path.join(process.cwd(), DEFAULT_FILENAME);\n this.cacheFile = options.cacheFile ?? false;\n }\n\n private async load(): Promise<Record<string, unknown>> {\n if (this.cache && this.cacheFile) return this.cache;\n\n let raw: string;\n try {\n raw = await readFile(this.filePath, \"utf8\");\n } catch (err) {\n throw new Error(\n `[local] could not read local secrets file at \"${this.filePath}\": ${\n err instanceof Error ? err.message : String(err)\n }. This file is gitignored by convention - see .secrefs.local.json in .gitignore.`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `[local] \"${this.filePath}\" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`[local] \"${this.filePath}\" must contain a top-level JSON object`);\n }\n\n this.cache = parsed as Record<string, unknown>;\n return this.cache;\n }\n\n async fetchOne(request: SecretFetchRequest): Promise<string> {\n const data = await this.load();\n if (!(request.path in data)) {\n throw new Error(`[local] no entry for path \"${request.path}\" in ${this.filePath}`);\n }\n\n const entry = data[request.path];\n const raw = typeof entry === \"string\" ? entry : JSON.stringify(entry);\n return extractField(raw, request.field, { provider: this.name, path: request.path });\n }\n\n async healthCheck(): Promise<ProviderHealth> {\n try {\n await this.load();\n return { provider: this.name, ok: true, message: this.filePath };\n } catch (err) {\n return {\n provider: this.name,\n ok: false,\n message: err instanceof Error ? err.message : String(err),\n };\n }\n }\n}\n","import { BitwardenClient } from \"@bitwarden/sdk-napi\";\nimport {\n BaseSecretProvider,\n errorMessage,\n extractField,\n type ProviderHealth,\n type SecretFetchRequest,\n} from \"./base.js\";\nimport { ControlPlaneClient, type ControlPlaneCredentialSource } from \"../controlPlaneClient.js\";\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The subset of `@bitwarden/sdk-napi`'s `BitwardenClient` this provider\n * calls - kept narrow so tests can inject a plain mock object instead of\n * a real client, the same pattern `VaultProvider`/`AwsSecretsManagerProvider`\n * use for their own SDK clients. */\nexport interface BitwardenClientLike {\n auth(): { loginAccessToken(accessToken: string, stateFile?: string): Promise<void> };\n secrets(): {\n get(id: string): Promise<{ value: string }>;\n list(organizationId: string): Promise<{ data: { id: string; key: string }[] }>;\n };\n}\n\nexport interface BitwardenProviderOptions {\n /** Defaults to $BWS_ACCESS_TOKEN. Ignored if `controlPlane` is set. */\n accessToken?: string;\n /** Required only to resolve a `path` given as a secret *name* rather\n * than its UUID (see class docs). Defaults to $BWS_ORGANIZATION_ID.\n * Ignored if `controlPlane` is set - the control plane's distributed\n * credential supplies this instead. */\n organizationId?: string;\n /** Self-hosted instance override. Defaults to $BWS_API_URL. */\n apiUrl?: string;\n /** Self-hosted instance override. Defaults to $BWS_IDENTITY_URL. */\n identityUrl?: string;\n /**\n * Opt-in path to an encrypted session-state file the SDK can reuse\n * across calls to reduce auth rate-limiting (Bitwarden's own docs\n * describe this file's contents as fully encrypted, not plaintext\n * secret material). Omitted by default - this provider re-authenticates\n * in memory each time it's constructed and writes nothing to disk\n * unless a caller opts in.\n */\n stateFile?: string;\n /**\n * Sources the access token/organizationId from a running control plane\n * (docs/control-plane-design.md §7/§10, and §8 for why Bitwarden's\n * distribution here isn't the same as AWS's per-request minting -\n * see apps/control-plane/src/providers/bitwarden.ts). Every `fetchOne`\n * call still requests a distribution for its specific `path`, so the\n * control plane's RBAC `Grant.path_pattern` is enforced per secret even\n * though the underlying Bitwarden token itself isn't scoped that\n * narrowly - \"SDK-side enforcement\" as documented on the control-plane\n * side.\n */\n controlPlane?: ControlPlaneCredentialSource;\n /** Inject a pre-configured client (primarily for testing). */\n client?: BitwardenClientLike;\n}\n\n/**\n * Bitwarden **Secrets Manager** provider (not the password vault - see\n * https://bitwarden.com/help/secrets-manager-overview/). Two structural\n * differences from `AwsSecretsManagerProvider`/`VaultProvider` worth\n * knowing before using this:\n *\n * 1. **Secrets are end-to-end encrypted.** There is no plain authenticated\n * REST call to fetch a value - the official SDK derives a decryption\n * key from the access token during login and decrypts client-side.\n * That's why this provider depends on `@bitwarden/sdk-napi` (a beta\n * Node-API binding maintained by Bitwarden) rather than a bare `fetch`.\n * 2. **Bitwarden addresses secrets by UUID, with no path hierarchy** the\n * way AWS/Vault secret names have. `path` may be that UUID directly, or\n * - if `organizationId` is configured (ambient mode) or supplied by the\n * control plane (control-plane mode) - a human-readable secret *name*\n * (Bitwarden's \"key\" field), resolved via one cached `secrets().list()`\n * call. With neither, only UUID paths work.\n */\nexport class BitwardenProvider extends BaseSecretProvider {\n readonly name = \"bitwarden\";\n\n private readonly explicitClient?: BitwardenClientLike;\n private readonly ambientAccessToken?: string;\n private readonly ambientOrganizationId?: string;\n private readonly apiUrl?: string;\n private readonly identityUrl?: string;\n private readonly stateFile?: string;\n private readonly controlPlane?: ControlPlaneCredentialSource;\n private readonly controlPlaneClient?: ControlPlaneClient;\n\n private client: BitwardenClientLike | null = null;\n private loggedInAccessToken: string | null = null;\n private loggedIn: Promise<void> | null = null;\n private organizationId: string | undefined;\n /** Secret name -> id, populated by one `list()` call the first time a\n * non-UUID path is requested. Invalidated if `organizationId` ever\n * changes (control-plane mode, defensively - static in practice). */\n private nameToId: Promise<Map<string, string>> | null = null;\n\n constructor(options: BitwardenProviderOptions = {}) {\n super();\n this.explicitClient = options.client;\n this.apiUrl = options.apiUrl ?? process.env.BWS_API_URL;\n this.identityUrl = options.identityUrl ?? process.env.BWS_IDENTITY_URL;\n this.stateFile = options.stateFile;\n this.controlPlane = options.controlPlane;\n\n if (this.controlPlane) {\n this.controlPlaneClient =\n this.controlPlane.client ??\n new ControlPlaneClient({ baseUrl: this.controlPlane.baseUrl, token: this.controlPlane.token });\n } else {\n this.ambientAccessToken = options.accessToken ?? process.env.BWS_ACCESS_TOKEN;\n this.ambientOrganizationId = options.organizationId ?? process.env.BWS_ORGANIZATION_ID;\n this.organizationId = this.ambientOrganizationId;\n }\n }\n\n private getClient(): BitwardenClientLike {\n if (this.explicitClient) return this.explicitClient;\n if (this.client) return this.client;\n this.client = new BitwardenClient({\n apiUrl: this.apiUrl,\n identityUrl: this.identityUrl,\n }) as unknown as BitwardenClientLike;\n return this.client;\n }\n\n private async loginWith(accessToken: string, organizationId: string | undefined): Promise<void> {\n if (organizationId !== this.organizationId) {\n this.nameToId = null; // stale cache keyed to a now-superseded org\n this.organizationId = organizationId;\n }\n if (this.loggedInAccessToken === accessToken && this.loggedIn) return this.loggedIn;\n\n this.loggedInAccessToken = accessToken;\n this.loggedIn = this.getClient()\n .auth()\n .loginAccessToken(accessToken, this.stateFile)\n .catch((err: unknown) => {\n this.loggedIn = null;\n this.loggedInAccessToken = null;\n throw new Error(`could not authenticate with the given access token: ${errorMessage(err)}`);\n });\n return this.loggedIn;\n }\n\n /** Ensures a session exists for `path`. Ambient mode logs in once\n * (memoized) with the ambient token; control-plane mode requests a\n * distribution for this specific `path` every call - see the\n * `controlPlane` option's docs for why that RBAC check has to be\n * per-path even though the token it returns doesn't vary. */\n private async ensureLoggedInFor(path: string): Promise<void> {\n if (!this.controlPlane) {\n if (!this.ambientAccessToken) {\n throw new Error(\"BWS_ACCESS_TOKEN is not set (required for sec://bitwarden/... references)\");\n }\n return this.loginWith(this.ambientAccessToken, this.ambientOrganizationId);\n }\n\n const minted = await this.controlPlaneClient!.mintCredential(this.controlPlane.alias, path);\n if (minted.provider !== \"bitwarden\") {\n throw new Error(\n `control plane returned a \"${minted.provider}\" credential for alias \"${this.controlPlane.alias}\", ` +\n `expected \"bitwarden\"`,\n );\n }\n return this.loginWith(minted.credentials.accessToken, minted.credentials.organizationId);\n }\n\n /** Assumes `ensureLoggedInFor(path)` has already run for this exact\n * `path` - callers always do that first, so `this.organizationId` is\n * already whatever this path's session resolved to. */\n private async resolveSecretId(path: string): Promise<string> {\n if (UUID_PATTERN.test(path)) return path;\n\n if (!this.organizationId) {\n throw new Error(\n `\"${path}\" is not a secret UUID, and no organizationId is available to look up a secret by name ` +\n `(set BWS_ORGANIZATION_ID, use the UUID directly, or - in control-plane mode - the distributed ` +\n `credential didn't include one)`,\n );\n }\n\n if (!this.nameToId) {\n const organizationId = this.organizationId;\n this.nameToId = (async () => {\n const { data } = await this.getClient().secrets().list(organizationId);\n return new Map(data.map((s) => [s.key, s.id]));\n })();\n }\n\n const map = await this.nameToId;\n const id = map.get(path);\n if (!id) {\n throw new Error(`no secret named \"${path}\" found in organization \"${this.organizationId}\"`);\n }\n return id;\n }\n\n async fetchOne(request: SecretFetchRequest): Promise<string> {\n let id: string;\n let secret: { value: string };\n try {\n // One ensureLoggedInFor per fetchOne - in control-plane mode this is\n // the one mint/RBAC-gate call for this path; resolveSecretId below\n // relies on it having already set this.organizationId.\n await this.ensureLoggedInFor(request.path);\n id = await this.resolveSecretId(request.path);\n secret = await this.getClient().secrets().get(id);\n } catch (err) {\n throw new Error(`could not fetch secret \"${request.path}\": ${errorMessage(err)}`);\n }\n return extractField(secret.value, request.field, { provider: this.name, path: request.path });\n }\n\n async healthCheck(): Promise<ProviderHealth> {\n try {\n await this.ensureLoggedInFor(\"__secrefs_health_check__\");\n return { provider: this.name, ok: true };\n } catch (err) {\n return { provider: this.name, ok: false, message: errorMessage(err) };\n }\n }\n}\n","/**\n * URI parser for SecRefs' `sec://` reference format:\n *\n * sec://<provider-alias>/<secret-path-or-id>[#<json-field>]\n *\n * sec://aws/prod/db#password\n * sec://vault/secret/data/stripe#key\n * sec://local/mock-db#password\n *\n * The provider alias is a bare identifier (letters/digits/`-`/`_`), the path\n * is opaque to this parser (providers interpret it however their backend\n * needs), and the optional `#field` fragment supports dot-notation for\n * traversing nested JSON secrets (e.g. `#nested.value`).\n */\n\nconst SEC_REF_PATTERN = /^sec:\\/\\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\\/([^\\s#]+)(?:#([^\\s#]+))?$/;\n\nexport interface ParsedSecretRef {\n /** The original, unmodified reference string. */\n raw: string;\n /** Lowercased provider alias, e.g. \"aws\", \"vault\", \"local\". */\n provider: string;\n /** The secret path/id as understood by the provider. */\n path: string;\n /** Optional dot-notation field to extract from a JSON secret. */\n field?: string;\n}\n\nexport class SecRefParseError extends Error {\n constructor(\n public readonly raw: string,\n public readonly reason: string,\n ) {\n super(`Invalid secret reference \"${raw}\": ${reason}`);\n this.name = \"SecRefParseError\";\n }\n}\n\n/** True if `value` is a string that looks like a `sec://` reference at all. */\nexport function isSecretRef(value: unknown): value is string {\n return typeof value === \"string\" && value.startsWith(\"sec://\");\n}\n\n/**\n * Parses a `sec://` reference string. Throws {@link SecRefParseError} if the\n * value isn't a string, doesn't start with `sec://`, or doesn't match the\n * full `<provider>/<path>[#field]` shape.\n */\nexport function parseSecretRef(raw: unknown): ParsedSecretRef {\n if (typeof raw !== \"string\") {\n throw new SecRefParseError(String(raw), \"reference must be a string\");\n }\n\n const trimmed = raw.trim();\n if (!trimmed.startsWith(\"sec://\")) {\n throw new SecRefParseError(raw, 'must start with \"sec://\"');\n }\n\n const match = SEC_REF_PATTERN.exec(trimmed);\n if (!match) {\n throw new SecRefParseError(\n raw,\n \"does not match sec://<provider>/<path>[#field] format\",\n );\n }\n\n const [, provider, path, field] = match;\n if (!provider) {\n throw new SecRefParseError(raw, \"missing provider alias\");\n }\n if (!path) {\n throw new SecRefParseError(raw, \"missing secret path\");\n }\n\n return {\n raw,\n provider: provider.toLowerCase(),\n path,\n field: field || undefined,\n };\n}\n\n/** Best-effort parse that returns `null` instead of throwing. */\nexport function tryParseSecretRef(raw: unknown): ParsedSecretRef | null {\n try {\n return parseSecretRef(raw);\n } catch {\n return null;\n }\n}\n","import { type ParsedSecretRef, isSecretRef, parseSecretRef } from \"./parser.js\";\nimport { errorMessage, type ISecretProvider, type SecretFetchRequest } from \"./providers/base.js\";\n\nexport type ProviderRegistry = Record<string, ISecretProvider>;\n\nexport interface ExpandOptions {\n providers: ProviderRegistry;\n /**\n * When true (default), a value that starts with `sec://` but fails to\n * parse throws immediately. When false, such values are left untouched.\n * This only affects syntactically malformed references - unknown\n * providers and provider-side fetch failures always surface as errors,\n * aggregated in a {@link SecRefsResolutionError}.\n */\n strict?: boolean;\n}\n\nexport interface ResolutionFailure {\n /** The env var / map key the reference was assigned to. */\n key: string;\n /** The original `sec://` string. */\n ref: string;\n message: string;\n}\n\nexport class SecRefsResolutionError extends Error {\n constructor(public readonly errors: ResolutionFailure[]) {\n super(\n `Failed to resolve ${errors.length} secret reference(s):\\n` +\n errors.map((e) => ` - ${e.key}: ${e.ref} -> ${e.message}`).join(\"\\n\"),\n );\n this.name = \"SecRefsResolutionError\";\n }\n}\n\nexport interface CheckResult {\n key: string;\n ref: string;\n provider: string;\n ok: boolean;\n /** Present only when ok is false. Never contains the secret value. */\n message?: string;\n}\n\nasync function resolveOne(\n ref: ParsedSecretRef,\n providers: ProviderRegistry,\n): Promise<string> {\n const provider = providers[ref.provider];\n if (!provider) {\n const available = Object.keys(providers).join(\", \") || \"none configured\";\n throw new Error(`unknown provider \"${ref.provider}\" (available: ${available})`);\n }\n const request: SecretFetchRequest = { path: ref.path, field: ref.field };\n return provider.fetchOne(request);\n}\n\n/**\n * Expands every `sec://` value in a plain key/value map, resolving all\n * references concurrently via `Promise.allSettled`. Non-reference values\n * pass through untouched. Never writes anything to disk - the caller\n * decides what to do with the returned map (assign to `process.env`,\n * template into a string, etc).\n *\n * Throws {@link SecRefsResolutionError} aggregating every failed\n * reference if any fail to resolve.\n */\nexport async function expandKeyValueMap(\n input: Record<string, string | undefined>,\n options: ExpandOptions,\n): Promise<Record<string, string>> {\n const strict = options.strict ?? true;\n const output: Record<string, string> = {};\n const pending: { key: string; ref: ParsedSecretRef }[] = [];\n\n for (const [key, value] of Object.entries(input)) {\n if (value === undefined) continue;\n if (!isSecretRef(value)) {\n output[key] = value;\n continue;\n }\n try {\n pending.push({ key, ref: parseSecretRef(value) });\n } catch (err) {\n if (strict) throw err;\n output[key] = value;\n }\n }\n\n if (pending.length === 0) {\n return output;\n }\n\n const settled = await Promise.allSettled(\n pending.map(({ ref }) => resolveOne(ref, options.providers)),\n );\n\n const errors: ResolutionFailure[] = [];\n settled.forEach((result, i) => {\n const { key, ref } = pending[i] as { key: string; ref: ParsedSecretRef };\n if (result.status === \"fulfilled\") {\n output[key] = result.value;\n } else {\n errors.push({ key, ref: ref.raw, message: errorMessage(result.reason) });\n }\n });\n\n if (errors.length > 0) {\n throw new SecRefsResolutionError(errors);\n }\n\n return output;\n}\n\n/**\n * Expands `sec://` values found in `process.env`, mutating it in place.\n * Returns the list of env var names that were rewritten.\n */\nexport async function expandProcessEnv(options: ExpandOptions): Promise<string[]> {\n const resolved = await expandKeyValueMap(process.env, options);\n const changedKeys: string[] = [];\n for (const [key, value] of Object.entries(resolved)) {\n if (process.env[key] !== value) {\n process.env[key] = value;\n changedKeys.push(key);\n }\n }\n return changedKeys;\n}\n\n/**\n * Dry-run validation: resolves every `sec://` reference found in `input`\n * but reports only ok/failure per reference - the secret values themselves\n * are never returned or logged. Used by `secrefs check`.\n */\nexport async function checkReferences(\n input: Record<string, string | undefined>,\n options: ExpandOptions,\n): Promise<CheckResult[]> {\n const results: CheckResult[] = [];\n const parsedEntries: { key: string; ref: ParsedSecretRef }[] = [];\n\n for (const [key, value] of Object.entries(input)) {\n if (value === undefined || !isSecretRef(value)) continue;\n try {\n parsedEntries.push({ key, ref: parseSecretRef(value) });\n } catch (err) {\n results.push({ key, ref: value, provider: \"unknown\", ok: false, message: errorMessage(err) });\n }\n }\n\n const settled = await Promise.allSettled(\n parsedEntries.map(({ ref }) => resolveOne(ref, options.providers)),\n );\n\n settled.forEach((result, i) => {\n const { key, ref } = parsedEntries[i] as { key: string; ref: ParsedSecretRef };\n results.push({\n key,\n ref: ref.raw,\n provider: ref.provider,\n ok: result.status === \"fulfilled\",\n message: result.status === \"rejected\" ? errorMessage(result.reason) : undefined,\n });\n });\n\n return results;\n}\n","import { parse as parseDotenv } from \"dotenv\";\n\n// Matches an *unquoted* `KEY=sec://...` assignment, capturing the rest of\n// the line verbatim as the value.\nconst UNQUOTED_SEC_REF_LINE =\n /^[ \\t]*(?:export[ \\t]+)?([A-Za-z_][A-Za-z0-9_]*)[ \\t]*=[ \\t]*(sec:\\/\\/\\S.*)$/;\n\n/**\n * dotenv treats `#` as a start-of-comment marker even mid-value (unless\n * the value is quoted), which silently truncates the `#field` fragment\n * off an unquoted `sec://provider/path#field` reference - the exact\n * format SecRefs itself uses. Rather than require every `sec://` value in\n * `.env` to be quoted (an easy thing to forget, with no error if you do),\n * this re-scans the raw file for unquoted `sec://` assignments and\n * restores the value dotenv would otherwise clip.\n *\n * Note: because of this, an unquoted `sec://` value can't have a\n * trailing inline comment on the same line - put comments on their own\n * line instead. Quoted values (`KEY=\"sec://...#field\"`) are unaffected\n * and already handled correctly by dotenv itself.\n */\nexport function recoverTruncatedSecRefs(\n rawText: string,\n parsed: Record<string, string>,\n): Record<string, string> {\n const result = { ...parsed };\n for (const line of rawText.split(/\\r?\\n/)) {\n const match = UNQUOTED_SEC_REF_LINE.exec(line);\n if (!match) continue;\n const [, key, value] = match;\n if (!key || value === undefined) continue;\n result[key] = value.trimEnd();\n }\n return result;\n}\n\n/** Parses a `.env` file's contents, correctly preserving `sec://...#field` fragments. */\nexport function parseEnvFileText(rawText: string): Record<string, string> {\n const parsed = parseDotenv(rawText);\n return recoverTruncatedSecRefs(rawText, parsed);\n}\n","import { AwsSecretsManagerProvider } from \"./providers/aws.js\";\nimport { VaultProvider } from \"./providers/vault.js\";\nimport { LocalProvider } from \"./providers/local.js\";\nimport { BitwardenProvider } from \"./providers/bitwarden.js\";\nimport { isSecretRef } from \"./parser.js\";\nimport {\n expandKeyValueMap,\n expandProcessEnv,\n checkReferences,\n type ExpandOptions,\n type ProviderRegistry,\n} from \"./resolver.js\";\n\nexport {\n parseSecretRef,\n tryParseSecretRef,\n isSecretRef,\n SecRefParseError,\n type ParsedSecretRef,\n} from \"./parser.js\";\n\nexport { parseEnvFileText, recoverTruncatedSecRefs } from \"./envFile.js\";\n\nexport { TtlCache, type TtlCacheOptions } from \"./ttlCache.js\";\n\nexport {\n BaseSecretProvider,\n SecretFetchError,\n extractField,\n type ISecretProvider,\n type SecretFetchRequest,\n type ProviderHealth,\n} from \"./providers/base.js\";\n\nexport { AwsSecretsManagerProvider, type AwsProviderOptions } from \"./providers/aws.js\";\nexport { VaultProvider, type VaultProviderOptions } from \"./providers/vault.js\";\nexport { LocalProvider, type LocalProviderOptions } from \"./providers/local.js\";\nexport {\n BitwardenProvider,\n type BitwardenProviderOptions,\n type BitwardenClientLike,\n} from \"./providers/bitwarden.js\";\n\nexport {\n ControlPlaneClient,\n ControlPlaneRequestError,\n type ControlPlaneClientOptions,\n type ControlPlaneCredentialSource,\n type MintCredentialResponse,\n type MintedAwsCredentials,\n type MintedBitwardenCredentials,\n} from \"./controlPlaneClient.js\";\n\nexport {\n expandKeyValueMap,\n expandProcessEnv,\n checkReferences,\n SecRefsResolutionError,\n type ExpandOptions,\n type ProviderRegistry,\n type ResolutionFailure,\n type CheckResult,\n} from \"./resolver.js\";\n\n/** Builds the default provider registry: aws, vault, local, bitwarden. */\nexport function createDefaultProviders(): ProviderRegistry {\n return {\n aws: new AwsSecretsManagerProvider(),\n vault: new VaultProvider(),\n local: new LocalProvider(),\n bitwarden: new BitwardenProvider(),\n };\n}\n\nexport interface SecRefsOptions {\n providers?: ProviderRegistry;\n strict?: boolean;\n}\n\n/**\n * The primary library entry point. Instantiate your own (with a custom\n * provider registry) or use the default `secRefs` singleton below.\n */\nexport class SecRefs {\n readonly providers: ProviderRegistry;\n readonly strict: boolean;\n\n constructor(options: SecRefsOptions = {}) {\n this.providers = options.providers ?? createDefaultProviders();\n this.strict = options.strict ?? true;\n }\n\n private get expandOptions(): ExpandOptions {\n return { providers: this.providers, strict: this.strict };\n }\n\n /**\n * Expands every `sec://` value found in `process.env`, mutating it in\n * place. Returns the list of env var names that were rewritten.\n */\n async init(): Promise<string[]> {\n return expandProcessEnv(this.expandOptions);\n }\n\n /**\n * Expands `sec://` values in an arbitrary key/value map (e.g. a parsed\n * `.env` file) without touching `process.env`.\n */\n async expandEnv(env: Record<string, string | undefined>): Promise<Record<string, string>> {\n return expandKeyValueMap(env, this.expandOptions);\n }\n\n /** Expands a single string if it's a `sec://` reference; otherwise returns it unchanged. */\n async expandString(value: string): Promise<string> {\n if (!isSecretRef(value)) return value;\n const resolved = await expandKeyValueMap({ __value__: value }, this.expandOptions);\n return resolved.__value__ as string;\n }\n\n /**\n * Dry-run validation of every `sec://` reference in `env` (defaults to\n * `process.env`). Never returns plaintext secret values.\n */\n async check(\n env: Record<string, string | undefined> = process.env,\n ): Promise<Awaited<ReturnType<typeof checkReferences>>> {\n return checkReferences(env, this.expandOptions);\n }\n}\n\n/** Convenience singleton mirroring `secRefs.init()` / `secRefs.expandEnv()` / `secRefs.expandString()`. */\nexport const secRefs = new SecRefs();\n"],"mappings":"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","names":["path","path","path","path","path","path"]}
|
package/dist/parser.cjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/parser.ts
|
|
23
|
+
var parser_exports = {};
|
|
24
|
+
__export(parser_exports, {
|
|
25
|
+
SecRefParseError: () => SecRefParseError,
|
|
26
|
+
isSecretRef: () => isSecretRef,
|
|
27
|
+
parseSecretRef: () => parseSecretRef,
|
|
28
|
+
tryParseSecretRef: () => tryParseSecretRef
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(parser_exports);
|
|
31
|
+
var SEC_REF_PATTERN = /^sec:\/\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/([^\s#]+)(?:#([^\s#]+))?$/;
|
|
32
|
+
var SecRefParseError = class extends Error {
|
|
33
|
+
constructor(raw, reason) {
|
|
34
|
+
super(`Invalid secret reference "${raw}": ${reason}`);
|
|
35
|
+
__publicField(this, "raw", raw);
|
|
36
|
+
__publicField(this, "reason", reason);
|
|
37
|
+
this.name = "SecRefParseError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function isSecretRef(value) {
|
|
41
|
+
return typeof value === "string" && value.startsWith("sec://");
|
|
42
|
+
}
|
|
43
|
+
function parseSecretRef(raw) {
|
|
44
|
+
if (typeof raw !== "string") {
|
|
45
|
+
throw new SecRefParseError(String(raw), "reference must be a string");
|
|
46
|
+
}
|
|
47
|
+
const trimmed = raw.trim();
|
|
48
|
+
if (!trimmed.startsWith("sec://")) {
|
|
49
|
+
throw new SecRefParseError(raw, 'must start with "sec://"');
|
|
50
|
+
}
|
|
51
|
+
const match = SEC_REF_PATTERN.exec(trimmed);
|
|
52
|
+
if (!match) {
|
|
53
|
+
throw new SecRefParseError(
|
|
54
|
+
raw,
|
|
55
|
+
"does not match sec://<provider>/<path>[#field] format"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const [, provider, path, field] = match;
|
|
59
|
+
if (!provider) {
|
|
60
|
+
throw new SecRefParseError(raw, "missing provider alias");
|
|
61
|
+
}
|
|
62
|
+
if (!path) {
|
|
63
|
+
throw new SecRefParseError(raw, "missing secret path");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
raw,
|
|
67
|
+
provider: provider.toLowerCase(),
|
|
68
|
+
path,
|
|
69
|
+
field: field || void 0
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function tryParseSecretRef(raw) {
|
|
73
|
+
try {
|
|
74
|
+
return parseSecretRef(raw);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=parser.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts"],"sourcesContent":["/**\n * URI parser for SecRefs' `sec://` reference format:\n *\n * sec://<provider-alias>/<secret-path-or-id>[#<json-field>]\n *\n * sec://aws/prod/db#password\n * sec://vault/secret/data/stripe#key\n * sec://local/mock-db#password\n *\n * The provider alias is a bare identifier (letters/digits/`-`/`_`), the path\n * is opaque to this parser (providers interpret it however their backend\n * needs), and the optional `#field` fragment supports dot-notation for\n * traversing nested JSON secrets (e.g. `#nested.value`).\n */\n\nconst SEC_REF_PATTERN = /^sec:\\/\\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\\/([^\\s#]+)(?:#([^\\s#]+))?$/;\n\nexport interface ParsedSecretRef {\n /** The original, unmodified reference string. */\n raw: string;\n /** Lowercased provider alias, e.g. \"aws\", \"vault\", \"local\". */\n provider: string;\n /** The secret path/id as understood by the provider. */\n path: string;\n /** Optional dot-notation field to extract from a JSON secret. */\n field?: string;\n}\n\nexport class SecRefParseError extends Error {\n constructor(\n public readonly raw: string,\n public readonly reason: string,\n ) {\n super(`Invalid secret reference \"${raw}\": ${reason}`);\n this.name = \"SecRefParseError\";\n }\n}\n\n/** True if `value` is a string that looks like a `sec://` reference at all. */\nexport function isSecretRef(value: unknown): value is string {\n return typeof value === \"string\" && value.startsWith(\"sec://\");\n}\n\n/**\n * Parses a `sec://` reference string. Throws {@link SecRefParseError} if the\n * value isn't a string, doesn't start with `sec://`, or doesn't match the\n * full `<provider>/<path>[#field]` shape.\n */\nexport function parseSecretRef(raw: unknown): ParsedSecretRef {\n if (typeof raw !== \"string\") {\n throw new SecRefParseError(String(raw), \"reference must be a string\");\n }\n\n const trimmed = raw.trim();\n if (!trimmed.startsWith(\"sec://\")) {\n throw new SecRefParseError(raw, 'must start with \"sec://\"');\n }\n\n const match = SEC_REF_PATTERN.exec(trimmed);\n if (!match) {\n throw new SecRefParseError(\n raw,\n \"does not match sec://<provider>/<path>[#field] format\",\n );\n }\n\n const [, provider, path, field] = match;\n if (!provider) {\n throw new SecRefParseError(raw, \"missing provider alias\");\n }\n if (!path) {\n throw new SecRefParseError(raw, \"missing secret path\");\n }\n\n return {\n raw,\n provider: provider.toLowerCase(),\n path,\n field: field || undefined,\n };\n}\n\n/** Best-effort parse that returns `null` instead of throwing. */\nexport function tryParseSecretRef(raw: unknown): ParsedSecretRef | null {\n try {\n return parseSecretRef(raw);\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,IAAM,kBAAkB;AAajB,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACkB,KACA,QAChB;AACA,UAAM,6BAA6B,GAAG,MAAM,MAAM,EAAE;AAHpC;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAGO,SAAS,YAAY,OAAiC;AAC3D,SAAO,OAAO,UAAU,YAAY,MAAM,WAAW,QAAQ;AAC/D;AAOO,SAAS,eAAe,KAA+B;AAC5D,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,IAAI,iBAAiB,OAAO,GAAG,GAAG,4BAA4B;AAAA,EACtE;AAEA,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,CAAC,QAAQ,WAAW,QAAQ,GAAG;AACjC,UAAM,IAAI,iBAAiB,KAAK,0BAA0B;AAAA,EAC5D;AAEA,QAAM,QAAQ,gBAAgB,KAAK,OAAO;AAC1C,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,CAAC,EAAE,UAAU,MAAM,KAAK,IAAI;AAClC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,iBAAiB,KAAK,wBAAwB;AAAA,EAC1D;AACA,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,iBAAiB,KAAK,qBAAqB;AAAA,EACvD;AAEA,SAAO;AAAA,IACL;AAAA,IACA,UAAU,SAAS,YAAY;AAAA,IAC/B;AAAA,IACA,OAAO,SAAS;AAAA,EAClB;AACF;AAGO,SAAS,kBAAkB,KAAsC;AACtE,MAAI;AACF,WAAO,eAAe,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URI parser for SecRefs' `sec://` reference format:
|
|
3
|
+
*
|
|
4
|
+
* sec://<provider-alias>/<secret-path-or-id>[#<json-field>]
|
|
5
|
+
*
|
|
6
|
+
* sec://aws/prod/db#password
|
|
7
|
+
* sec://vault/secret/data/stripe#key
|
|
8
|
+
* sec://local/mock-db#password
|
|
9
|
+
*
|
|
10
|
+
* The provider alias is a bare identifier (letters/digits/`-`/`_`), the path
|
|
11
|
+
* is opaque to this parser (providers interpret it however their backend
|
|
12
|
+
* needs), and the optional `#field` fragment supports dot-notation for
|
|
13
|
+
* traversing nested JSON secrets (e.g. `#nested.value`).
|
|
14
|
+
*/
|
|
15
|
+
interface ParsedSecretRef {
|
|
16
|
+
/** The original, unmodified reference string. */
|
|
17
|
+
raw: string;
|
|
18
|
+
/** Lowercased provider alias, e.g. "aws", "vault", "local". */
|
|
19
|
+
provider: string;
|
|
20
|
+
/** The secret path/id as understood by the provider. */
|
|
21
|
+
path: string;
|
|
22
|
+
/** Optional dot-notation field to extract from a JSON secret. */
|
|
23
|
+
field?: string;
|
|
24
|
+
}
|
|
25
|
+
declare class SecRefParseError extends Error {
|
|
26
|
+
readonly raw: string;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
constructor(raw: string, reason: string);
|
|
29
|
+
}
|
|
30
|
+
/** True if `value` is a string that looks like a `sec://` reference at all. */
|
|
31
|
+
declare function isSecretRef(value: unknown): value is string;
|
|
32
|
+
/**
|
|
33
|
+
* Parses a `sec://` reference string. Throws {@link SecRefParseError} if the
|
|
34
|
+
* value isn't a string, doesn't start with `sec://`, or doesn't match the
|
|
35
|
+
* full `<provider>/<path>[#field]` shape.
|
|
36
|
+
*/
|
|
37
|
+
declare function parseSecretRef(raw: unknown): ParsedSecretRef;
|
|
38
|
+
/** Best-effort parse that returns `null` instead of throwing. */
|
|
39
|
+
declare function tryParseSecretRef(raw: unknown): ParsedSecretRef | null;
|
|
40
|
+
|
|
41
|
+
export { type ParsedSecretRef, SecRefParseError, isSecretRef, parseSecretRef, tryParseSecretRef };
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URI parser for SecRefs' `sec://` reference format:
|
|
3
|
+
*
|
|
4
|
+
* sec://<provider-alias>/<secret-path-or-id>[#<json-field>]
|
|
5
|
+
*
|
|
6
|
+
* sec://aws/prod/db#password
|
|
7
|
+
* sec://vault/secret/data/stripe#key
|
|
8
|
+
* sec://local/mock-db#password
|
|
9
|
+
*
|
|
10
|
+
* The provider alias is a bare identifier (letters/digits/`-`/`_`), the path
|
|
11
|
+
* is opaque to this parser (providers interpret it however their backend
|
|
12
|
+
* needs), and the optional `#field` fragment supports dot-notation for
|
|
13
|
+
* traversing nested JSON secrets (e.g. `#nested.value`).
|
|
14
|
+
*/
|
|
15
|
+
interface ParsedSecretRef {
|
|
16
|
+
/** The original, unmodified reference string. */
|
|
17
|
+
raw: string;
|
|
18
|
+
/** Lowercased provider alias, e.g. "aws", "vault", "local". */
|
|
19
|
+
provider: string;
|
|
20
|
+
/** The secret path/id as understood by the provider. */
|
|
21
|
+
path: string;
|
|
22
|
+
/** Optional dot-notation field to extract from a JSON secret. */
|
|
23
|
+
field?: string;
|
|
24
|
+
}
|
|
25
|
+
declare class SecRefParseError extends Error {
|
|
26
|
+
readonly raw: string;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
constructor(raw: string, reason: string);
|
|
29
|
+
}
|
|
30
|
+
/** True if `value` is a string that looks like a `sec://` reference at all. */
|
|
31
|
+
declare function isSecretRef(value: unknown): value is string;
|
|
32
|
+
/**
|
|
33
|
+
* Parses a `sec://` reference string. Throws {@link SecRefParseError} if the
|
|
34
|
+
* value isn't a string, doesn't start with `sec://`, or doesn't match the
|
|
35
|
+
* full `<provider>/<path>[#field]` shape.
|
|
36
|
+
*/
|
|
37
|
+
declare function parseSecretRef(raw: unknown): ParsedSecretRef;
|
|
38
|
+
/** Best-effort parse that returns `null` instead of throwing. */
|
|
39
|
+
declare function tryParseSecretRef(raw: unknown): ParsedSecretRef | null;
|
|
40
|
+
|
|
41
|
+
export { type ParsedSecretRef, SecRefParseError, isSecretRef, parseSecretRef, tryParseSecretRef };
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/parser.ts
|
|
6
|
+
var SEC_REF_PATTERN = /^sec:\/\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/([^\s#]+)(?:#([^\s#]+))?$/;
|
|
7
|
+
var SecRefParseError = class extends Error {
|
|
8
|
+
constructor(raw, reason) {
|
|
9
|
+
super(`Invalid secret reference "${raw}": ${reason}`);
|
|
10
|
+
__publicField(this, "raw", raw);
|
|
11
|
+
__publicField(this, "reason", reason);
|
|
12
|
+
this.name = "SecRefParseError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function isSecretRef(value) {
|
|
16
|
+
return typeof value === "string" && value.startsWith("sec://");
|
|
17
|
+
}
|
|
18
|
+
function parseSecretRef(raw) {
|
|
19
|
+
if (typeof raw !== "string") {
|
|
20
|
+
throw new SecRefParseError(String(raw), "reference must be a string");
|
|
21
|
+
}
|
|
22
|
+
const trimmed = raw.trim();
|
|
23
|
+
if (!trimmed.startsWith("sec://")) {
|
|
24
|
+
throw new SecRefParseError(raw, 'must start with "sec://"');
|
|
25
|
+
}
|
|
26
|
+
const match = SEC_REF_PATTERN.exec(trimmed);
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new SecRefParseError(
|
|
29
|
+
raw,
|
|
30
|
+
"does not match sec://<provider>/<path>[#field] format"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const [, provider, path, field] = match;
|
|
34
|
+
if (!provider) {
|
|
35
|
+
throw new SecRefParseError(raw, "missing provider alias");
|
|
36
|
+
}
|
|
37
|
+
if (!path) {
|
|
38
|
+
throw new SecRefParseError(raw, "missing secret path");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
raw,
|
|
42
|
+
provider: provider.toLowerCase(),
|
|
43
|
+
path,
|
|
44
|
+
field: field || void 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function tryParseSecretRef(raw) {
|
|
48
|
+
try {
|
|
49
|
+
return parseSecretRef(raw);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
SecRefParseError,
|
|
56
|
+
isSecretRef,
|
|
57
|
+
parseSecretRef,
|
|
58
|
+
tryParseSecretRef
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts"],"sourcesContent":["/**\n * URI parser for SecRefs' `sec://` reference format:\n *\n * sec://<provider-alias>/<secret-path-or-id>[#<json-field>]\n *\n * sec://aws/prod/db#password\n * sec://vault/secret/data/stripe#key\n * sec://local/mock-db#password\n *\n * The provider alias is a bare identifier (letters/digits/`-`/`_`), the path\n * is opaque to this parser (providers interpret it however their backend\n * needs), and the optional `#field` fragment supports dot-notation for\n * traversing nested JSON secrets (e.g. `#nested.value`).\n */\n\nconst SEC_REF_PATTERN = /^sec:\\/\\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\\/([^\\s#]+)(?:#([^\\s#]+))?$/;\n\nexport interface ParsedSecretRef {\n /** The original, unmodified reference string. */\n raw: string;\n /** Lowercased provider alias, e.g. \"aws\", \"vault\", \"local\". */\n provider: string;\n /** The secret path/id as understood by the provider. */\n path: string;\n /** Optional dot-notation field to extract from a JSON secret. */\n field?: string;\n}\n\nexport class SecRefParseError extends Error {\n constructor(\n public readonly raw: string,\n public readonly reason: string,\n ) {\n super(`Invalid secret reference \"${raw}\": ${reason}`);\n this.name = \"SecRefParseError\";\n }\n}\n\n/** True if `value` is a string that looks like a `sec://` reference at all. */\nexport function isSecretRef(value: unknown): value is string {\n return typeof value === \"string\" && value.startsWith(\"sec://\");\n}\n\n/**\n * Parses a `sec://` reference string. Throws {@link SecRefParseError} if the\n * value isn't a string, doesn't start with `sec://`, or doesn't match the\n * full `<provider>/<path>[#field]` shape.\n */\nexport function parseSecretRef(raw: unknown): ParsedSecretRef {\n if (typeof raw !== \"string\") {\n throw new SecRefParseError(String(raw), \"reference must be a string\");\n }\n\n const trimmed = raw.trim();\n if (!trimmed.startsWith(\"sec://\")) {\n throw new SecRefParseError(raw, 'must start with \"sec://\"');\n }\n\n const match = SEC_REF_PATTERN.exec(trimmed);\n if (!match) {\n throw new SecRefParseError(\n raw,\n \"does not match sec://<provider>/<path>[#field] format\",\n );\n }\n\n const [, provider, path, field] = match;\n if (!provider) {\n throw new SecRefParseError(raw, \"missing provider alias\");\n }\n if (!path) {\n throw new SecRefParseError(raw, \"missing secret path\");\n }\n\n return {\n raw,\n provider: provider.toLowerCase(),\n path,\n field: field || undefined,\n };\n}\n\n/** Best-effort parse that returns `null` instead of throwing. */\nexport function tryParseSecretRef(raw: unknown): ParsedSecretRef | null {\n try {\n return parseSecretRef(raw);\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;AAeA,IAAM,kBAAkB;AAajB,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAC1C,YACkB,KACA,QAChB;AACA,UAAM,6BAA6B,GAAG,MAAM,MAAM,EAAE;AAHpC;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAGO,SAAS,YAAY,OAAiC;AAC3D,SAAO,OAAO,UAAU,YAAY,MAAM,WAAW,QAAQ;AAC/D;AAOO,SAAS,eAAe,KAA+B;AAC5D,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,IAAI,iBAAiB,OAAO,GAAG,GAAG,4BAA4B;AAAA,EACtE;AAEA,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,CAAC,QAAQ,WAAW,QAAQ,GAAG;AACjC,UAAM,IAAI,iBAAiB,KAAK,0BAA0B;AAAA,EAC5D;AAEA,QAAM,QAAQ,gBAAgB,KAAK,OAAO;AAC1C,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,CAAC,EAAE,UAAU,MAAM,KAAK,IAAI;AAClC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,iBAAiB,KAAK,wBAAwB;AAAA,EAC1D;AACA,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,iBAAiB,KAAK,qBAAqB;AAAA,EACvD;AAEA,SAAO;AAAA,IACL;AAAA,IACA,UAAU,SAAS,YAAY;AAAA,IAC/B;AAAA,IACA,OAAO,SAAS;AAAA,EAClB;AACF;AAGO,SAAS,kBAAkB,KAAsC;AACtE,MAAI;AACF,WAAO,eAAe,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|