@neon/env 0.14.1 → 0.15.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/README.md CHANGED
@@ -55,7 +55,7 @@ Both return the same namespaced `NeonEnv` shape: `postgres` is always present; `
55
55
  | Function | Description |
56
56
  | --- | --- |
57
57
  | `fetchEnv(config, { projectId, branch, ... })` | Async. Calls the Neon API for the given project + branch and returns live connection strings (and Auth/Data API values when enabled). `projectId` and `branch` are required; `branch` accepts a branch **name** (e.g. `main`) or a `br-…` id. (The legacy id-only `branchId` option still works.) Pass `keys` to fetch only some vars — see [Fetching a subset](#fetching-a-subset). Reads nothing from `process.env` or disk. |
58
- | `fetchEnvReusingSecrets(config, { projectId, branch, env })` | Async, from **`@neon/env/runtime`**. `fetchEnv` plus reuse of one-time secrets you already hold: verifies them against the branch, keeps what's valid, mints and revokes only when it must. Returns `{ vars, credential }`. Use this rather than `fetchEnv` anywhere the same branch is resolved repeatedly — see [The branch credential](#the-branch-credential). |
58
+ | `fetchEnvReusingSecrets(config, { projectId, branch, env })` | Async, from **`@neon/env/runtime`**. `fetchEnv` plus reuse of one-time secrets you already hold: verifies them against the branch, keeps what's valid, mints and revokes only when it must. Returns `{ vars, credential }`. Takes an optional `revokeSuperseded`, which keeps the replaced credential live when your call resolves only part of the branch. Use this rather than `fetchEnv` anywhere the same branch is resolved repeatedly — see [The branch credential](#the-branch-credential). |
59
59
  | `parseEnv(config)` / `parseEnv(config, slug)` / `parseEnv(config, keys)` | Sync. Reads/validates the Neon env vars already present in `process.env` against the static policy toggles. With a function `slug`, also returns a typed `function` namespace of that function's declared env keys. With a `keys` array (e.g. `["DATABASE_URL"]`), only those vars are required and returned, as a narrowed namespaced shape — the keys are typesafe against the policy. Throws `PlatformError(EnvNotInjected)` listing missing vars when the env isn't populated. |
60
60
  | `toEntries(env)` | Project a resolved `NeonEnv` into `{ KEY: value }` pairs for cross-process transport (named after the web `.entries()` convention; returns a `Record`). |
61
61
 
@@ -149,7 +149,8 @@ const { vars, credential } = await fetchEnvReusingSecrets(config, {
149
149
  // vars: { DATABASE_URL: "…", AWS_ACCESS_KEY_ID: "…", … } — ready to write or inject
150
150
  if (credential.issued) {
151
151
  console.log(`new values for ${credential.keys.join(", ")}`);
152
- // credential.revoked holds the token ids it superseded
152
+ // credential.revoked — ids it replaced and revoked
153
+ // credential.superseded — ids it replaced but left live (`revokeSuperseded: false`)
153
154
  }
154
155
  ```
155
156
 
@@ -157,6 +158,8 @@ The check is a real verification, not a presence test. A persisted secret is kep
157
158
 
158
159
  No local bookkeeping backs this: `AWS_ACCESS_KEY_ID` **is** the credential's token id, and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`, so the persisted secrets already name the credential that issued them.
159
160
 
161
+ Revoking is only safe because the call resolves everything the policy enables. Pass `revokeSuperseded: false` when yours resolves a **subset** — the credential your persisted secrets name may also back a service you are not resolving, and revoking it would break that service while its vars, which you are not rewriting, stay in place. The cost is an orphaned credential, which is the safer of the two failures, and `credential.superseded` names it so you can report it rather than leave it invisible. `neon env pull --service` is the caller this exists for.
162
+
160
163
  ### Fetching a subset
161
164
 
162
165
  `fetchEnv` takes a `keys` filter, the same typesafe selection `parseEnv` accepts:
@@ -23,6 +23,13 @@ interface CredentialOutcome {
23
23
  * secrets named *and* that this tool issued; empty otherwise.
24
24
  */
25
25
  revoked: string[];
26
+ /**
27
+ * `tokenId`s this call superseded but left live, because `revokeSuperseded` was `false`.
28
+ * The counterpart to {@link CredentialOutcome.revoked}: exactly the ids that would be
29
+ * there instead. Lets a caller name what it orphaned rather than saying that it might
30
+ * have orphaned something — always empty on the default path.
31
+ */
32
+ superseded: string[];
26
33
  }
27
34
  /** A resolved branch env, ready to write to a dotenv file or inject into a process. */
