@neondatabase/env 1.0.0 → 1.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/README.md +21 -1
- package/dist/cli.js +1304 -6
- package/dist/cli.js.map +1 -1
- package/dist/{_shared/env-core/env.js → env.js} +62 -41
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +528 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +197 -2
- package/dist/{lib/parse-env.js.map → index.js.map} +1 -1
- package/package.json +12 -7
- package/dist/_shared/auth_selection.d.ts +0 -95
- package/dist/_shared/auth_selection.d.ts.map +0 -1
- package/dist/_shared/auth_selection.js +0 -85
- package/dist/_shared/auth_selection.js.map +0 -1
- package/dist/_shared/credentials.d.ts +0 -186
- package/dist/_shared/credentials.d.ts.map +0 -1
- package/dist/_shared/credentials.js +0 -190
- package/dist/_shared/credentials.js.map +0 -1
- package/dist/_shared/env-core/env.d.ts +0 -424
- package/dist/_shared/env-core/env.d.ts.map +0 -1
- package/dist/_shared/env-core/env.js.map +0 -1
- package/dist/_shared/env-core/reuse-secrets.d.ts +0 -95
- package/dist/_shared/env-core/reuse-secrets.d.ts.map +0 -1
- package/dist/_shared/env-core/reuse-secrets.js +0 -181
- package/dist/_shared/env-core/reuse-secrets.js.map +0 -1
- package/dist/_shared/paths.d.ts +0 -116
- package/dist/_shared/paths.d.ts.map +0 -1
- package/dist/_shared/paths.js +0 -153
- package/dist/_shared/paths.js.map +0 -1
- package/dist/_shared/profiles.d.ts +0 -145
- package/dist/_shared/profiles.d.ts.map +0 -1
- package/dist/_shared/profiles.js +0 -228
- package/dist/_shared/profiles.js.map +0 -1
- package/dist/_shared/secure_file.d.ts +0 -25
- package/dist/_shared/secure_file.d.ts.map +0 -1
- package/dist/_shared/secure_file.js +0 -43
- package/dist/_shared/secure_file.js.map +0 -1
- package/dist/config/dist/lib/define-config.d.ts +0 -20
- package/dist/config/dist/lib/define-config.d.ts.map +0 -1
- package/dist/config/dist/lib/neon-api.d.ts +0 -375
- package/dist/config/dist/lib/neon-api.d.ts.map +0 -1
- package/dist/config/dist/lib/types.d.ts +0 -603
- package/dist/config/dist/lib/types.d.ts.map +0 -1
- package/dist/config/dist/v1.d.ts +0 -5
- package/dist/lib/cli/commands.d.ts +0 -68
- package/dist/lib/cli/commands.d.ts.map +0 -1
- package/dist/lib/cli/commands.js +0 -233
- package/dist/lib/cli/commands.js.map +0 -1
- package/dist/lib/cli/resolve-api-key.d.ts +0 -29
- package/dist/lib/cli/resolve-api-key.d.ts.map +0 -1
- package/dist/lib/cli/resolve-api-key.js +0 -74
- package/dist/lib/cli/resolve-api-key.js.map +0 -1
- package/dist/lib/cli/resolve-context.d.ts +0 -34
- package/dist/lib/cli/resolve-context.d.ts.map +0 -1
- package/dist/lib/cli/resolve-context.js +0 -88
- package/dist/lib/cli/resolve-context.js.map +0 -1
- package/dist/lib/parse-env.d.ts +0 -95
- package/dist/lib/parse-env.d.ts.map +0 -1
- package/dist/lib/parse-env.js +0 -198
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { Config } from "../../config/dist/lib/types.js";
|
|
2
|
-
import "../../config/dist/v1.js";
|
|
3
|
-
import { FetchEnvOptions } from "./env.js";
|
|
4
|
-
|
|
5
|
-
//#region src/_shared/env-core/reuse-secrets.d.ts
|
|
6
|
-
/**
|
|
7
|
-
* What happened to the branch credential during a {@link fetchEnvReusingSecrets} call.
|
|
8
|
-
*/
|
|
9
|
-
interface CredentialOutcome {
|
|
10
|
-
/**
|
|
11
|
-
* `true` when a new credential was minted — because none was persisted, or because the
|
|
12
|
-
* persisted secrets could not be verified against this branch. `false` when the persisted
|
|
13
|
-
* secrets were verified and kept, and when the policy enables nothing credential-backed.
|
|
14
|
-
*/
|
|
15
|
-
issued: boolean;
|
|
16
|
-
/**
|
|
17
|
-
* The env-var keys the branch credential's secrets surface under, given what the policy
|
|
18
|
-
* enables. Empty when the policy enables neither object storage nor the AI Gateway.
|
|
19
|
-
*/
|
|
20
|
-
keys: string[];
|
|
21
|
-
/**
|
|
22
|
-
* `tokenId`s revoked because this call superseded them. Only ever credentials the persisted
|
|
23
|
-
* secrets named *and* that this tool issued; empty otherwise.
|
|
24
|
-
*/
|
|
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[];
|
|
33
|
-
}
|
|
34
|
-
/** A resolved branch env, ready to write to a dotenv file or inject into a process. */
|
|
35
|
-
interface ReusedBranchEnv {
|
|
36
|
-
/** Every Neon env var for the branch, as `{ KEY: value }`. */
|
|
37
|
-
vars: Record<string, string>;
|
|
38
|
-
/** What happened to the branch credential. */
|
|
39
|
-
credential: CredentialOutcome;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Resolve a branch's env while keeping one-time secrets the caller already holds.
|
|
43
|
-
*
|
|
44
|
-
* {@link fetchEnvKeys} — and the public `fetchEnv` — only ever *fetch*. The Neon API returns a
|
|
45
|
-
* credential's `api_token` / `s3_secret_access_key` exactly once, at mint time, so "fetching"
|
|
46
|
-
* them means minting a new credential; a plain `fetchEnv` on every `neon dev` start or `env
|
|
47
|
-
* pull` would leave a live credential behind each time. This is the wrapper that avoids that:
|
|
48
|
-
* it looks at what the caller already has, decides what is still usable, and asks `fetchEnv`
|
|
49
|
-
* for only the rest.
|
|
50
|
-
*
|
|
51
|
-
* The check is a real verification, not a presence test. A persisted secret is kept only when
|
|
52
|
-
* it names a credential that still exists on this branch, is not revoked or expired, and
|
|
53
|
-
* carries every scope the policy needs. A `.env.example` placeholder, a credential revoked in
|
|
54
|
-
* the console, one copied in from another branch, or one predating a newly-enabled feature all
|
|
55
|
-
* fail that check and get replaced.
|
|
56
|
-
*
|
|
57
|
-
* None of this needs local bookkeeping, because the secrets carry their own credential id:
|
|
58
|
-
* `AWS_ACCESS_KEY_ID` **is** the credential's `tokenId` (the storage gateway authenticates
|
|
59
|
-
* against the full id), and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`,
|
|
60
|
-
* where `tokenIdShort` is what the credentials list reports. The env source being replaced is
|
|
61
|
-
* the record of what the last call issued.
|
|
62
|
-
*
|
|
63
|
-
* ```ts
|
|
64
|
-
* import { fetchEnvReusingSecrets } from "../_shared/env-core/reuse-secrets.js";
|
|
65
|
-
*
|
|
66
|
-
* const { vars, credential } = await fetchEnvReusingSecrets(config, {
|
|
67
|
-
* projectId,
|
|
68
|
-
* branch: "main",
|
|
69
|
-
* env: { ...process.env, ...readEnvFile(".env") },
|
|
70
|
-
* });
|
|
71
|
-
* if (credential.issued) console.log(`new values for ${credential.keys.join(", ")}`);
|
|
72
|
-
* ```
|
|
73
|
-
*/
|
|
74
|
-
declare function fetchEnvReusingSecrets<const C extends Config>(config: C, options: FetchEnvOptions & {
|
|
75
|
-
/**
|
|
76
|
-
* Env source holding secrets a previous call persisted — `process.env` layered with a
|
|
77
|
-
* `.env` file, typically. Defaults to `process.env`.
|
|
78
|
-
*/
|
|
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;
|
|
92
|
-
}): Promise<ReusedBranchEnv>;
|
|
93
|
-
//#endregion
|
|
94
|
-
export { CredentialOutcome, ReusedBranchEnv, fetchEnvReusingSecrets };
|
|
95
|
-
//# sourceMappingURL=reuse-secrets.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"reuse-secrets.d.ts","names":[],"sources":["../../../src/_shared/env-core/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"}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { NEON_ENV_VAR_KEYS, createApiFromOptions, credentialEnvKeys, credentialName, fetchEnvKeys, policyEnvKeys, previewCredentialScopes, resolveBranchPolicy, toEntries } from "./env.js";
|
|
2
|
-
import { credentialScopesSatisfied } from "@neon/config/v1";
|
|
3
|
-
//#region src/_shared/env-core/reuse-secrets.ts
|
|
4
|
-
/**
|
|
5
|
-
* Resolve a branch's env while keeping one-time secrets the caller already holds.
|
|
6
|
-
*
|
|
7
|
-
* {@link fetchEnvKeys} — and the public `fetchEnv` — only ever *fetch*. The Neon API returns a
|
|
8
|
-
* credential's `api_token` / `s3_secret_access_key` exactly once, at mint time, so "fetching"
|
|
9
|
-
* them means minting a new credential; a plain `fetchEnv` on every `neon dev` start or `env
|
|
10
|
-
* pull` would leave a live credential behind each time. This is the wrapper that avoids that:
|
|
11
|
-
* it looks at what the caller already has, decides what is still usable, and asks `fetchEnv`
|
|
12
|
-
* for only the rest.
|
|
13
|
-
*
|
|
14
|
-
* The check is a real verification, not a presence test. A persisted secret is kept only when
|
|
15
|
-
* it names a credential that still exists on this branch, is not revoked or expired, and
|
|
16
|
-
* carries every scope the policy needs. A `.env.example` placeholder, a credential revoked in
|
|
17
|
-
* the console, one copied in from another branch, or one predating a newly-enabled feature all
|
|
18
|
-
* fail that check and get replaced.
|
|
19
|
-
*
|
|
20
|
-
* None of this needs local bookkeeping, because the secrets carry their own credential id:
|
|
21
|
-
* `AWS_ACCESS_KEY_ID` **is** the credential's `tokenId` (the storage gateway authenticates
|
|
22
|
-
* against the full id), and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`,
|
|
23
|
-
* where `tokenIdShort` is what the credentials list reports. The env source being replaced is
|
|
24
|
-
* the record of what the last call issued.
|
|
25
|
-
*
|
|
26
|
-
* ```ts
|
|
27
|
-
* import { fetchEnvReusingSecrets } from "../_shared/env-core/reuse-secrets.js";
|
|
28
|
-
*
|
|
29
|
-
* const { vars, credential } = await fetchEnvReusingSecrets(config, {
|
|
30
|
-
* projectId,
|
|
31
|
-
* branch: "main",
|
|
32
|
-
* env: { ...process.env, ...readEnvFile(".env") },
|
|
33
|
-
* });
|
|
34
|
-
* if (credential.issued) console.log(`new values for ${credential.keys.join(", ")}`);
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
async function fetchEnvReusingSecrets(config, options) {
|
|
38
|
-
const { env: source = process.env, revokeSuperseded = true, ...fetchOptions } = options;
|
|
39
|
-
const api = options.api ?? createApiFromOptions(options);
|
|
40
|
-
const { branch, desired } = await resolveBranchPolicy(config, options, api);
|
|
41
|
-
const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
|
|
42
|
-
const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
|
|
43
|
-
const secretKeys = credentialEnvKeys({
|
|
44
|
-
storage: storageEnabled,
|
|
45
|
-
aiGateway: gatewayEnabled
|
|
46
|
-
});
|
|
47
|
-
if (secretKeys.length === 0) {
|
|
48
|
-
const fetched = await fetchEnvKeys(config, fetchOptions, null);
|
|
49
|
-
return {
|
|
50
|
-
vars: preferPersisted(toEntries(fetched), source),
|
|
51
|
-
credential: {
|
|
52
|
-
issued: false,
|
|
53
|
-
keys: [],
|
|
54
|
-
revoked: [],
|
|
55
|
-
superseded: []
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
const persisted = readPersistedSecrets(source);
|
|
60
|
-
const complete = (!storageEnabled || Boolean(persisted.accessKeyId && persisted.secretAccessKey)) && (!gatewayEnabled || Boolean(persisted.apiToken));
|
|
61
|
-
const named = persisted.accessKeyId !== "" || persisted.apiToken !== "" ? namedCredentials(await api.listCredentials(options.projectId, branch.id), persisted) : {
|
|
62
|
-
storage: null,
|
|
63
|
-
gateway: null
|
|
64
|
-
};
|
|
65
|
-
const reusable = complete ? reusableCredential(named, {
|
|
66
|
-
storageEnabled,
|
|
67
|
-
gatewayEnabled
|
|
68
|
-
}) : null;
|
|
69
|
-
const scopes = previewCredentialScopes(desired.preview);
|
|
70
|
-
const keep = reusable !== null && credentialScopesSatisfied(reusable.scopes, scopes);
|
|
71
|
-
const allKeys = policyEnvKeys(desired);
|
|
72
|
-
const fetchKeys = keep ? allKeys.filter((key) => !secretKeys.includes(key)) : allKeys;
|
|
73
|
-
const fetched = await fetchEnvKeys(config, {
|
|
74
|
-
...fetchOptions,
|
|
75
|
-
branchId: branch.id,
|
|
76
|
-
api
|
|
77
|
-
}, fetchKeys);
|
|
78
|
-
const vars = preferPersisted(toEntries(fetched), source);
|
|
79
|
-
if (keep) {
|
|
80
|
-
for (const key of secretKeys) {
|
|
81
|
-
const value = source[key];
|
|
82
|
-
if (value !== void 0) vars[key] = value;
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
vars,
|
|
86
|
-
credential: {
|
|
87
|
-
issued: false,
|
|
88
|
-
keys: secretKeys,
|
|
89
|
-
revoked: [],
|
|
90
|
-
superseded: []
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
const ours = /* @__PURE__ */ new Set();
|
|
95
|
-
for (const meta of [named.storage, named.gateway]) if (meta !== null && meta.principalType === "user" && meta.name === credentialName(branch.name)) ours.add(meta.tokenId);
|
|
96
|
-
if (revokeSuperseded) for (const tokenId of ours) await api.revokeCredential(options.projectId, branch.id, tokenId);
|
|
97
|
-
return {
|
|
98
|
-
vars,
|
|
99
|
-
credential: {
|
|
100
|
-
issued: true,
|
|
101
|
-
keys: secretKeys,
|
|
102
|
-
revoked: revokeSuperseded ? [...ours] : [],
|
|
103
|
-
superseded: revokeSuperseded ? [] : [...ours]
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
/** Read the branch credential's secrets out of an env source. */
|
|
108
|
-
function readPersistedSecrets(source) {
|
|
109
|
-
const storage = NEON_ENV_VAR_KEYS.storage;
|
|
110
|
-
const gateway = NEON_ENV_VAR_KEYS.aiGateway;
|
|
111
|
-
return {
|
|
112
|
-
accessKeyId: source[storage.accessKeyId] ?? "",
|
|
113
|
-
secretAccessKey: source[storage.secretAccessKey] ?? "",
|
|
114
|
-
apiToken: source[gateway.apiKey] ?? ""
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Keep a persisted value rather than overwriting it with an empty fetched one.
|
|
119
|
-
*
|
|
120
|
-
* Neon Auth's `base_url` is the case that needs this: integrations created before the API
|
|
121
|
-
* returned it answer with an empty string, and the persisted copy is the only one left. An
|
|
122
|
-
* empty fetched value never carries more information than a non-empty persisted one, so
|
|
123
|
-
* preferring the latter is safe for every var — and it keeps a pull from blanking a working
|
|
124
|
-
* line in someone's `.env`.
|
|
125
|
-
*/
|
|
126
|
-
function preferPersisted(vars, source) {
|
|
127
|
-
const out = { ...vars };
|
|
128
|
-
for (const [key, value] of Object.entries(out)) {
|
|
129
|
-
if (value !== "") continue;
|
|
130
|
-
const persisted = source[key];
|
|
131
|
-
if (persisted !== void 0 && persisted !== "") out[key] = persisted;
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* The credential id embedded in an AI Gateway token. The API mints them as
|
|
137
|
-
* `nt_live_<tokenIdShort>_<secret>`, and `tokenIdShort` is the public identifier the credentials
|
|
138
|
-
* list reports — so a persisted token names the credential that issued it. Returns `null` for
|
|
139
|
-
* anything not in that shape (a `.env.example` placeholder, a hand-typed value), which callers
|
|
140
|
-
* treat as unverifiable.
|
|
141
|
-
*/
|
|
142
|
-
function gatewayTokenIdShort(apiToken) {
|
|
143
|
-
return /^nt_live_([^_]+)_.+$/.exec(apiToken)?.[1] ?? null;
|
|
144
|
-
}
|
|
145
|
-
/** Whether an issued credential can still be used: not revoked, not past its expiry. */
|
|
146
|
-
function isLiveCredential(meta, now) {
|
|
147
|
-
if (meta.revokedAt !== void 0) return false;
|
|
148
|
-
if (meta.expiresAt === void 0) return true;
|
|
149
|
-
const expiresAt = Date.parse(meta.expiresAt);
|
|
150
|
-
return Number.isNaN(expiresAt) || expiresAt > now;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* The live credentials the persisted secrets name — at most one per half. A half that names
|
|
154
|
-
* nothing contributes nothing, which is what a placeholder, a credential revoked in the
|
|
155
|
-
* console, and one copied in from another branch all look like from here.
|
|
156
|
-
*/
|
|
157
|
-
function namedCredentials(live, persisted) {
|
|
158
|
-
const usable = live.filter((meta) => isLiveCredential(meta, Date.now()));
|
|
159
|
-
const shortId = persisted.apiToken ? gatewayTokenIdShort(persisted.apiToken) : null;
|
|
160
|
-
return {
|
|
161
|
-
storage: persisted.accessKeyId ? usable.find((meta) => meta.tokenId === persisted.accessKeyId) ?? null : null,
|
|
162
|
-
gateway: shortId ? usable.find((meta) => meta.tokenIdShort === shortId) ?? null : null
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* The credential the persisted secrets can be *reused* as, or `null`.
|
|
167
|
-
*
|
|
168
|
-
* Strict on purpose: every half the policy enables has to name a live credential, and when both
|
|
169
|
-
* features are enabled they must name the *same* one — they share a single credential, so
|
|
170
|
-
* halves that disagree came from two different calls and neither can be trusted.
|
|
171
|
-
*/
|
|
172
|
-
function reusableCredential(named, enabled) {
|
|
173
|
-
if (enabled.storageEnabled && enabled.gatewayEnabled) return named.storage && named.gateway && named.storage.tokenId === named.gateway.tokenId ? named.storage : null;
|
|
174
|
-
if (enabled.storageEnabled) return named.storage;
|
|
175
|
-
if (enabled.gatewayEnabled) return named.gateway;
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
//#endregion
|
|
179
|
-
export { fetchEnvReusingSecrets };
|
|
180
|
-
|
|
181
|
-
//# sourceMappingURL=reuse-secrets.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"reuse-secrets.js","names":[],"sources":["../../../src/_shared/env-core/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 \"../_shared/env-core/reuse-secrets.js\";\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/dist/_shared/paths.d.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
//#region src/_shared/paths.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* # Where the Neon CLIs keep their files on disk
|
|
4
|
-
*
|
|
5
|
-
**Deliberately impure.** It reads environment variables and touches the filesystem, which
|
|
6
|
-
* `@neon/config` — the package this used to be a subpath of — must never do from its root
|
|
7
|
-
* export. It lives here instead of there precisely so that a policy-facing package does not
|
|
8
|
-
* carry implementor-only code.
|
|
9
|
-
*
|
|
10
|
-
* It exists because three separate readers each grew their own answer to "where is the
|
|
11
|
-
* config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
|
|
12
|
-
* not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
|
|
13
|
-
* flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
|
|
14
|
-
* credentials somewhere the other two never looked.
|
|
15
|
-
*
|
|
16
|
-
* ## The directory
|
|
17
|
-
*
|
|
18
|
-
* `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
|
|
19
|
-
* each entry winning over the next:
|
|
20
|
-
*
|
|
21
|
-
* 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
|
|
22
|
-
* 2. `NEON_CONFIG_DIR` — exact.
|
|
23
|
-
* 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
|
|
24
|
-
* 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
|
|
25
|
-
*
|
|
26
|
-
* An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
|
|
27
|
-
* quietly read `~/.config/neonctl` would defeat the point of passing it.
|
|
28
|
-
*
|
|
29
|
-
* ## The files
|
|
30
|
-
*
|
|
31
|
-
* {@link resolveConfigFile} answers "which path should I use for this file", and it is the
|
|
32
|
-
* same answer for reading and writing:
|
|
33
|
-
*
|
|
34
|
-
* - Present in `neon/` → use it.
|
|
35
|
-
* - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
|
|
36
|
-
* never copied or moved, so nothing is left behind to go stale and no other tool starts
|
|
37
|
-
* reading an abandoned token.
|
|
38
|
-
* - Present in neither → the new location. New files only ever appear under `neon/`.
|
|
39
|
-
*/
|
|
40
|
-
/** Current directory name. New files are created here. */
|
|
41
|
-
declare const CONFIG_DIR_NAME = "neon";
|
|
42
|
-
/** Legacy directory name, read forever so existing installs keep working untouched. */
|
|
43
|
-
declare const LEGACY_CONFIG_DIR_NAME = "neonctl";
|
|
44
|
-
interface ConfigPathOptions {
|
|
45
|
-
/**
|
|
46
|
-
* An explicit directory, e.g. from a `--config-dir` flag. Used exactly as given: no
|
|
47
|
-
* environment variables are consulted and the legacy directory is never searched.
|
|
48
|
-
*/
|
|
49
|
-
dir?: string;
|
|
50
|
-
/** Environment to read. Defaults to `process.env`. Injectable for tests. */
|
|
51
|
-
env?: NodeJS.ProcessEnv;
|
|
52
|
-
}
|
|
53
|
-
/** Where files are created. See the module docs for the precedence. */
|
|
54
|
-
declare function configDir(options?: ConfigPathOptions): string;
|
|
55
|
-
/**
|
|
56
|
-
* The legacy directory, or `undefined` when the location was chosen explicitly (in which
|
|
57
|
-
* case there is no legacy counterpart to fall back to).
|
|
58
|
-
*/
|
|
59
|
-
declare function legacyConfigDir(options?: ConfigPathOptions): string | undefined;
|
|
60
|
-
interface ResolvedConfigFile {
|
|
61
|
-
/** The path to use, for both reading and writing. */
|
|
62
|
-
path: string;
|
|
63
|
-
/** The directory `path` lives in. */
|
|
64
|
-
dir: string;
|
|
65
|
-
/** True when the file was found in the legacy `neonctl` directory. */
|
|
66
|
-
isLegacy: boolean;
|
|
67
|
-
/** Whether the file exists at `path` right now. */
|
|
68
|
-
exists: boolean;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Resolve one file inside the config directory. Prefers the current location, falls back to
|
|
72
|
-
* an existing legacy file **in place**, and otherwise points at the current location so new
|
|
73
|
-
* files are created there.
|
|
74
|
-
*/
|
|
75
|
-
declare function resolveConfigFile(fileName: string, options?: ConfigPathOptions): ResolvedConfigFile;
|
|
76
|
-
declare const CREDENTIALS_FILE = "credentials.json";
|
|
77
|
-
/**
|
|
78
|
-
* Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
|
|
79
|
-
*
|
|
80
|
-
* The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
|
|
81
|
-
* see {@link credentialsPath} — but it is never written to, moved, or deleted.
|
|
82
|
-
*/
|
|
83
|
-
declare const defaultDir: string;
|
|
84
|
-
/**
|
|
85
|
-
* Where this invocation's `credentials.json` lives.
|
|
86
|
-
*
|
|
87
|
-
* When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
|
|
88
|
-
* directory is used **in place**: an install that predates the rename keeps working, and its
|
|
89
|
-
* credentials are never duplicated into a second location where one copy could go stale while
|
|
90
|
-
* another tool still reads it.
|
|
91
|
-
*
|
|
92
|
-
* A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
|
|
93
|
-
* explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
|
|
94
|
-
* scratch directory must never pick up a developer's real credentials.
|
|
95
|
-
*/
|
|
96
|
-
declare const credentialsPath: (dir: string) => string;
|
|
97
|
-
/**
|
|
98
|
-
* Whether a credentials file is one the CLI created, rather than a path a profile adopted.
|
|
99
|
-
*
|
|
100
|
-
* Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
|
|
101
|
-
* that is what makes adopting an existing directory a one-line edit — and a file we did not
|
|
102
|
-
* create is not ours to remove.
|
|
103
|
-
*/
|
|
104
|
-
declare const isInsideConfigDir: (configDirectory: string, file: string) => boolean;
|
|
105
|
-
/**
|
|
106
|
-
* Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
|
|
107
|
-
*
|
|
108
|
-
* {@link credentialsPath} deliberately reads an existing legacy file in place rather than
|
|
109
|
-
* migrating it, so for a default config directory that file is ours even though it sits outside
|
|
110
|
-
* `neon/`. Judging ownership on the current directory alone would call an install that predates
|
|
111
|
-
* the rename "adopted".
|
|
112
|
-
*/
|
|
113
|
-
declare const isOwnedCredentialPath: (configDirectory: string, file: string) => boolean;
|
|
114
|
-
//#endregion
|
|
115
|
-
export { CONFIG_DIR_NAME, CREDENTIALS_FILE, ConfigPathOptions, LEGACY_CONFIG_DIR_NAME, ResolvedConfigFile, configDir, credentialsPath, defaultDir, isInsideConfigDir, isOwnedCredentialPath, legacyConfigDir, resolveConfigFile };
|
|
116
|
-
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","names":[],"sources":["../../src/_shared/paths.ts"],"mappings":";;AA2CA;AAGA;AAEA;AAWA;AAUA;AAOA;AAgBA;AAAiC;AAEvB;AACP;AAAkB;AA4CrB;AAQA;AAcA;AAUA;AAaA;;;;;;;;;;;;;;;;;;;;;;;cA7Ia,eAAA;;cAGA,sBAAA;UAEI,iBAAA;;;;;;;QAOV,MAAA,CAAO;;;iBAIE,SAAA,WAAmB;;;;;iBAUnB,eAAA,WACN;UAMO,kBAAA;;;;;;;;;;;;;;;iBAgBD,iBAAA,6BAEN,oBACP;cA4CU,gBAAA;;;;;;;cAQA;;;;;;;;;;;;;cAcA;;;;;;;;cAUA;;;;;;;;;cAaA"}
|
package/dist/_shared/paths.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
//#region src/_shared/paths.ts
|
|
4
|
-
/**
|
|
5
|
-
* # Where the Neon CLIs keep their files on disk
|
|
6
|
-
*
|
|
7
|
-
**Deliberately impure.** It reads environment variables and touches the filesystem, which
|
|
8
|
-
* `@neon/config` — the package this used to be a subpath of — must never do from its root
|
|
9
|
-
* export. It lives here instead of there precisely so that a policy-facing package does not
|
|
10
|
-
* carry implementor-only code.
|
|
11
|
-
*
|
|
12
|
-
* It exists because three separate readers each grew their own answer to "where is the
|
|
13
|
-
* config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
|
|
14
|
-
* not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
|
|
15
|
-
* flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
|
|
16
|
-
* credentials somewhere the other two never looked.
|
|
17
|
-
*
|
|
18
|
-
* ## The directory
|
|
19
|
-
*
|
|
20
|
-
* `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
|
|
21
|
-
* each entry winning over the next:
|
|
22
|
-
*
|
|
23
|
-
* 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
|
|
24
|
-
* 2. `NEON_CONFIG_DIR` — exact.
|
|
25
|
-
* 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
|
|
26
|
-
* 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
|
|
27
|
-
*
|
|
28
|
-
* An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
|
|
29
|
-
* quietly read `~/.config/neonctl` would defeat the point of passing it.
|
|
30
|
-
*
|
|
31
|
-
* ## The files
|
|
32
|
-
*
|
|
33
|
-
* {@link resolveConfigFile} answers "which path should I use for this file", and it is the
|
|
34
|
-
* same answer for reading and writing:
|
|
35
|
-
*
|
|
36
|
-
* - Present in `neon/` → use it.
|
|
37
|
-
* - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
|
|
38
|
-
* never copied or moved, so nothing is left behind to go stale and no other tool starts
|
|
39
|
-
* reading an abandoned token.
|
|
40
|
-
* - Present in neither → the new location. New files only ever appear under `neon/`.
|
|
41
|
-
*/
|
|
42
|
-
/** Current directory name. New files are created here. */
|
|
43
|
-
const CONFIG_DIR_NAME = "neon";
|
|
44
|
-
/** Legacy directory name, read forever so existing installs keep working untouched. */
|
|
45
|
-
const LEGACY_CONFIG_DIR_NAME = "neonctl";
|
|
46
|
-
/** Where files are created. See the module docs for the precedence. */
|
|
47
|
-
function configDir(options = {}) {
|
|
48
|
-
const explicit = explicitDir(options);
|
|
49
|
-
if (explicit) return explicit;
|
|
50
|
-
return join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The legacy directory, or `undefined` when the location was chosen explicitly (in which
|
|
54
|
-
* case there is no legacy counterpart to fall back to).
|
|
55
|
-
*/
|
|
56
|
-
function legacyConfigDir(options = {}) {
|
|
57
|
-
if (explicitDir(options)) return void 0;
|
|
58
|
-
return join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Resolve one file inside the config directory. Prefers the current location, falls back to
|
|
62
|
-
* an existing legacy file **in place**, and otherwise points at the current location so new
|
|
63
|
-
* files are created there.
|
|
64
|
-
*/
|
|
65
|
-
function resolveConfigFile(fileName, options = {}) {
|
|
66
|
-
const dir = configDir(options);
|
|
67
|
-
const current = resolve(dir, fileName);
|
|
68
|
-
if (existsSync(current)) return {
|
|
69
|
-
path: current,
|
|
70
|
-
dir,
|
|
71
|
-
isLegacy: false,
|
|
72
|
-
exists: true
|
|
73
|
-
};
|
|
74
|
-
const legacyDir = legacyConfigDir(options);
|
|
75
|
-
if (legacyDir) {
|
|
76
|
-
const legacy = resolve(legacyDir, fileName);
|
|
77
|
-
if (existsSync(legacy)) return {
|
|
78
|
-
path: legacy,
|
|
79
|
-
dir: legacyDir,
|
|
80
|
-
isLegacy: true,
|
|
81
|
-
exists: true
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
path: current,
|
|
86
|
-
dir,
|
|
87
|
-
isLegacy: false,
|
|
88
|
-
exists: false
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
/** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */
|
|
92
|
-
function configHome(env) {
|
|
93
|
-
const xdg = nonEmpty(env.XDG_CONFIG_HOME);
|
|
94
|
-
if (xdg) return xdg;
|
|
95
|
-
const home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);
|
|
96
|
-
return home ? join(home, ".config") : ".config";
|
|
97
|
-
}
|
|
98
|
-
function explicitDir(options) {
|
|
99
|
-
const env = options.env ?? process.env;
|
|
100
|
-
return nonEmpty(options.dir) ?? nonEmpty(env.NEON_CONFIG_DIR) ?? nonEmpty(env.NEONCTL_CONFIG_DIR);
|
|
101
|
-
}
|
|
102
|
-
function nonEmpty(value) {
|
|
103
|
-
if (typeof value !== "string") return void 0;
|
|
104
|
-
const trimmed = value.trim();
|
|
105
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
106
|
-
}
|
|
107
|
-
const CREDENTIALS_FILE = "credentials.json";
|
|
108
|
-
/**
|
|
109
|
-
* Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
|
|
110
|
-
*
|
|
111
|
-
* The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
|
|
112
|
-
* see {@link credentialsPath} — but it is never written to, moved, or deleted.
|
|
113
|
-
*/
|
|
114
|
-
const defaultDir = configDir();
|
|
115
|
-
/**
|
|
116
|
-
* Where this invocation's `credentials.json` lives.
|
|
117
|
-
*
|
|
118
|
-
* When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
|
|
119
|
-
* directory is used **in place**: an install that predates the rename keeps working, and its
|
|
120
|
-
* credentials are never duplicated into a second location where one copy could go stale while
|
|
121
|
-
* another tool still reads it.
|
|
122
|
-
*
|
|
123
|
-
* A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
|
|
124
|
-
* explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
|
|
125
|
-
* scratch directory must never pick up a developer's real credentials.
|
|
126
|
-
*/
|
|
127
|
-
const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
128
|
-
/**
|
|
129
|
-
* Whether a credentials file is one the CLI created, rather than a path a profile adopted.
|
|
130
|
-
*
|
|
131
|
-
* Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
|
|
132
|
-
* that is what makes adopting an existing directory a one-line edit — and a file we did not
|
|
133
|
-
* create is not ours to remove.
|
|
134
|
-
*/
|
|
135
|
-
const isInsideConfigDir = (configDirectory, file) => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);
|
|
136
|
-
/**
|
|
137
|
-
* Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
|
|
138
|
-
*
|
|
139
|
-
* {@link credentialsPath} deliberately reads an existing legacy file in place rather than
|
|
140
|
-
* migrating it, so for a default config directory that file is ours even though it sits outside
|
|
141
|
-
* `neon/`. Judging ownership on the current directory alone would call an install that predates
|
|
142
|
-
* the rename "adopted".
|
|
143
|
-
*/
|
|
144
|
-
const isOwnedCredentialPath = (configDirectory, file) => {
|
|
145
|
-
if (isInsideConfigDir(configDirectory, file)) return true;
|
|
146
|
-
if (configDirectory !== defaultDir) return false;
|
|
147
|
-
const legacy = legacyConfigDir();
|
|
148
|
-
return legacy !== void 0 && isInsideConfigDir(legacy, file);
|
|
149
|
-
};
|
|
150
|
-
//#endregion
|
|
151
|
-
export { CONFIG_DIR_NAME, CREDENTIALS_FILE, LEGACY_CONFIG_DIR_NAME, configDir, credentialsPath, defaultDir, isInsideConfigDir, isOwnedCredentialPath, legacyConfigDir, resolveConfigFile };
|
|
152
|
-
|
|
153
|
-
//# sourceMappingURL=paths.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","names":[],"sources":["../../src/_shared/paths.ts"],"sourcesContent":["/**\n * # Where the Neon CLIs keep their files on disk\n *\n **Deliberately impure.** It reads environment variables and touches the filesystem, which\n * `@neon/config` — the package this used to be a subpath of — must never do from its root\n * export. It lives here instead of there precisely so that a policy-facing package does not\n * carry implementor-only code.\n *\n * It exists because three separate readers each grew their own answer to \"where is the\n * config directory\", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but\n * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init\n * flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote\n * credentials somewhere the other two never looked.\n *\n * ## The directory\n *\n * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,\n * each entry winning over the next:\n *\n * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.\n * 2. `NEON_CONFIG_DIR` — exact.\n * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.\n * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.\n *\n * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that\n * quietly read `~/.config/neonctl` would defeat the point of passing it.\n *\n * ## The files\n *\n * {@link resolveConfigFile} answers \"which path should I use for this file\", and it is the\n * same answer for reading and writing:\n *\n * - Present in `neon/` → use it.\n * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is\n * never copied or moved, so nothing is left behind to go stale and no other tool starts\n * reading an abandoned token.\n * - Present in neither → the new location. New files only ever appear under `neon/`.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\n/** Current directory name. New files are created here. */\nexport const CONFIG_DIR_NAME = \"neon\";\n\n/** Legacy directory name, read forever so existing installs keep working untouched. */\nexport const LEGACY_CONFIG_DIR_NAME = \"neonctl\";\n\nexport interface ConfigPathOptions {\n\t/**\n\t * An explicit directory, e.g. from a `--config-dir` flag. Used exactly as given: no\n\t * environment variables are consulted and the legacy directory is never searched.\n\t */\n\tdir?: string;\n\t/** Environment to read. Defaults to `process.env`. Injectable for tests. */\n\tenv?: NodeJS.ProcessEnv;\n}\n\n/** Where files are created. See the module docs for the precedence. */\nexport function configDir(options: ConfigPathOptions = {}): string {\n\tconst explicit = explicitDir(options);\n\tif (explicit) return explicit;\n\treturn join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);\n}\n\n/**\n * The legacy directory, or `undefined` when the location was chosen explicitly (in which\n * case there is no legacy counterpart to fall back to).\n */\nexport function legacyConfigDir(\n\toptions: ConfigPathOptions = {},\n): string | undefined {\n\tif (explicitDir(options)) return undefined;\n\treturn join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);\n}\n\nexport interface ResolvedConfigFile {\n\t/** The path to use, for both reading and writing. */\n\tpath: string;\n\t/** The directory `path` lives in. */\n\tdir: string;\n\t/** True when the file was found in the legacy `neonctl` directory. */\n\tisLegacy: boolean;\n\t/** Whether the file exists at `path` right now. */\n\texists: boolean;\n}\n\n/**\n * Resolve one file inside the config directory. Prefers the current location, falls back to\n * an existing legacy file **in place**, and otherwise points at the current location so new\n * files are created there.\n */\nexport function resolveConfigFile(\n\tfileName: string,\n\toptions: ConfigPathOptions = {},\n): ResolvedConfigFile {\n\tconst dir = configDir(options);\n\tconst current = resolve(dir, fileName);\n\tif (existsSync(current))\n\t\treturn { path: current, dir, isLegacy: false, exists: true };\n\n\tconst legacyDir = legacyConfigDir(options);\n\tif (legacyDir) {\n\t\tconst legacy = resolve(legacyDir, fileName);\n\t\tif (existsSync(legacy))\n\t\t\treturn {\n\t\t\t\tpath: legacy,\n\t\t\t\tdir: legacyDir,\n\t\t\t\tisLegacy: true,\n\t\t\t\texists: true,\n\t\t\t};\n\t}\n\n\treturn { path: current, dir, isLegacy: false, exists: false };\n}\n\n/** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */\nfunction configHome(env: NodeJS.ProcessEnv): string {\n\tconst xdg = nonEmpty(env.XDG_CONFIG_HOME);\n\tif (xdg) return xdg;\n\tconst home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);\n\treturn home ? join(home, \".config\") : \".config\";\n}\n\nfunction explicitDir(options: ConfigPathOptions): string | undefined {\n\tconst env = options.env ?? process.env;\n\treturn (\n\t\tnonEmpty(options.dir) ??\n\t\tnonEmpty(env.NEON_CONFIG_DIR) ??\n\t\tnonEmpty(env.NEONCTL_CONFIG_DIR)\n\t);\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\treturn trimmed === \"\" ? undefined : trimmed;\n}\n\nexport const CREDENTIALS_FILE = \"credentials.json\";\n\n/**\n * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.\n *\n * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —\n * see {@link credentialsPath} — but it is never written to, moved, or deleted.\n */\nexport const defaultDir = configDir();\n\n/**\n * Where this invocation's `credentials.json` lives.\n *\n * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`\n * directory is used **in place**: an install that predates the rename keeps working, and its\n * credentials are never duplicated into a second location where one copy could go stale while\n * another tool still reads it.\n *\n * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an\n * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a\n * scratch directory must never pick up a developer's real credentials.\n */\nexport const credentialsPath = (dir: string): string =>\n\tresolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;\n\n/**\n * Whether a credentials file is one the CLI created, rather than a path a profile adopted.\n *\n * Anything that deletes a credential has to ask this first. A profile entry may point anywhere —\n * that is what makes adopting an existing directory a one-line edit — and a file we did not\n * create is not ours to remove.\n */\nexport const isInsideConfigDir = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);\n\n/**\n * Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.\n *\n * {@link credentialsPath} deliberately reads an existing legacy file in place rather than\n * migrating it, so for a default config directory that file is ours even though it sits outside\n * `neon/`. Judging ownership on the current directory alone would call an install that predates\n * the rename \"adopted\".\n */\nexport const isOwnedCredentialPath = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => {\n\tif (isInsideConfigDir(configDirectory, file)) return true;\n\tif (configDirectory !== defaultDir) return false;\n\tconst legacy = legacyConfigDir();\n\treturn legacy !== undefined && isInsideConfigDir(legacy, file);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,kBAAkB;;AAG/B,MAAa,yBAAyB;;AAatC,SAAgB,UAAU,UAA6B,CAAC,GAAW;CAClE,MAAM,WAAW,YAAY,OAAO;CACpC,IAAI,UAAU,OAAO;CACrB,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,eAAe;AACpE;;;;;AAMA,SAAgB,gBACf,UAA6B,CAAC,GACT;CACrB,IAAI,YAAY,OAAO,GAAG,OAAO,KAAA;CACjC,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,sBAAsB;AAC3E;;;;;;AAkBA,SAAgB,kBACf,UACA,UAA6B,CAAC,GACT;CACrB,MAAM,MAAM,UAAU,OAAO;CAC7B,MAAM,UAAU,QAAQ,KAAK,QAAQ;CACrC,IAAI,WAAW,OAAO,GACrB,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAK;CAE5D,MAAM,YAAY,gBAAgB,OAAO;CACzC,IAAI,WAAW;EACd,MAAM,SAAS,QAAQ,WAAW,QAAQ;EAC1C,IAAI,WAAW,MAAM,GACpB,OAAO;GACN,MAAM;GACN,KAAK;GACL,UAAU;GACV,QAAQ;EACT;CACF;CAEA,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAM;AAC7D;;AAGA,SAAS,WAAW,KAAgC;CACnD,MAAM,MAAM,SAAS,IAAI,eAAe;CACxC,IAAI,KAAK,OAAO;CAChB,MAAM,OAAO,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,WAAW;CAC3D,OAAO,OAAO,KAAK,MAAM,SAAS,IAAI;AACvC;AAEA,SAAS,YAAY,SAAgD;CACpE,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,OACC,SAAS,QAAQ,GAAG,KACpB,SAAS,IAAI,eAAe,KAC5B,SAAS,IAAI,kBAAkB;AAEjC;AAEA,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC;AAEA,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,aAAa,UAAU;;;;;;;;;;;;;AAcpC,MAAa,mBAAmB,QAC/B,kBAAkB,kBAAkB,QAAQ,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;;;;;;;;AASxE,MAAa,qBACZ,iBACA,SACa,GAAG,QAAQ,IAAI,EAAE,GAAG,WAAW,GAAG,QAAQ,eAAe,EAAE,EAAE;;;;;;;;;AAU3E,MAAa,yBACZ,iBACA,SACa;CACb,IAAI,kBAAkB,iBAAiB,IAAI,GAAG,OAAO;CACrD,IAAI,oBAAoB,YAAY,OAAO;CAC3C,MAAM,SAAS,gBAAgB;CAC/B,OAAO,WAAW,KAAA,KAAa,kBAAkB,QAAQ,IAAI;AAC9D"}
|