@neondatabase/env 1.2.3 → 1.3.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 +6 -4
- package/dist/cli.js +49 -57
- package/dist/cli.js.map +1 -1
- package/dist/env.js +66 -11
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -153,13 +153,15 @@ functions.hello.baseUrl; // string
|
|
|
153
153
|
|
|
154
154
|
### The branch credential
|
|
155
155
|
|
|
156
|
-
Object storage and the AI Gateway are backed by
|
|
156
|
+
Object storage and the AI Gateway are backed by platform default credentials on every branch in a region that has those products (`Default object storage credential`, `Default AI gateway credential`). `fetchEnv` reveals those by exact name. It mints a combined `neon-env ${branch}` credential only when a needed default is missing.
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
Secrets (`s3_secret_access_key`, `api_token`) are returned from reveal as well as at mint time. Call `fetchEnv` on every `neon dev` start without reusing what you already hold and you re-reveal the same defaults — or, in a region with no defaults, you leave a live minted credential behind each time.
|
|
159
|
+
|
|
160
|
+
A persisted secret is only reusable if it still names the platform default (when one exists), or a still-live minted credential (when none does). A presence check cannot tell a real secret from a `.env.example` placeholder.
|
|
159
161
|
|
|
160
162
|
No local bookkeeping is needed to do it, because the secrets carry their own credential id: `AWS_ACCESS_KEY_ID` **is** the credential's token id, and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`. So the `.env` you are about to rewrite already records which credential issued it.
|
|
161
163
|
|
|
162
|
-
The [`neon` CLI](../cli)
|
|
164
|
+
The [`neon` CLI](../cli) additionally reuses persisted secrets (`neon env pull`, `neon dev`) so a loop does not re-reveal every start. `fetchEnv` itself reveals or mints on every call.
|
|
163
165
|
|
|
164
166
|
### Fetching a subset
|
|
165
167
|
|
|
@@ -187,7 +189,7 @@ selected.branch?.name; // string | undefined
|
|
|
187
189
|
|
|
188
190
|
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are one credential and must be selected together. Literal lists that contain only one half are a type error; a runtime-built list that resolves to one half throws before any API request or credential issuance.
|
|
189
191
|
|
|
190
|
-
Work is skipped, not just the result narrowed. Leave out `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `NEON_AI_GATEWAY_TOKEN` and **no credential is minted at all** — which is exactly how `fetchEnvReusingSecrets` refreshes everything else while keeping secrets you already have. The non-secret vars of those features (`AWS_ENDPOINT_URL_S3`, `AWS_REGION`, `NEON_AI_GATEWAY_BASE_URL`) are branch metadata and stay available on their own.
|
|
192
|
+
Work is skipped, not just the result narrowed. Leave out `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `NEON_AI_GATEWAY_TOKEN` and **no credential is revealed or minted at all** — which is exactly how `fetchEnvReusingSecrets` refreshes everything else while keeping secrets you already have. The non-secret vars of those features (`AWS_ENDPOINT_URL_S3`, `AWS_REGION`, `NEON_AI_GATEWAY_BASE_URL`) are branch metadata and stay available on their own.
|
|
191
193
|
|
|
192
194
|
## Connection role & database selection
|
|
193
195
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as
|
|
2
|
+
import { a as defaultAiGatewayCredential, c as fetchEnvKeysState, d as isLiveCredential, h as toEntries, i as credentialName, m as resolveBranchPolicy, n as createApiFromOptions, o as defaultStorageCredential, p as policyEnvKeys, r as credentialEnvKeys, t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "./env.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { existsSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -12,20 +12,19 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
//#region ../../internals/env-core/dist/reuse-secrets.js
|
|
14
14
|
/**
|
|
15
|
-
* Resolve a branch's env while keeping
|
|
15
|
+
* Resolve a branch's env while keeping secrets the caller already holds.
|
|
16
16
|
*
|
|
17
|
-
* {@link fetchEnvKeys}
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* for only the rest.
|
|
17
|
+
* {@link fetchEnvKeys} reveals the platform default credentials (or mints a fallback when a
|
|
18
|
+
* region has none). Calling it on every `neon dev` start would rewrite `.env` with freshly
|
|
19
|
+
* revealed secrets each time. This wrapper looks at what the caller already has, keeps a
|
|
20
|
+
* half when it already *is* that default (storage and gateway are independent credentials),
|
|
21
|
+
* and asks `fetchEnv` for only the rest.
|
|
23
22
|
*
|
|
24
23
|
* The check is a real verification, not a presence test. A persisted secret is kept only when
|
|
25
|
-
* it names a credential that still exists on this branch, is not revoked or expired, and
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* it names a credential that still exists on this branch, is not revoked or expired, and —
|
|
25
|
+
* when defaults exist — *is* that default. A leftover `neon-env ${branch}` credential this
|
|
26
|
+
* tool minted is replaced by the defaults and revoked. A `.env.example` placeholder, a
|
|
27
|
+
* credential revoked in the console, or one copied in from another branch fails that check.
|
|
29
28
|
*
|
|
30
29
|
* None of this needs local bookkeeping, because the secrets carry their own credential id:
|
|
31
30
|
* `AWS_ACCESS_KEY_ID` **is** the credential's `tokenId` (the storage gateway authenticates
|
|
@@ -75,47 +74,47 @@ async function fetchEnvReusingSecrets(config, options) {
|
|
|
75
74
|
const persisted = readPersistedSecrets(source);
|
|
76
75
|
const storageCredentialManaged = requested === null || storageCredentialSelected;
|
|
77
76
|
const gatewayCredentialManaged = requested === null || gatewayCredentialSelected;
|
|
78
|
-
const
|
|
79
|
-
const
|
|
77
|
+
const storageComplete = Boolean(persisted.accessKeyId && persisted.secretAccessKey);
|
|
78
|
+
const gatewayComplete = Boolean(persisted.apiToken);
|
|
79
|
+
const listed = storageCredentialManaged && persisted.accessKeyId !== "" || gatewayCredentialManaged && persisted.apiToken !== "" ? await api.listCredentials(options.projectId, branch.id) : [];
|
|
80
|
+
const named = listed.length > 0 ? namedCredentials(listed, persisted) : {
|
|
80
81
|
storage: null,
|
|
81
82
|
gateway: null
|
|
82
83
|
};
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const fetchKeys = requested === null ? null :
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const storageDefault = defaultStorageCredential(listed, now);
|
|
86
|
+
const gatewayDefault = defaultAiGatewayCredential(listed, now);
|
|
87
|
+
const keepStorage = !storageCredentialSelected || storageComplete && halfReusable(named.storage, storageDefault, ["storage:read", "storage:write"]);
|
|
88
|
+
const keepGateway = !gatewayCredentialSelected || gatewayComplete && halfReusable(named.gateway, gatewayDefault, ["ai_gateway:invoke"]);
|
|
89
|
+
const keptSecretKeys = credentialEnvKeys({
|
|
90
|
+
storage: storageCredentialSelected && keepStorage,
|
|
91
|
+
aiGateway: gatewayCredentialSelected && keepGateway
|
|
92
|
+
}).filter((key) => selected.has(key));
|
|
93
|
+
const fetchKeys = requested === null ? null : selectedPolicyKeys.filter((key) => !keptSecretKeys.includes(key));
|
|
93
94
|
const fetched = await fetchEnvKeysState(config, {
|
|
94
95
|
...fetchOptions,
|
|
95
96
|
branchId: branch.id,
|
|
96
97
|
api,
|
|
97
|
-
...
|
|
98
|
+
...keptSecretKeys.length > 0 && requested === null ? { omitKeys: keptSecretKeys } : {}
|
|
98
99
|
}, fetchKeys);
|
|
99
100
|
const vars = preferPersisted(toEntries(fetched.env), source);
|
|
100
101
|
const unavailable = fetched.functionUrlsUnavailable ? { functionUrlsUnavailable: true } : {};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (value !== void 0) vars[key] = value;
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
vars,
|
|
108
|
-
credential: {
|
|
109
|
-
issued: false,
|
|
110
|
-
keys: secretKeys,
|
|
111
|
-
revoked: [],
|
|
112
|
-
superseded: []
|
|
113
|
-
},
|
|
114
|
-
...unavailable
|
|
115
|
-
};
|
|
102
|
+
for (const key of keptSecretKeys) {
|
|
103
|
+
const value = source[key];
|
|
104
|
+
if (value !== void 0) vars[key] = value;
|
|
116
105
|
}
|
|
106
|
+
if (!(storageCredentialSelected && !keepStorage || gatewayCredentialSelected && !keepGateway)) return {
|
|
107
|
+
vars,
|
|
108
|
+
credential: {
|
|
109
|
+
issued: false,
|
|
110
|
+
keys: secretKeys,
|
|
111
|
+
revoked: [],
|
|
112
|
+
superseded: []
|
|
113
|
+
},
|
|
114
|
+
...unavailable
|
|
115
|
+
};
|
|
117
116
|
const ours = /* @__PURE__ */ new Set();
|
|
118
|
-
for (const meta of [storageCredentialManaged ? named.storage : null, gatewayCredentialManaged ? named.gateway : null]) if (meta !== null && meta.principalType === "user" && meta.name === credentialName(branch.name)) ours.add(meta.tokenId);
|
|
117
|
+
for (const meta of [storageCredentialManaged && !keepStorage ? named.storage : null, gatewayCredentialManaged && !keepGateway ? named.gateway : null]) if (meta !== null && meta.principalType === "user" && meta.name === credentialName(branch.name)) ours.add(meta.tokenId);
|
|
119
118
|
if (revokeSuperseded) for (const tokenId of ours) await api.revokeCredential(options.projectId, branch.id, tokenId);
|
|
120
119
|
return {
|
|
121
120
|
vars,
|
|
@@ -166,13 +165,6 @@ function preferPersisted(vars, source) {
|
|
|
166
165
|
function gatewayTokenIdShort(apiToken) {
|
|
167
166
|
return /^nt_live_([^_]+)_.+$/.exec(apiToken)?.[1] ?? null;
|
|
168
167
|
}
|
|
169
|
-
/** Whether an issued credential can still be used: not revoked, not past its expiry. */
|
|
170
|
-
function isLiveCredential(meta, now) {
|
|
171
|
-
if (meta.revokedAt !== void 0) return false;
|
|
172
|
-
if (meta.expiresAt === void 0) return true;
|
|
173
|
-
const expiresAt = Date.parse(meta.expiresAt);
|
|
174
|
-
return Number.isNaN(expiresAt) || expiresAt > now;
|
|
175
|
-
}
|
|
176
168
|
/**
|
|
177
169
|
* The live credentials the persisted secrets name — at most one per half. A half that names
|
|
178
170
|
* nothing contributes nothing, which is what a placeholder, a credential revoked in the
|
|
@@ -187,17 +179,17 @@ function namedCredentials(live, persisted) {
|
|
|
187
179
|
};
|
|
188
180
|
}
|
|
189
181
|
/**
|
|
190
|
-
*
|
|
182
|
+
* Whether a persisted half can be reused.
|
|
191
183
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
184
|
+
* When a platform default exists, only that default is reusable — a leftover
|
|
185
|
+
* `neon-env ${branch}` mint is replaced. When no default exists (mint fallback),
|
|
186
|
+
* any live credential the secrets name is reusable if it still carries the
|
|
187
|
+
* scopes that half needs.
|
|
195
188
|
*/
|
|
196
|
-
function
|
|
197
|
-
if (
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
return null;
|
|
189
|
+
function halfReusable(named, defaultMeta, requiredScopes) {
|
|
190
|
+
if (named === null) return false;
|
|
191
|
+
if (defaultMeta !== null) return named.tokenId === defaultMeta.tokenId;
|
|
192
|
+
return credentialScopesSatisfied(named.scopes, requiredScopes);
|
|
201
193
|
}
|
|
202
194
|
//#endregion
|
|
203
195
|
//#region ../../internals/cli-core/dist/cli_config.js
|