28
35
  interface ReusedBranchEnv {
@@ -70,6 +77,18 @@ declare function fetchEnvReusingSecrets<const C extends Config>(config: C, optio
70
77
  * `.env` file, typically. Defaults to `process.env`.
71
78
  */
72
79
  env?: NodeJS.ProcessEnv;
80
+ /**
81
+ * Revoke the credential a freshly-minted one supersedes. Defaults to `true`.
82
+ *
83
+ * Pass `false` when this resolve covers only *part* of what the branch has. Object
84
+ * storage and the AI Gateway share one credential, so a partial resolve cannot tell
85
+ * whether the credential its persisted secrets name also backs a service it is not
86
+ * resolving — and revoking it would kill that service while its vars, which this call
87
+ * is not rewriting, stay on disk and stop working. The cost is an orphaned credential,
88
+ * which is the safer of the two failures. `neon env pull --service` is the caller that
89
+ * needs this.
90
+ */
91
+ revokeSuperseded?: boolean;
73
92
  }): Promise<ReusedBranchEnv>;
74
93
  //#endregion
75
94
  export { CredentialOutcome, ReusedBranchEnv, fetchEnvReusingSecrets };
@@ -1 +1 @@
1
- {"version":3,"file":"reuse-secrets.d.ts","names":[],"sources":["../../src/lib/reuse-secrets.ts"],"mappings":";;;;;;;;AAsBiB,UAAA,iBAAA,CAAiB;EAoBjB;AAAe;AAEzB;AAEM;AAAiB;EA2CR,MAAA,EAAA,OAAA;EAAsB;AAAiB;AACpD;AACC;EAKF,IAAA,EAAA,MAAO,EAAA;EAEJ;AAAR;AAAO;;;;;UAxDO,eAAA;;QAEV;;cAEM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2CS,uCAAuC,gBACpD,YACC;;;;;QAKF,MAAA,CAAO;IAEZ,QAAQ"}
1
+ {"version":3,"file":"reuse-secrets.d.ts","names":[],"sources":["../../src/lib/reuse-secrets.ts"],"mappings":";;;;;;;;AAsBiB,UAAA,iBAAA,CAAiB;EA2BjB;AAAe;AAEzB;AAEM;AAAiB;EA2CR,MAAA,EAAA,OAAA;EAAsB;AAAiB;AACpD;AACC;EAKF,IAAA,EAAA,MAAO,EAAA;EAcJ;AAAR;AAAO;;;;;;;;;;;;UApEO,eAAA;;QAEV;;cAEM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2CS,uCAAuC,gBACpD,YACC;;;;;QAKF,MAAA,CAAO;;;;;;;;;;;;;IAcZ,QAAQ"}
@@ -35,7 +35,7 @@ import { credentialScopesSatisfied } from "@neon/config/v1";
35
35
  * ```
36
36
  */
37
37
  async function fetchEnvReusingSecrets(config, options) {
38
- const { env: source = process.env, ...fetchOptions } = options;
38
+ const { env: source = process.env, revokeSuperseded = true, ...fetchOptions } = options;
39
39
  const api = options.api ?? createApiFromOptions(options);
40
40
  const { branch, desired } = await resolveBranchPolicy(config, options, api);
41
41
  const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
@@ -51,7 +51,8 @@ async function fetchEnvReusingSecrets(config, options) {
51
51
  credential: {
52
52
  issued: false,
53
53
  keys: [],
54
- revoked: []
54
+ revoked: [],
55
+ superseded: []
55
56
  }
56
57
  };
57
58
  }
@@ -85,19 +86,21 @@ async function fetchEnvReusingSecrets(config, options) {
85
86
  credential: {
86
87
  issued: false,
87
88
  keys: secretKeys,
88
- revoked: []
89
+ revoked: [],
90
+ superseded: []
89
91
  }
90
92
  };
91
93
  }
92
94
  const ours = /* @__PURE__ */ new Set();
93
95
  for (const meta of [named.storage, named.gateway]) if (meta !== null && meta.principalType === "user" && meta.name === credentialName(branch.name)) ours.add(meta.tokenId);
94
- for (const tokenId of ours) await api.revokeCredential(options.projectId, branch.id, tokenId);
96
+ if (revokeSuperseded) for (const tokenId of ours) await api.revokeCredential(options.projectId, branch.id, tokenId);
95
97
  return {
96
98
  vars,
97
99
  credential: {
98
100
  issued: true,
99
101
  keys: secretKeys,
100
- revoked: [...ours]
102
+ revoked: revokeSuperseded ? [...ours] : [],
103
+ superseded: revokeSuperseded ? [] : [...ours]
101
104
  }
102
105
  };
103
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"reuse-secrets.js","names":[],"sources":["../../src/lib/reuse-secrets.ts"],"sourcesContent":["import {\n\ttype Config,\n\tcredentialScopesSatisfied,\n\ttype NeonCredentialMeta,\n} from \"@neon/config/v1\";\n\nimport {\n\tcreateApiFromOptions,\n\tcredentialEnvKeys,\n\tcredentialName,\n\ttype FetchEnvOptions,\n\tfetchEnvKeys,\n\tNEON_ENV_VAR_KEYS,\n\tpolicyEnvKeys,\n\tpreviewCredentialScopes,\n\tresolveBranchPolicy,\n\ttoEntries,\n} from \"./env.js\";\n\n/**\n * What happened to the branch credential during a {@link fetchEnvReusingSecrets} call.\n */\nexport interface CredentialOutcome {\n\t/**\n\t * `true` when a new credential was minted — because none was persisted, or because the\n\t * persisted secrets could not be verified against this branch. `false` when the persisted\n\t * secrets were verified and kept, and when the policy enables nothing credential-backed.\n\t */\n\tissued: boolean;\n\t/**\n\t * The env-var keys the branch credential's secrets surface under, given what the policy\n\t * enables. Empty when the policy enables neither object storage nor the AI Gateway.\n\t */\n\tkeys: string[];\n\t/**\n\t * `tokenId`s revoked because this call superseded them. Only ever credentials the persisted\n\t * secrets named *and* that this tool issued; empty otherwise.\n\t */\n\trevoked: string[];\n}\n\n/** A resolved branch env, ready to write to a dotenv file or inject into a process. */\nexport interface ReusedBranchEnv {\n\t/** Every Neon env var for the branch, as `{ KEY: value }`. */\n\tvars: Record<string, string>;\n\t/** What happened to the branch credential. */\n\tcredential: CredentialOutcome;\n}\n\n/** The branch credential's secrets as persisted in an env source. Empty string means absent. */\ninterface PersistedSecrets {\n\taccessKeyId: string;\n\tsecretAccessKey: string;\n\tapiToken: string;\n}\n\n/**\n * Resolve a branch's env while keeping one-time secrets the caller already holds.\n *\n * {@link fetchEnvKeys} — and the public `fetchEnv` — only ever *fetch*. The Neon API returns a\n * credential's `api_token` / `s3_secret_access_key` exactly once, at mint time, so \"fetching\"\n * them means minting a new credential; a plain `fetchEnv` on every `neon dev` start or `env\n * pull` would leave a live credential behind each time. This is the wrapper that avoids that:\n * it looks at what the caller already has, decides what is still usable, and asks `fetchEnv`\n * for only the rest.\n *\n * The check is a real verification, not a presence test. A persisted secret is kept only when\n * it names a credential that still exists on this branch, is not revoked or expired, and\n * carries every scope the policy needs. A `.env.example` placeholder, a credential revoked in\n * the console, one copied in from another branch, or one predating a newly-enabled feature all\n * fail that check and get replaced.\n *\n * None of this needs local bookkeeping, because the secrets carry their own credential id:\n * `AWS_ACCESS_KEY_ID` **is** the credential's `tokenId` (the storage gateway authenticates\n * against the full id), and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`,\n * where `tokenIdShort` is what the credentials list reports. The env source being replaced is\n * the record of what the last call issued.\n *\n * ```ts\n * import { fetchEnvReusingSecrets } from \"@neon/env/runtime\";\n *\n * const { vars, credential } = await fetchEnvReusingSecrets(config, {\n * projectId,\n * branch: \"main\",\n * env: { ...process.env, ...readEnvFile(\".env\") },\n * });\n * if (credential.issued) console.log(`new values for ${credential.keys.join(\", \")}`);\n * ```\n */\nexport async function fetchEnvReusingSecrets<const C extends Config>(\n\tconfig: C,\n\toptions: FetchEnvOptions & {\n\t\t/**\n\t\t * Env source holding secrets a previous call persisted — `process.env` layered with a\n\t\t * `.env` file, typically. Defaults to `process.env`.\n\t\t */\n\t\tenv?: NodeJS.ProcessEnv;\n\t},\n): Promise<ReusedBranchEnv> {\n\tconst { env: source = process.env, ...fetchOptions } = options;\n\tconst api = options.api ?? createApiFromOptions(options);\n\tconst { branch, desired } = await resolveBranchPolicy(config, options, api);\n\n\tconst storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;\n\tconst gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;\n\tconst secretKeys = credentialEnvKeys({\n\t\tstorage: storageEnabled,\n\t\taiGateway: gatewayEnabled,\n\t});\n\n\t// Nothing credential-backed on this branch, so there is nothing to preserve and no\n\t// credential to spend: fetch everything and skip the credentials endpoint entirely.\n\tif (secretKeys.length === 0) {\n\t\tconst fetched = await fetchEnvKeys(config, fetchOptions, null);\n\t\treturn {\n\t\t\tvars: preferPersisted(toEntries(fetched), source),\n\t\t\tcredential: { issued: false, keys: [], revoked: [] },\n\t\t};\n\t}\n\n\tconst persisted = readPersistedSecrets(source);\n\tconst complete =\n\t\t(!storageEnabled ||\n\t\t\tBoolean(persisted.accessKeyId && persisted.secretAccessKey)) &&\n\t\t(!gatewayEnabled || Boolean(persisted.apiToken));\n\n\t// Look the persisted secrets up whenever there are any — not only when they're complete.\n\t// An incomplete set still names the credential a newly-enabled feature is about to\n\t// supersede (a storage-only credential on a branch that just gained the AI Gateway), and\n\t// that one should be revoked rather than left live.\n\tconst named =\n\t\tpersisted.accessKeyId !== \"\" || persisted.apiToken !== \"\"\n\t\t\t? namedCredentials(\n\t\t\t\t\tawait api.listCredentials(options.projectId, branch.id),\n\t\t\t\t\tpersisted,\n\t\t\t\t)\n\t\t\t: { storage: null, gateway: null };\n\n\tconst reusable = complete\n\t\t? reusableCredential(named, { storageEnabled, gatewayEnabled })\n\t\t: null;\n\tconst scopes = previewCredentialScopes(desired.preview);\n\tconst keep =\n\t\treusable !== null && credentialScopesSatisfied(reusable.scopes, scopes);\n\n\t// Ask for everything the policy produces, minus the secrets we're keeping — which is what\n\t// stops `fetchEnv` from minting a credential it doesn't need.\n\tconst allKeys = policyEnvKeys(desired);\n\tconst fetchKeys = keep\n\t\t? allKeys.filter((key) => !secretKeys.includes(key))\n\t\t: allKeys;\n\tconst fetched = await fetchEnvKeys(\n\t\tconfig,\n\t\t// Pass the resolved id so `fetchEnv` targets the same branch this call verified against,\n\t\t// even if `options.branch` was a name that has since been reused.\n\t\t{ ...fetchOptions, branchId: branch.id, api },\n\t\tfetchKeys,\n\t);\n\n\tconst vars = preferPersisted(toEntries(fetched), source);\n\tif (keep) {\n\t\tfor (const key of secretKeys) {\n\t\t\tconst value = source[key];\n\t\t\tif (value !== undefined) vars[key] = value;\n\t\t}\n\t\treturn {\n\t\t\tvars,\n\t\t\tcredential: { issued: false, keys: secretKeys, revoked: [] },\n\t\t};\n\t}\n\n\t// A replacement was minted, so revoke what it supersedes: the credentials the old secrets\n\t// named, minus any this tool did not issue. Their secrets lived nowhere but the env source\n\t// this call replaces, so revoking them strands nothing — and it keeps a branch from\n\t// accumulating a live credential per call. Everything else on the branch is left alone: it\n\t// may belong to a teammate, another checkout, or a deployed function, and nothing\n\t// observable distinguishes those from an orphan of our own.\n\t//\n\t// Revoked *after* the fetch, so a failed fetch leaves the caller's existing secrets working.\n\tconst ours = new Set<string>();\n\tfor (const meta of [named.storage, named.gateway]) {\n\t\tif (\n\t\t\tmeta !== null &&\n\t\t\tmeta.principalType === \"user\" &&\n\t\t\tmeta.name === credentialName(branch.name)\n\t\t) {\n\t\t\tours.add(meta.tokenId);\n\t\t}\n\t}\n\tfor (const tokenId of ours) {\n\t\tawait api.revokeCredential(options.projectId, branch.id, tokenId);\n\t}\n\n\treturn {\n\t\tvars,\n\t\tcredential: { issued: true, keys: secretKeys, revoked: [...ours] },\n\t};\n}\n\n/** Read the branch credential's secrets out of an env source. */\nfunction readPersistedSecrets(source: NodeJS.ProcessEnv): PersistedSecrets {\n\tconst storage = NEON_ENV_VAR_KEYS.storage;\n\tconst gateway = NEON_ENV_VAR_KEYS.aiGateway;\n\treturn {\n\t\taccessKeyId: source[storage.accessKeyId] ?? \"\",\n\t\tsecretAccessKey: source[storage.secretAccessKey] ?? \"\",\n\t\tapiToken: source[gateway.apiKey] ?? \"\",\n\t};\n}\n\n/**\n * Keep a persisted value rather than overwriting it with an empty fetched one.\n *\n * Neon Auth's `base_url` is the case that needs this: integrations created before the API\n * returned it answer with an empty string, and the persisted copy is the only one left. An\n * empty fetched value never carries more information than a non-empty persisted one, so\n * preferring the latter is safe for every var — and it keeps a pull from blanking a working\n * line in someone's `.env`.\n */\nfunction preferPersisted(\n\tvars: Record<string, string>,\n\tsource: NodeJS.ProcessEnv,\n): Record<string, string> {\n\tconst out = { ...vars };\n\tfor (const [key, value] of Object.entries(out)) {\n\t\tif (value !== \"\") continue;\n\t\tconst persisted = source[key];\n\t\tif (persisted !== undefined && persisted !== \"\") out[key] = persisted;\n\t}\n\treturn out;\n}\n\n/**\n * The credential id embedded in an AI Gateway token. The API mints them as\n * `nt_live_<tokenIdShort>_<secret>`, and `tokenIdShort` is the public identifier the credentials\n * list reports — so a persisted token names the credential that issued it. Returns `null` for\n * anything not in that shape (a `.env.example` placeholder, a hand-typed value), which callers\n * treat as unverifiable.\n */\nfunction gatewayTokenIdShort(apiToken: string): string | null {\n\treturn /^nt_live_([^_]+)_.+$/.exec(apiToken)?.[1] ?? null;\n}\n\n/** Whether an issued credential can still be used: not revoked, not past its expiry. */\nfunction isLiveCredential(meta: NeonCredentialMeta, now: number): boolean {\n\tif (meta.revokedAt !== undefined) return false;\n\tif (meta.expiresAt === undefined) return true;\n\tconst expiresAt = Date.parse(meta.expiresAt);\n\treturn Number.isNaN(expiresAt) || expiresAt > now;\n}\n\n/**\n * The live credentials the persisted secrets name — at most one per half. A half that names\n * nothing contributes nothing, which is what a placeholder, a credential revoked in the\n * console, and one copied in from another branch all look like from here.\n */\nfunction namedCredentials(\n\tlive: NeonCredentialMeta[],\n\tpersisted: PersistedSecrets,\n): { storage: NeonCredentialMeta | null; gateway: NeonCredentialMeta | null } {\n\tconst usable = live.filter((meta) => isLiveCredential(meta, Date.now()));\n\tconst shortId = persisted.apiToken\n\t\t? gatewayTokenIdShort(persisted.apiToken)\n\t\t: null;\n\treturn {\n\t\tstorage: persisted.accessKeyId\n\t\t\t? (usable.find((meta) => meta.tokenId === persisted.accessKeyId) ??\n\t\t\t\tnull)\n\t\t\t: null,\n\t\tgateway: shortId\n\t\t\t? (usable.find((meta) => meta.tokenIdShort === shortId) ?? null)\n\t\t\t: null,\n\t};\n}\n\n/**\n * The credential the persisted secrets can be *reused* as, or `null`.\n *\n * Strict on purpose: every half the policy enables has to name a live credential, and when both\n * features are enabled they must name the *same* one — they share a single credential, so\n * halves that disagree came from two different calls and neither can be trusted.\n */\nfunction reusableCredential(\n\tnamed: ReturnType<typeof namedCredentials>,\n\tenabled: { storageEnabled: boolean; gatewayEnabled: boolean },\n): NeonCredentialMeta | null {\n\tif (enabled.storageEnabled && enabled.gatewayEnabled) {\n\t\treturn named.storage &&\n\t\t\tnamed.gateway &&\n\t\t\tnamed.storage.tokenId === named.gateway.tokenId\n\t\t\t? named.storage\n\t\t\t: null;\n\t}\n\tif (enabled.storageEnabled) return named.storage;\n\tif (enabled.gatewayEnabled) return named.gateway;\n\treturn null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"reuse-secrets.js","names":[],"sources":["../../src/lib/reuse-secrets.ts"],"sourcesContent":["import {\n\ttype Config,\n\tcredentialScopesSatisfied,\n\ttype NeonCredentialMeta,\n} from \"@neon/config/v1\";\n\nimport {\n\tcreateApiFromOptions,\n\tcredentialEnvKeys,\n\tcredentialName,\n\ttype FetchEnvOptions,\n\tfetchEnvKeys,\n\tNEON_ENV_VAR_KEYS,\n\tpolicyEnvKeys,\n\tpreviewCredentialScopes,\n\tresolveBranchPolicy,\n\ttoEntries,\n} from \"./env.js\";\n\n/**\n * What happened to the branch credential during a {@link fetchEnvReusingSecrets} call.\n */\nexport interface CredentialOutcome {\n\t/**\n\t * `true` when a new credential was minted — because none was persisted, or because the\n\t * persisted secrets could not be verified against this branch. `false` when the persisted\n\t * secrets were verified and kept, and when the policy enables nothing credential-backed.\n\t */\n\tissued: boolean;\n\t/**\n\t * The env-var keys the branch credential's secrets surface under, given what the policy\n\t * enables. Empty when the policy enables neither object storage nor the AI Gateway.\n\t */\n\tkeys: string[];\n\t/**\n\t * `tokenId`s revoked because this call superseded them. Only ever credentials the persisted\n\t * secrets named *and* that this tool issued; empty otherwise.\n\t */\n\trevoked: string[];\n\t/**\n\t * `tokenId`s this call superseded but left live, because `revokeSuperseded` was `false`.\n\t * The counterpart to {@link CredentialOutcome.revoked}: exactly the ids that would be\n\t * there instead. Lets a caller name what it orphaned rather than saying that it might\n\t * have orphaned something — always empty on the default path.\n\t */\n\tsuperseded: string[];\n}\n\n/** A resolved branch env, ready to write to a dotenv file or inject into a process. */\nexport interface ReusedBranchEnv {\n\t/** Every Neon env var for the branch, as `{ KEY: value }`. */\n\tvars: Record<string, string>;\n\t/** What happened to the branch credential. */\n\tcredential: CredentialOutcome;\n}\n\n/** The branch credential's secrets as persisted in an env source. Empty string means absent. */\ninterface PersistedSecrets {\n\taccessKeyId: string;\n\tsecretAccessKey: string;\n\tapiToken: string;\n}\n\n/**\n * Resolve a branch's env while keeping one-time secrets the caller already holds.\n *\n * {@link fetchEnvKeys} — and the public `fetchEnv` — only ever *fetch*. The Neon API returns a\n * credential's `api_token` / `s3_secret_access_key` exactly once, at mint time, so \"fetching\"\n * them means minting a new credential; a plain `fetchEnv` on every `neon dev` start or `env\n * pull` would leave a live credential behind each time. This is the wrapper that avoids that:\n * it looks at what the caller already has, decides what is still usable, and asks `fetchEnv`\n * for only the rest.\n *\n * The check is a real verification, not a presence test. A persisted secret is kept only when\n * it names a credential that still exists on this branch, is not revoked or expired, and\n * carries every scope the policy needs. A `.env.example` placeholder, a credential revoked in\n * the console, one copied in from another branch, or one predating a newly-enabled feature all\n * fail that check and get replaced.\n *\n * None of this needs local bookkeeping, because the secrets carry their own credential id:\n * `AWS_ACCESS_KEY_ID` **is** the credential's `tokenId` (the storage gateway authenticates\n * against the full id), and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`,\n * where `tokenIdShort` is what the credentials list reports. The env source being replaced is\n * the record of what the last call issued.\n *\n * ```ts\n * import { fetchEnvReusingSecrets } from \"@neon/env/runtime\";\n *\n * const { vars, credential } = await fetchEnvReusingSecrets(config, {\n * projectId,\n * branch: \"main\",\n * env: { ...process.env, ...readEnvFile(\".env\") },\n * });\n * if (credential.issued) console.log(`new values for ${credential.keys.join(\", \")}`);\n * ```\n */\nexport async function fetchEnvReusingSecrets<const C extends Config>(\n\tconfig: C,\n\toptions: FetchEnvOptions & {\n\t\t/**\n\t\t * Env source holding secrets a previous call persisted — `process.env` layered with a\n\t\t * `.env` file, typically. Defaults to `process.env`.\n\t\t */\n\t\tenv?: NodeJS.ProcessEnv;\n\t\t/**\n\t\t * Revoke the credential a freshly-minted one supersedes. Defaults to `true`.\n\t\t *\n\t\t * Pass `false` when this resolve covers only *part* of what the branch has. Object\n\t\t * storage and the AI Gateway share one credential, so a partial resolve cannot tell\n\t\t * whether the credential its persisted secrets name also backs a service it is not\n\t\t * resolving — and revoking it would kill that service while its vars, which this call\n\t\t * is not rewriting, stay on disk and stop working. The cost is an orphaned credential,\n\t\t * which is the safer of the two failures. `neon env pull --service` is the caller that\n\t\t * needs this.\n\t\t */\n\t\trevokeSuperseded?: boolean;\n\t},\n): Promise<ReusedBranchEnv> {\n\tconst {\n\t\tenv: source = process.env,\n\t\trevokeSuperseded = true,\n\t\t...fetchOptions\n\t} = options;\n\tconst api = options.api ?? createApiFromOptions(options);\n\tconst { branch, desired } = await resolveBranchPolicy(config, options, api);\n\n\tconst storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;\n\tconst gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;\n\tconst secretKeys = credentialEnvKeys({\n\t\tstorage: storageEnabled,\n\t\taiGateway: gatewayEnabled,\n\t});\n\n\t// Nothing credential-backed on this branch, so there is nothing to preserve and no\n\t// credential to spend: fetch everything and skip the credentials endpoint entirely.\n\tif (secretKeys.length === 0) {\n\t\tconst fetched = await fetchEnvKeys(config, fetchOptions, null);\n\t\treturn {\n\t\t\tvars: preferPersisted(toEntries(fetched), source),\n\t\t\tcredential: {\n\t\t\t\tissued: false,\n\t\t\t\tkeys: [],\n\t\t\t\trevoked: [],\n\t\t\t\tsuperseded: [],\n\t\t\t},\n\t\t};\n\t}\n\n\tconst persisted = readPersistedSecrets(source);\n\tconst complete =\n\t\t(!storageEnabled ||\n\t\t\tBoolean(persisted.accessKeyId && persisted.secretAccessKey)) &&\n\t\t(!gatewayEnabled || Boolean(persisted.apiToken));\n\n\t// Look the persisted secrets up whenever there are any — not only when they're complete.\n\t// An incomplete set still names the credential a newly-enabled feature is about to\n\t// supersede (a storage-only credential on a branch that just gained the AI Gateway), and\n\t// that one should be revoked rather than left live.\n\tconst named =\n\t\tpersisted.accessKeyId !== \"\" || persisted.apiToken !== \"\"\n\t\t\t? namedCredentials(\n\t\t\t\t\tawait api.listCredentials(options.projectId, branch.id),\n\t\t\t\t\tpersisted,\n\t\t\t\t)\n\t\t\t: { storage: null, gateway: null };\n\n\tconst reusable = complete\n\t\t? reusableCredential(named, { storageEnabled, gatewayEnabled })\n\t\t: null;\n\tconst scopes = previewCredentialScopes(desired.preview);\n\tconst keep =\n\t\treusable !== null && credentialScopesSatisfied(reusable.scopes, scopes);\n\n\t// Ask for everything the policy produces, minus the secrets we're keeping — which is what\n\t// stops `fetchEnv` from minting a credential it doesn't need.\n\tconst allKeys = policyEnvKeys(desired);\n\tconst fetchKeys = keep\n\t\t? allKeys.filter((key) => !secretKeys.includes(key))\n\t\t: allKeys;\n\tconst fetched = await fetchEnvKeys(\n\t\tconfig,\n\t\t// Pass the resolved id so `fetchEnv` targets the same branch this call verified against,\n\t\t// even if `options.branch` was a name that has since been reused.\n\t\t{ ...fetchOptions, branchId: branch.id, api },\n\t\tfetchKeys,\n\t);\n\n\tconst vars = preferPersisted(toEntries(fetched), source);\n\tif (keep) {\n\t\tfor (const key of secretKeys) {\n\t\t\tconst value = source[key];\n\t\t\tif (value !== undefined) vars[key] = value;\n\t\t}\n\t\treturn {\n\t\t\tvars,\n\t\t\tcredential: {\n\t\t\t\tissued: false,\n\t\t\t\tkeys: secretKeys,\n\t\t\t\trevoked: [],\n\t\t\t\tsuperseded: [],\n\t\t\t},\n\t\t};\n\t}\n\n\t// A replacement was minted, so revoke what it supersedes: the credentials the old secrets\n\t// named, minus any this tool did not issue. Their secrets lived nowhere but the env source\n\t// this call replaces, so revoking them strands nothing — and it keeps a branch from\n\t// accumulating a live credential per call. Everything else on the branch is left alone: it\n\t// may belong to a teammate, another checkout, or a deployed function, and nothing\n\t// observable distinguishes those from an orphan of our own.\n\t//\n\t// Revoked *after* the fetch, so a failed fetch leaves the caller's existing secrets working.\n\tconst ours = new Set<string>();\n\tfor (const meta of [named.storage, named.gateway]) {\n\t\tif (\n\t\t\tmeta !== null &&\n\t\t\tmeta.principalType === \"user\" &&\n\t\t\tmeta.name === credentialName(branch.name)\n\t\t) {\n\t\t\tours.add(meta.tokenId);\n\t\t}\n\t}\n\tif (revokeSuperseded) {\n\t\tfor (const tokenId of ours) {\n\t\t\tawait api.revokeCredential(options.projectId, branch.id, tokenId);\n\t\t}\n\t}\n\n\treturn {\n\t\tvars,\n\t\tcredential: {\n\t\t\tissued: true,\n\t\t\tkeys: secretKeys,\n\t\t\trevoked: revokeSuperseded ? [...ours] : [],\n\t\t\tsuperseded: revokeSuperseded ? [] : [...ours],\n\t\t},\n\t};\n}\n\n/** Read the branch credential's secrets out of an env source. */\nfunction readPersistedSecrets(source: NodeJS.ProcessEnv): PersistedSecrets {\n\tconst storage = NEON_ENV_VAR_KEYS.storage;\n\tconst gateway = NEON_ENV_VAR_KEYS.aiGateway;\n\treturn {\n\t\taccessKeyId: source[storage.accessKeyId] ?? \"\",\n\t\tsecretAccessKey: source[storage.secretAccessKey] ?? \"\",\n\t\tapiToken: source[gateway.apiKey] ?? \"\",\n\t};\n}\n\n/**\n * Keep a persisted value rather than overwriting it with an empty fetched one.\n *\n * Neon Auth's `base_url` is the case that needs this: integrations created before the API\n * returned it answer with an empty string, and the persisted copy is the only one left. An\n * empty fetched value never carries more information than a non-empty persisted one, so\n * preferring the latter is safe for every var — and it keeps a pull from blanking a working\n * line in someone's `.env`.\n */\nfunction preferPersisted(\n\tvars: Record<string, string>,\n\tsource: NodeJS.ProcessEnv,\n): Record<string, string> {\n\tconst out = { ...vars };\n\tfor (const [key, value] of Object.entries(out)) {\n\t\tif (value !== \"\") continue;\n\t\tconst persisted = source[key];\n\t\tif (persisted !== undefined && persisted !== \"\") out[key] = persisted;\n\t}\n\treturn out;\n}\n\n/**\n * The credential id embedded in an AI Gateway token. The API mints them as\n * `nt_live_<tokenIdShort>_<secret>`, and `tokenIdShort` is the public identifier the credentials\n * list reports — so a persisted token names the credential that issued it. Returns `null` for\n * anything not in that shape (a `.env.example` placeholder, a hand-typed value), which callers\n * treat as unverifiable.\n */\nfunction gatewayTokenIdShort(apiToken: string): string | null {\n\treturn /^nt_live_([^_]+)_.+$/.exec(apiToken)?.[1] ?? null;\n}\n\n/** Whether an issued credential can still be used: not revoked, not past its expiry. */\nfunction isLiveCredential(meta: NeonCredentialMeta, now: number): boolean {\n\tif (meta.revokedAt !== undefined) return false;\n\tif (meta.expiresAt === undefined) return true;\n\tconst expiresAt = Date.parse(meta.expiresAt);\n\treturn Number.isNaN(expiresAt) || expiresAt > now;\n}\n\n/**\n * The live credentials the persisted secrets name — at most one per half. A half that names\n * nothing contributes nothing, which is what a placeholder, a credential revoked in the\n * console, and one copied in from another branch all look like from here.\n */\nfunction namedCredentials(\n\tlive: NeonCredentialMeta[],\n\tpersisted: PersistedSecrets,\n): { storage: NeonCredentialMeta | null; gateway: NeonCredentialMeta | null } {\n\tconst usable = live.filter((meta) => isLiveCredential(meta, Date.now()));\n\tconst shortId = persisted.apiToken\n\t\t? gatewayTokenIdShort(persisted.apiToken)\n\t\t: null;\n\treturn {\n\t\tstorage: persisted.accessKeyId\n\t\t\t? (usable.find((meta) => meta.tokenId === persisted.accessKeyId) ??\n\t\t\t\tnull)\n\t\t\t: null,\n\t\tgateway: shortId\n\t\t\t? (usable.find((meta) => meta.tokenIdShort === shortId) ?? null)\n\t\t\t: null,\n\t};\n}\n\n/**\n * The credential the persisted secrets can be *reused* as, or `null`.\n *\n * Strict on purpose: every half the policy enables has to name a live credential, and when both\n * features are enabled they must name the *same* one — they share a single credential, so\n * halves that disagree came from two different calls and neither can be trusted.\n */\nfunction reusableCredential(\n\tnamed: ReturnType<typeof namedCredentials>,\n\tenabled: { storageEnabled: boolean; gatewayEnabled: boolean },\n): NeonCredentialMeta | null {\n\tif (enabled.storageEnabled && enabled.gatewayEnabled) {\n\t\treturn named.storage &&\n\t\t\tnamed.gateway &&\n\t\t\tnamed.storage.tokenId === named.gateway.tokenId\n\t\t\t? named.storage\n\t\t\t: null;\n\t}\n\tif (enabled.storageEnabled) return named.storage;\n\tif (enabled.gatewayEnabled) return named.gateway;\n\treturn null;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neon/env",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "Resolve and inject Neon connection strings for the branch selected by your neon.ts policy. fetchEnv / parseEnv plus a `neon-env` CLI with `run` and `export`.",
5
5
  "keywords": [
6
6
  "neon",