@neon/config 0.0.0 → 0.8.1

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.
Files changed (60) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +146 -0
  3. package/dist/index.d.ts +11 -0
  4. package/dist/index.js +10 -0
  5. package/dist/lib/auth.d.ts +67 -0
  6. package/dist/lib/auth.d.ts.map +1 -0
  7. package/dist/lib/auth.js +107 -0
  8. package/dist/lib/auth.js.map +1 -0
  9. package/dist/lib/credentials.d.ts +37 -0
  10. package/dist/lib/credentials.d.ts.map +1 -0
  11. package/dist/lib/credentials.js +30 -0
  12. package/dist/lib/credentials.js.map +1 -0
  13. package/dist/lib/define-config.d.ts +123 -0
  14. package/dist/lib/define-config.d.ts.map +1 -0
  15. package/dist/lib/define-config.js +168 -0
  16. package/dist/lib/define-config.js.map +1 -0
  17. package/dist/lib/diff.d.ts +120 -0
  18. package/dist/lib/diff.d.ts.map +1 -0
  19. package/dist/lib/diff.js +284 -0
  20. package/dist/lib/diff.js.map +1 -0
  21. package/dist/lib/duration.d.ts +68 -0
  22. package/dist/lib/duration.d.ts.map +1 -0
  23. package/dist/lib/duration.js +111 -0
  24. package/dist/lib/duration.js.map +1 -0
  25. package/dist/lib/errors.d.ts +140 -0
  26. package/dist/lib/errors.d.ts.map +1 -0
  27. package/dist/lib/errors.js +185 -0
  28. package/dist/lib/errors.js.map +1 -0
  29. package/dist/lib/loader.d.ts +44 -0
  30. package/dist/lib/loader.d.ts.map +1 -0
  31. package/dist/lib/loader.js +120 -0
  32. package/dist/lib/loader.js.map +1 -0
  33. package/dist/lib/neon-api-real.d.ts +92 -0
  34. package/dist/lib/neon-api-real.d.ts.map +1 -0
  35. package/dist/lib/neon-api-real.js +957 -0
  36. package/dist/lib/neon-api-real.js.map +1 -0
  37. package/dist/lib/neon-api.d.ts +373 -0
  38. package/dist/lib/neon-api.d.ts.map +1 -0
  39. package/dist/lib/neon-api.js +1 -0
  40. package/dist/lib/patterns.d.ts +43 -0
  41. package/dist/lib/patterns.d.ts.map +1 -0
  42. package/dist/lib/patterns.js +76 -0
  43. package/dist/lib/patterns.js.map +1 -0
  44. package/dist/lib/schema.d.ts +215 -0
  45. package/dist/lib/schema.d.ts.map +1 -0
  46. package/dist/lib/schema.js +284 -0
  47. package/dist/lib/schema.js.map +1 -0
  48. package/dist/lib/types.d.ts +546 -0
  49. package/dist/lib/types.d.ts.map +1 -0
  50. package/dist/lib/types.js +18 -0
  51. package/dist/lib/types.js.map +1 -0
  52. package/dist/lib/wrap-neon-error.d.ts +30 -0
  53. package/dist/lib/wrap-neon-error.d.ts.map +1 -0
  54. package/dist/lib/wrap-neon-error.js +139 -0
  55. package/dist/lib/wrap-neon-error.js.map +1 -0
  56. package/dist/v1.d.ts +211 -0
  57. package/dist/v1.d.ts.map +1 -0
  58. package/dist/v1.js +82 -0
  59. package/dist/v1.js.map +1 -0
  60. package/package.json +57 -18
@@ -0,0 +1,140 @@
1
+ import { ConflictReport } from "./types.js";
2
+
3
+ //#region src/lib/errors.d.ts
4
+
5
+ /**
6
+ * Every code a {@link PlatformError} can carry. Stable identifiers — consumers can rely on
7
+ * these for `instanceof PlatformError && err.code === ErrorCode.…` style checks instead of
8
+ * matching on free-text messages.
9
+ *
10
+ * Grouped by source:
11
+ * - `PLATFORM_INVALID_CONFIG` — `defineConfig` / `configSchema` rejected the input.
12
+ * - `PLATFORM_MISSING_CONTEXT` — no project / branch context could be resolved.
13
+ * - `PLATFORM_PUSH_CONFLICT` — local config conflicts with remote and the caller did not
14
+ * opt in to apply.
15
+ * - `PLATFORM_CONFIG_LOAD_FAILED` — `neon.ts` could not be found or evaluated.
16
+ * - `PLATFORM_MISSING_API_KEY` — no `NEON_API_KEY` and no explicit `apiKey` was provided.
17
+ * - `PLATFORM_MISSING_PARENT_BRANCH` — push tried to create a child of a non-existent
18
+ * branch.
19
+ * - `PLATFORM_UNAUTHORIZED` / `PLATFORM_FORBIDDEN` / `PLATFORM_NOT_FOUND` /
20
+ * `PLATFORM_CONFLICT` / `PLATFORM_RATE_LIMITED` / `PLATFORM_LOCKED` /
21
+ * `PLATFORM_SERVER_ERROR` — wrappings of Neon HTTP failures.
22
+ * - `PLATFORM_NETWORK_ERROR` — transport-level failure (no HTTP response at all).
23
+ * - `PLATFORM_INTERNAL_ERROR` — invariant violations. Should never happen in production;
24
+ * if you see one, please open an issue.
25
+ */
26
+ declare const ErrorCode: {
27
+ readonly InvalidConfig: "PLATFORM_INVALID_CONFIG";
28
+ readonly EnvNotInjected: "PLATFORM_ENV_NOT_INJECTED";
29
+ readonly MissingContext: "PLATFORM_MISSING_CONTEXT";
30
+ readonly PushConflict: "PLATFORM_PUSH_CONFLICT";
31
+ readonly PushAborted: "PLATFORM_PUSH_ABORTED";
32
+ readonly ConfigLoadFailed: "PLATFORM_CONFIG_LOAD_FAILED";
33
+ readonly MissingApiKey: "PLATFORM_MISSING_API_KEY";
34
+ readonly AmbiguousBranchAuth: "PLATFORM_AMBIGUOUS_BRANCH_AUTH";
35
+ readonly BranchNotFound: "PLATFORM_BRANCH_NOT_FOUND";
36
+ readonly FeatureUnavailable: "PLATFORM_FEATURE_UNAVAILABLE";
37
+ readonly MissingParentBranch: "PLATFORM_MISSING_PARENT_BRANCH";
38
+ readonly Unauthorized: "PLATFORM_UNAUTHORIZED";
39
+ readonly Forbidden: "PLATFORM_FORBIDDEN";
40
+ readonly NotFound: "PLATFORM_NOT_FOUND";
41
+ readonly Conflict: "PLATFORM_CONFLICT";
42
+ readonly RateLimited: "PLATFORM_RATE_LIMITED";
43
+ readonly Locked: "PLATFORM_LOCKED";
44
+ readonly ServerError: "PLATFORM_SERVER_ERROR";
45
+ readonly NetworkError: "PLATFORM_NETWORK_ERROR";
46
+ readonly InternalError: "PLATFORM_INTERNAL_ERROR";
47
+ };
48
+ type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
49
+ /**
50
+ * Base class for all errors thrown by `@neon/config`. Always extend this so callers
51
+ * can catch every package-thrown error with a single `instanceof` check.
52
+ *
53
+ * Optional `details` carries structured context that the CLI prints under `--debug` and
54
+ * that programmatic consumers can read (e.g. `details.status` for HTTP wrappings,
55
+ * `details.requestId` for Neon API failures).
56
+ */
57
+ declare class PlatformError extends Error {
58
+ readonly name: string;
59
+ readonly code: string;
60
+ readonly details: Readonly<Record<string, unknown>>;
61
+ constructor(code: string, message: string, options?: {
62
+ cause?: unknown;
63
+ details?: Record<string, unknown>;
64
+ });
65
+ }
66
+ /**
67
+ * Structural check for a {@link PlatformError}.
68
+ *
69
+ * Prefer this over a bare `instanceof PlatformError` whenever the error may have crossed a
70
+ * module-realm boundary. A `neon.ts` loaded through jiti imports its *own* copy of this
71
+ * package, so a `PlatformError` thrown while evaluating it has a different class identity
72
+ * than ours and fails `instanceof` — but its stable string `code` (always prefixed
73
+ * `PLATFORM_`) survives the boundary intact, which is what we match on here.
74
+ */
75
+ declare function isPlatformError(value: unknown): value is PlatformError;
76
+ /**
77
+ * Append a "report-a-bug" footer to an error message. Used only on truly unreachable
78
+ * internal errors — never on user-facing validation / configuration errors where the user
79
+ * is supposed to fix something on their end.
80
+ */
81
+ declare function bugReportFooter(): string;
82
+ /**
83
+ * Thrown by {@link defineConfig} when the user-provided configuration object is invalid.
84
+ *
85
+ * The class collects every validation failure rather than throwing on the first one so that
86
+ * users get a complete picture of what is wrong with their `neon.ts`.
87
+ */
88
+ declare class ConfigValidationError extends PlatformError {
89
+ readonly name = "ConfigValidationError";
90
+ readonly issues: readonly string[];
91
+ constructor(issues: readonly string[]);
92
+ }
93
+ /**
94
+ * Thrown when the package cannot resolve which Neon project to operate on.
95
+ *
96
+ * Per the package's read-only-filesystem contract, we never create a `.neon` context file;
97
+ * callers must either pass `projectId`/`orgId` explicitly or rely on an existing context file
98
+ * (`.neon/project.json` or neonctl's `.neon`).
99
+ */
100
+ declare class MissingContextError extends PlatformError {
101
+ readonly name = "MissingContextError";
102
+ constructor(message: string);
103
+ }
104
+ /**
105
+ * Thrown by {@link pushConfig} when it detects differences between the local config and
106
+ * the remote project that the caller hasn't opted in to apply.
107
+ *
108
+ * The message lists every conflict with both the current and desired value plus a
109
+ * per-conflict hint. Mutable branch drift is applied by passing `updateExisting: true`.
110
+ */
111
+ declare class PushConflictError extends PlatformError {
112
+ readonly name = "PushConflictError";
113
+ readonly conflicts: readonly ConflictReport[];
114
+ constructor(conflicts: readonly ConflictReport[]);
115
+ }
116
+ /**
117
+ * Thrown by {@link pushConfig} when the caller-supplied `confirm` callback declines a
118
+ * push that requires confirmation (protected branch and/or mutable drift overriding
119
+ * existing remote settings).
120
+ *
121
+ * The CLI maps this to a non-zero exit so users see "aborted" rather than a stack trace.
122
+ */
123
+ declare class PushAbortedError extends PlatformError {
124
+ readonly name = "PushAbortedError";
125
+ readonly branchName: string;
126
+ readonly reasons: readonly ("protected-branch" | "override-updates")[];
127
+ constructor(branchName: string, reasons: readonly ("protected-branch" | "override-updates")[]);
128
+ }
129
+ /**
130
+ * Thrown when the SDK fails to find or load a `neon.ts` config file.
131
+ */
132
+ declare class ConfigLoadError extends PlatformError {
133
+ readonly name = "ConfigLoadError";
134
+ constructor(message: string, options?: {
135
+ cause?: unknown;
136
+ });
137
+ }
138
+ //#endregion
139
+ export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, bugReportFooter, isPlatformError };
140
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../../src/lib/errors.ts"],"mappings":";;;;;;AAuBA;AAsBA;;;;AAAiE;AAYjE;;;;;;AAAwC;AAyBxC;AAaA;AAUA;AAoBA;AAeA;;AAE8B,cAvHjB,SAuHiB,EAAA;WAEG,aAAA,EAAA,yBAAA;WAJM,cAAA,EAAA,2BAAA;EAAa,SAAA,cAAA,EAAA,0BAAA;EAqDvC,SAAA,YAAiB,EAAA,wBAAqB;EA8BtC,SAAA,WAAgB,EAAA,uBAAqB;;;;;;;;;;;;;;;;;KAlLtC,SAAA,WAAoB,wBAAwB;;;;;;;;;cAY3C,aAAA,SAAsB,KAAA;;;oBAGhB,SAAS;;;cAKa;;;;;;;;;;;;iBAiBzB,eAAA,2BAA0C;;;;;;iBAa1C,eAAA,CAAA;;;;;;;cAUH,qBAAA,SAA8B,aAAa;;;;;;;;;;;;cAoB3C,mBAAA,SAA4B,aAAa;;;;;;;;;;;cAezC,iBAAA,SAA0B,aAAA;;+BAET;kCAEG;;;;;;;;;cAiDpB,gBAAA,SAAyB,aAAa;;;;;;;;;cA8BtC,eAAA,SAAwB,aAAa"}
@@ -0,0 +1,185 @@
1
+ //#region src/lib/errors.ts
2
+ /**
3
+ * Every code a {@link PlatformError} can carry. Stable identifiers — consumers can rely on
4
+ * these for `instanceof PlatformError && err.code === ErrorCode.…` style checks instead of
5
+ * matching on free-text messages.
6
+ *
7
+ * Grouped by source:
8
+ * - `PLATFORM_INVALID_CONFIG` — `defineConfig` / `configSchema` rejected the input.
9
+ * - `PLATFORM_MISSING_CONTEXT` — no project / branch context could be resolved.
10
+ * - `PLATFORM_PUSH_CONFLICT` — local config conflicts with remote and the caller did not
11
+ * opt in to apply.
12
+ * - `PLATFORM_CONFIG_LOAD_FAILED` — `neon.ts` could not be found or evaluated.
13
+ * - `PLATFORM_MISSING_API_KEY` — no `NEON_API_KEY` and no explicit `apiKey` was provided.
14
+ * - `PLATFORM_MISSING_PARENT_BRANCH` — push tried to create a child of a non-existent
15
+ * branch.
16
+ * - `PLATFORM_UNAUTHORIZED` / `PLATFORM_FORBIDDEN` / `PLATFORM_NOT_FOUND` /
17
+ * `PLATFORM_CONFLICT` / `PLATFORM_RATE_LIMITED` / `PLATFORM_LOCKED` /
18
+ * `PLATFORM_SERVER_ERROR` — wrappings of Neon HTTP failures.
19
+ * - `PLATFORM_NETWORK_ERROR` — transport-level failure (no HTTP response at all).
20
+ * - `PLATFORM_INTERNAL_ERROR` — invariant violations. Should never happen in production;
21
+ * if you see one, please open an issue.
22
+ */
23
+ const ErrorCode = {
24
+ InvalidConfig: "PLATFORM_INVALID_CONFIG",
25
+ EnvNotInjected: "PLATFORM_ENV_NOT_INJECTED",
26
+ MissingContext: "PLATFORM_MISSING_CONTEXT",
27
+ PushConflict: "PLATFORM_PUSH_CONFLICT",
28
+ PushAborted: "PLATFORM_PUSH_ABORTED",
29
+ ConfigLoadFailed: "PLATFORM_CONFIG_LOAD_FAILED",
30
+ MissingApiKey: "PLATFORM_MISSING_API_KEY",
31
+ AmbiguousBranchAuth: "PLATFORM_AMBIGUOUS_BRANCH_AUTH",
32
+ BranchNotFound: "PLATFORM_BRANCH_NOT_FOUND",
33
+ FeatureUnavailable: "PLATFORM_FEATURE_UNAVAILABLE",
34
+ MissingParentBranch: "PLATFORM_MISSING_PARENT_BRANCH",
35
+ Unauthorized: "PLATFORM_UNAUTHORIZED",
36
+ Forbidden: "PLATFORM_FORBIDDEN",
37
+ NotFound: "PLATFORM_NOT_FOUND",
38
+ Conflict: "PLATFORM_CONFLICT",
39
+ RateLimited: "PLATFORM_RATE_LIMITED",
40
+ Locked: "PLATFORM_LOCKED",
41
+ ServerError: "PLATFORM_SERVER_ERROR",
42
+ NetworkError: "PLATFORM_NETWORK_ERROR",
43
+ InternalError: "PLATFORM_INTERNAL_ERROR"
44
+ };
45
+ const ISSUE_URL = "https://github.com/neondatabase/neon-pkgs/issues/new";
46
+ /**
47
+ * Base class for all errors thrown by `@neon/config`. Always extend this so callers
48
+ * can catch every package-thrown error with a single `instanceof` check.
49
+ *
50
+ * Optional `details` carries structured context that the CLI prints under `--debug` and
51
+ * that programmatic consumers can read (e.g. `details.status` for HTTP wrappings,
52
+ * `details.requestId` for Neon API failures).
53
+ */
54
+ var PlatformError = class extends Error {
55
+ name = "PlatformError";
56
+ code;
57
+ details;
58
+ constructor(code, message, options) {
59
+ super(message, options);
60
+ this.code = code;
61
+ this.details = Object.freeze({ ...options?.details ?? {} });
62
+ }
63
+ };
64
+ /**
65
+ * Structural check for a {@link PlatformError}.
66
+ *
67
+ * Prefer this over a bare `instanceof PlatformError` whenever the error may have crossed a
68
+ * module-realm boundary. A `neon.ts` loaded through jiti imports its *own* copy of this
69
+ * package, so a `PlatformError` thrown while evaluating it has a different class identity
70
+ * than ours and fails `instanceof` — but its stable string `code` (always prefixed
71
+ * `PLATFORM_`) survives the boundary intact, which is what we match on here.
72
+ */
73
+ function isPlatformError(value) {
74
+ if (value instanceof PlatformError) return true;
75
+ if (typeof value !== "object" || value === null) return false;
76
+ if (!("code" in value)) return false;
77
+ const { code } = value;
78
+ return typeof code === "string" && code.startsWith("PLATFORM_");
79
+ }
80
+ /**
81
+ * Append a "report-a-bug" footer to an error message. Used only on truly unreachable
82
+ * internal errors — never on user-facing validation / configuration errors where the user
83
+ * is supposed to fix something on their end.
84
+ */
85
+ function bugReportFooter() {
86
+ return `\nThis indicates a bug in @neon/config. Please file an issue: ${ISSUE_URL}`;
87
+ }
88
+ /**
89
+ * Thrown by {@link defineConfig} when the user-provided configuration object is invalid.
90
+ *
91
+ * The class collects every validation failure rather than throwing on the first one so that
92
+ * users get a complete picture of what is wrong with their `neon.ts`.
93
+ */
94
+ var ConfigValidationError = class extends PlatformError {
95
+ name = "ConfigValidationError";
96
+ issues;
97
+ constructor(issues) {
98
+ super("PLATFORM_INVALID_CONFIG", `Invalid Neon platform config:\n - ${issues.join("\n - ")}`);
99
+ this.issues = issues;
100
+ }
101
+ };
102
+ /**
103
+ * Thrown when the package cannot resolve which Neon project to operate on.
104
+ *
105
+ * Per the package's read-only-filesystem contract, we never create a `.neon` context file;
106
+ * callers must either pass `projectId`/`orgId` explicitly or rely on an existing context file
107
+ * (`.neon/project.json` or neonctl's `.neon`).
108
+ */
109
+ var MissingContextError = class extends PlatformError {
110
+ name = "MissingContextError";
111
+ constructor(message) {
112
+ super("PLATFORM_MISSING_CONTEXT", message);
113
+ }
114
+ };
115
+ /**
116
+ * Thrown by {@link pushConfig} when it detects differences between the local config and
117
+ * the remote project that the caller hasn't opted in to apply.
118
+ *
119
+ * The message lists every conflict with both the current and desired value plus a
120
+ * per-conflict hint. Mutable branch drift is applied by passing `updateExisting: true`.
121
+ */
122
+ var PushConflictError = class extends PlatformError {
123
+ name = "PushConflictError";
124
+ conflicts;
125
+ constructor(conflicts) {
126
+ const lines = ["pushConfig refused to apply: local config conflicts with remote state.", ""];
127
+ for (const c of conflicts) lines.push(` - [${c.kind}:${c.identifier}] ${c.field}: ${c.reason}`, ` current : ${formatValue(c.current)}`, ` desired : ${formatValue(c.desired)}`, ` fix : ${suggestFix(c)}`);
128
+ const hasMutable = conflicts.some((c) => !isImmutableConflict(c));
129
+ lines.push("");
130
+ if (hasMutable) lines.push("For mutable conflicts, pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.");
131
+ super("PLATFORM_PUSH_CONFLICT", lines.join("\n"), { details: { conflicts: conflicts.map((c) => ({ ...c })) } });
132
+ this.conflicts = conflicts;
133
+ }
134
+ };
135
+ function isImmutableConflict(_c) {
136
+ return false;
137
+ }
138
+ function suggestFix(c) {
139
+ if (isImmutableConflict(c)) return "immutable on Neon — recreate the project, or change your `neon.ts` to match the remote.";
140
+ if (c.kind === "branch" && c.field === "parent") return "create the parent branch on Neon first, or change the `parent` reference to an existing branch.";
141
+ return "pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.";
142
+ }
143
+ /**
144
+ * Thrown by {@link pushConfig} when the caller-supplied `confirm` callback declines a
145
+ * push that requires confirmation (protected branch and/or mutable drift overriding
146
+ * existing remote settings).
147
+ *
148
+ * The CLI maps this to a non-zero exit so users see "aborted" rather than a stack trace.
149
+ */
150
+ var PushAbortedError = class extends PlatformError {
151
+ name = "PushAbortedError";
152
+ branchName;
153
+ reasons;
154
+ constructor(branchName, reasons) {
155
+ super("PLATFORM_PUSH_ABORTED", [
156
+ `Aborted push to branch ${JSON.stringify(branchName)}.`,
157
+ reasons.length > 0 ? `Reason${reasons.length === 1 ? "" : "s"}: ${reasons.join(", ")}.` : void 0,
158
+ "Re-run with `--update-existing` (override existing settings) or `--allow-protected-branch` (push to a protected branch) to skip the prompt."
159
+ ].filter(Boolean).join(" "), { details: {
160
+ branchName,
161
+ reasons: [...reasons]
162
+ } });
163
+ this.branchName = branchName;
164
+ this.reasons = reasons;
165
+ }
166
+ };
167
+ /**
168
+ * Thrown when the SDK fails to find or load a `neon.ts` config file.
169
+ */
170
+ var ConfigLoadError = class extends PlatformError {
171
+ name = "ConfigLoadError";
172
+ constructor(message, options) {
173
+ super("PLATFORM_CONFIG_LOAD_FAILED", message, options);
174
+ }
175
+ };
176
+ function formatValue(value) {
177
+ if (value === void 0) return "<unset>";
178
+ if (typeof value === "string") return JSON.stringify(value);
179
+ if (typeof value === "object" && value !== null) return JSON.stringify(value);
180
+ return String(value);
181
+ }
182
+ //#endregion
183
+ export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, bugReportFooter, isPlatformError };
184
+
185
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/lib/errors.ts"],"sourcesContent":["import type { ConflictReport } from \"./types.js\";\n\n/**\n * Every code a {@link PlatformError} can carry. Stable identifiers — consumers can rely on\n * these for `instanceof PlatformError && err.code === ErrorCode.…` style checks instead of\n * matching on free-text messages.\n *\n * Grouped by source:\n * - `PLATFORM_INVALID_CONFIG` — `defineConfig` / `configSchema` rejected the input.\n * - `PLATFORM_MISSING_CONTEXT` — no project / branch context could be resolved.\n * - `PLATFORM_PUSH_CONFLICT` — local config conflicts with remote and the caller did not\n * opt in to apply.\n * - `PLATFORM_CONFIG_LOAD_FAILED` — `neon.ts` could not be found or evaluated.\n * - `PLATFORM_MISSING_API_KEY` — no `NEON_API_KEY` and no explicit `apiKey` was provided.\n * - `PLATFORM_MISSING_PARENT_BRANCH` — push tried to create a child of a non-existent\n * branch.\n * - `PLATFORM_UNAUTHORIZED` / `PLATFORM_FORBIDDEN` / `PLATFORM_NOT_FOUND` /\n * `PLATFORM_CONFLICT` / `PLATFORM_RATE_LIMITED` / `PLATFORM_LOCKED` /\n * `PLATFORM_SERVER_ERROR` — wrappings of Neon HTTP failures.\n * - `PLATFORM_NETWORK_ERROR` — transport-level failure (no HTTP response at all).\n * - `PLATFORM_INTERNAL_ERROR` — invariant violations. Should never happen in production;\n * if you see one, please open an issue.\n */\nexport const ErrorCode = {\n\tInvalidConfig: \"PLATFORM_INVALID_CONFIG\",\n\tEnvNotInjected: \"PLATFORM_ENV_NOT_INJECTED\",\n\tMissingContext: \"PLATFORM_MISSING_CONTEXT\",\n\tPushConflict: \"PLATFORM_PUSH_CONFLICT\",\n\tPushAborted: \"PLATFORM_PUSH_ABORTED\",\n\tConfigLoadFailed: \"PLATFORM_CONFIG_LOAD_FAILED\",\n\tMissingApiKey: \"PLATFORM_MISSING_API_KEY\",\n\tAmbiguousBranchAuth: \"PLATFORM_AMBIGUOUS_BRANCH_AUTH\",\n\tBranchNotFound: \"PLATFORM_BRANCH_NOT_FOUND\",\n\tFeatureUnavailable: \"PLATFORM_FEATURE_UNAVAILABLE\",\n\tMissingParentBranch: \"PLATFORM_MISSING_PARENT_BRANCH\",\n\tUnauthorized: \"PLATFORM_UNAUTHORIZED\",\n\tForbidden: \"PLATFORM_FORBIDDEN\",\n\tNotFound: \"PLATFORM_NOT_FOUND\",\n\tConflict: \"PLATFORM_CONFLICT\",\n\tRateLimited: \"PLATFORM_RATE_LIMITED\",\n\tLocked: \"PLATFORM_LOCKED\",\n\tServerError: \"PLATFORM_SERVER_ERROR\",\n\tNetworkError: \"PLATFORM_NETWORK_ERROR\",\n\tInternalError: \"PLATFORM_INTERNAL_ERROR\",\n} as const;\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nconst ISSUE_URL = \"https://github.com/neondatabase/neon-pkgs/issues/new\";\n\n/**\n * Base class for all errors thrown by `@neon/config`. Always extend this so callers\n * can catch every package-thrown error with a single `instanceof` check.\n *\n * Optional `details` carries structured context that the CLI prints under `--debug` and\n * that programmatic consumers can read (e.g. `details.status` for HTTP wrappings,\n * `details.requestId` for Neon API failures).\n */\nexport class PlatformError extends Error {\n\toverride readonly name: string = \"PlatformError\";\n\treadonly code: string;\n\treadonly details: Readonly<Record<string, unknown>>;\n\n\tconstructor(\n\t\tcode: string,\n\t\tmessage: string,\n\t\toptions?: { cause?: unknown; details?: Record<string, unknown> },\n\t) {\n\t\tsuper(message, options);\n\t\tthis.code = code;\n\t\tthis.details = Object.freeze({ ...(options?.details ?? {}) });\n\t}\n}\n\n/**\n * Structural check for a {@link PlatformError}.\n *\n * Prefer this over a bare `instanceof PlatformError` whenever the error may have crossed a\n * module-realm boundary. A `neon.ts` loaded through jiti imports its *own* copy of this\n * package, so a `PlatformError` thrown while evaluating it has a different class identity\n * than ours and fails `instanceof` — but its stable string `code` (always prefixed\n * `PLATFORM_`) survives the boundary intact, which is what we match on here.\n */\nexport function isPlatformError(value: unknown): value is PlatformError {\n\tif (value instanceof PlatformError) return true;\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"code\" in value)) return false;\n\tconst { code } = value;\n\treturn typeof code === \"string\" && code.startsWith(\"PLATFORM_\");\n}\n\n/**\n * Append a \"report-a-bug\" footer to an error message. Used only on truly unreachable\n * internal errors — never on user-facing validation / configuration errors where the user\n * is supposed to fix something on their end.\n */\nexport function bugReportFooter(): string {\n\treturn `\\nThis indicates a bug in @neon/config. Please file an issue: ${ISSUE_URL}`;\n}\n\n/**\n * Thrown by {@link defineConfig} when the user-provided configuration object is invalid.\n *\n * The class collects every validation failure rather than throwing on the first one so that\n * users get a complete picture of what is wrong with their `neon.ts`.\n */\nexport class ConfigValidationError extends PlatformError {\n\toverride readonly name = \"ConfigValidationError\";\n\treadonly issues: readonly string[];\n\n\tconstructor(issues: readonly string[]) {\n\t\tsuper(\n\t\t\t\"PLATFORM_INVALID_CONFIG\",\n\t\t\t`Invalid Neon platform config:\\n - ${issues.join(\"\\n - \")}`,\n\t\t);\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * Thrown when the package cannot resolve which Neon project to operate on.\n *\n * Per the package's read-only-filesystem contract, we never create a `.neon` context file;\n * callers must either pass `projectId`/`orgId` explicitly or rely on an existing context file\n * (`.neon/project.json` or neonctl's `.neon`).\n */\nexport class MissingContextError extends PlatformError {\n\toverride readonly name = \"MissingContextError\";\n\n\tconstructor(message: string) {\n\t\tsuper(\"PLATFORM_MISSING_CONTEXT\", message);\n\t}\n}\n\n/**\n * Thrown by {@link pushConfig} when it detects differences between the local config and\n * the remote project that the caller hasn't opted in to apply.\n *\n * The message lists every conflict with both the current and desired value plus a\n * per-conflict hint. Mutable branch drift is applied by passing `updateExisting: true`.\n */\nexport class PushConflictError extends PlatformError {\n\toverride readonly name = \"PushConflictError\";\n\treadonly conflicts: readonly ConflictReport[];\n\n\tconstructor(conflicts: readonly ConflictReport[]) {\n\t\tconst lines: string[] = [\n\t\t\t\"pushConfig refused to apply: local config conflicts with remote state.\",\n\t\t\t\"\",\n\t\t];\n\t\tfor (const c of conflicts) {\n\t\t\tlines.push(\n\t\t\t\t` - [${c.kind}:${c.identifier}] ${c.field}: ${c.reason}`,\n\t\t\t\t` current : ${formatValue(c.current)}`,\n\t\t\t\t` desired : ${formatValue(c.desired)}`,\n\t\t\t\t` fix : ${suggestFix(c)}`,\n\t\t\t);\n\t\t}\n\t\tconst hasMutable = conflicts.some((c) => !isImmutableConflict(c));\n\t\tlines.push(\"\");\n\t\tif (hasMutable) {\n\t\t\tlines.push(\n\t\t\t\t\"For mutable conflicts, pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.\",\n\t\t\t);\n\t\t}\n\n\t\tsuper(\"PLATFORM_PUSH_CONFLICT\", lines.join(\"\\n\"), {\n\t\t\tdetails: { conflicts: conflicts.map((c) => ({ ...c })) },\n\t\t});\n\t\tthis.conflicts = conflicts;\n\t}\n}\n\nfunction isImmutableConflict(_c: ConflictReport): boolean {\n\treturn false;\n}\n\nfunction suggestFix(c: ConflictReport): string {\n\tif (isImmutableConflict(c)) {\n\t\treturn \"immutable on Neon — recreate the project, or change your `neon.ts` to match the remote.\";\n\t}\n\tif (c.kind === \"branch\" && c.field === \"parent\") {\n\t\treturn \"create the parent branch on Neon first, or change the `parent` reference to an existing branch.\";\n\t}\n\treturn \"pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.\";\n}\n\n/**\n * Thrown by {@link pushConfig} when the caller-supplied `confirm` callback declines a\n * push that requires confirmation (protected branch and/or mutable drift overriding\n * existing remote settings).\n *\n * The CLI maps this to a non-zero exit so users see \"aborted\" rather than a stack trace.\n */\nexport class PushAbortedError extends PlatformError {\n\toverride readonly name = \"PushAbortedError\";\n\treadonly branchName: string;\n\treadonly reasons: readonly (\"protected-branch\" | \"override-updates\")[];\n\n\tconstructor(\n\t\tbranchName: string,\n\t\treasons: readonly (\"protected-branch\" | \"override-updates\")[],\n\t) {\n\t\tsuper(\n\t\t\t\"PLATFORM_PUSH_ABORTED\",\n\t\t\t[\n\t\t\t\t`Aborted push to branch ${JSON.stringify(branchName)}.`,\n\t\t\t\treasons.length > 0\n\t\t\t\t\t? `Reason${reasons.length === 1 ? \"\" : \"s\"}: ${reasons.join(\", \")}.`\n\t\t\t\t\t: undefined,\n\t\t\t\t\"Re-run with `--update-existing` (override existing settings) or `--allow-protected-branch` (push to a protected branch) to skip the prompt.\",\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\" \"),\n\t\t\t{ details: { branchName, reasons: [...reasons] } },\n\t\t);\n\t\tthis.branchName = branchName;\n\t\tthis.reasons = reasons;\n\t}\n}\n\n/**\n * Thrown when the SDK fails to find or load a `neon.ts` config file.\n */\nexport class ConfigLoadError extends PlatformError {\n\toverride readonly name = \"ConfigLoadError\";\n\n\tconstructor(message: string, options?: { cause?: unknown }) {\n\t\tsuper(\"PLATFORM_CONFIG_LOAD_FAILED\", message, options);\n\t}\n}\n\nfunction formatValue(value: unknown): string {\n\tif (value === undefined) return \"<unset>\";\n\tif (typeof value === \"string\") return JSON.stringify(value);\n\tif (typeof value === \"object\" && value !== null)\n\t\treturn JSON.stringify(value);\n\treturn String(value);\n}\n"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { Config } from "./types.js";
2
+
3
+ //#region src/lib/loader.d.ts
4
+
5
+ /**
6
+ * Default file names tried (in order) when {@link loadConfigFromFile} is called without an
7
+ * explicit path. We accept `.ts` first because that's the documented format; `.mjs` and `.js`
8
+ * fall out for free since jiti handles all of them.
9
+ */
10
+ declare const DEFAULT_CONFIG_FILENAMES: readonly ["neon.ts", "neon.mts", "neon.js", "neon.mjs"];
11
+ interface LoadConfigOptions {
12
+ /** Explicit absolute or cwd-relative path to a config file. Takes precedence over the search. */
13
+ path?: string;
14
+ /** Starting directory for the upward search. Defaults to `process.cwd()`. */
15
+ cwd?: string;
16
+ /**
17
+ * Hard ceiling for the upward walk — once `current === stopAt` the search returns
18
+ * `null` even if no `.git` boundary was hit. Defaults to the OS home directory so
19
+ * stray runs from outside any repo never leak into the user's `~` files.
20
+ */
21
+ stopAt?: string;
22
+ }
23
+ /**
24
+ * Load a `neon.ts` (or any other supported extension) and return the validated {@link Config}.
25
+ *
26
+ * Behavior:
27
+ * - When `path` is set, that file is loaded directly. The file must exist and must default-export
28
+ * a value produced by `defineConfig()`.
29
+ * - When `path` is omitted, we walk up from `cwd` picking the **closest** file matching
30
+ * {@link DEFAULT_CONFIG_FILENAMES}. The walk is monorepo-friendly: intermediate
31
+ * `package.json` files do **not** stop it, so a single `neon.ts` lifted to the workspace
32
+ * root keeps working when invoked from inside any sub-package. The walk terminates at the
33
+ * first directory containing `.git`, at `stopAt`, or at the filesystem root.
34
+ *
35
+ * jiti is loaded lazily so that callers who pass an already-resolved `Config` to `pushConfig`
36
+ * never pay the import cost.
37
+ */
38
+ declare function loadConfigFromFile(options?: LoadConfigOptions): Promise<{
39
+ config: Config;
40
+ resolvedPath: string;
41
+ }>;
42
+ //#endregion
43
+ export { DEFAULT_CONFIG_FILENAMES, LoadConfigOptions, loadConfigFromFile };
44
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","names":[],"sources":["../../src/lib/loader.ts"],"mappings":";;;;;;AAaA;AAOA;AA4BA;AAAwC,cAnC3B,wBAmC2B,EAAA,SAAA,CAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,UAAA,CAAA;AAC9B,UA7BO,iBAAA,CA6BP;;MACP,CAAA,EAAA,MAAA;EAAO;;;;;;;;;;;;;;;;;;;;;;;;iBAFY,kBAAA,WACZ,oBACP;UACM"}
@@ -0,0 +1,120 @@
1
+ import { ConfigLoadError, isPlatformError } from "./errors.js";
2
+ import { defineConfig } from "./define-config.js";
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { dirname, isAbsolute, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region src/lib/loader.ts
8
+ /**
9
+ * Default file names tried (in order) when {@link loadConfigFromFile} is called without an
10
+ * explicit path. We accept `.ts` first because that's the documented format; `.mjs` and `.js`
11
+ * fall out for free since jiti handles all of them.
12
+ */
13
+ const DEFAULT_CONFIG_FILENAMES = [
14
+ "neon.ts",
15
+ "neon.mts",
16
+ "neon.js",
17
+ "neon.mjs"
18
+ ];
19
+ /**
20
+ * Load a `neon.ts` (or any other supported extension) and return the validated {@link Config}.
21
+ *
22
+ * Behavior:
23
+ * - When `path` is set, that file is loaded directly. The file must exist and must default-export
24
+ * a value produced by `defineConfig()`.
25
+ * - When `path` is omitted, we walk up from `cwd` picking the **closest** file matching
26
+ * {@link DEFAULT_CONFIG_FILENAMES}. The walk is monorepo-friendly: intermediate
27
+ * `package.json` files do **not** stop it, so a single `neon.ts` lifted to the workspace
28
+ * root keeps working when invoked from inside any sub-package. The walk terminates at the
29
+ * first directory containing `.git`, at `stopAt`, or at the filesystem root.
30
+ *
31
+ * jiti is loaded lazily so that callers who pass an already-resolved `Config` to `pushConfig`
32
+ * never pay the import cost.
33
+ */
34
+ async function loadConfigFromFile(options = {}) {
35
+ const resolvedPath = options.path ? resolveExplicitPath(options.path, options.cwd) : findDefaultConfig(options.cwd, options.stopAt);
36
+ if (!resolvedPath) throw new ConfigLoadError([
37
+ `Could not find a Neon config file while walking up from ${resolve(options.cwd ?? process.cwd())}.`,
38
+ `Looked for: ${DEFAULT_CONFIG_FILENAMES.join(", ")} (stopping at the first directory with a \`.git\`).`,
39
+ `Create one at your repository root (or anywhere on the path from cwd up to .git), or pass an explicit \`configPath\` (SDK) / \`--config <path>\` (CLI).`
40
+ ].join("\n"));
41
+ let mod;
42
+ try {
43
+ mod = await importModule(resolvedPath);
44
+ } catch (cause) {
45
+ if (isPlatformError(cause)) throw cause;
46
+ throw new ConfigLoadError([
47
+ `Failed to evaluate ${resolvedPath}.`,
48
+ `Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`,
49
+ "This is usually a TypeScript syntax error, a missing dependency, or a runtime exception inside the config file. Run the file directly (e.g. `npx tsx neon.ts`) to reproduce."
50
+ ].join("\n"), { cause });
51
+ }
52
+ const exported = extractDefaultExport(mod);
53
+ if (exported === void 0) throw new ConfigLoadError([`${resolvedPath} loaded successfully but did not default-export a config.`, "Add `export default defineConfig({ ... })` at the bottom of the file. (Named exports are ignored.)"].join("\n"));
54
+ return {
55
+ config: defineConfig(exported),
56
+ resolvedPath
57
+ };
58
+ }
59
+ function resolveExplicitPath(input, cwd) {
60
+ const base = resolve(cwd ?? process.cwd());
61
+ const abs = isAbsolute(input) ? input : resolve(base, input);
62
+ if (!existsSync(abs)) throw new ConfigLoadError(`Config file not found at ${abs}. The path was resolved from \`${input}\` against ${base}.`);
63
+ if (!statSync(abs).isFile()) throw new ConfigLoadError(`Config path ${abs} is a directory, not a file. Pass a path to the file itself (e.g. ./neon.ts).`);
64
+ return abs;
65
+ }
66
+ function findDefaultConfig(cwd, stopAt) {
67
+ let current = resolve(cwd ?? process.cwd());
68
+ const stop = resolve(stopAt ?? homedir());
69
+ let lastSeen = null;
70
+ while (true) {
71
+ for (const name of DEFAULT_CONFIG_FILENAMES) {
72
+ const candidate = resolve(current, name);
73
+ if (existsSync(candidate) && safeIsFile(candidate)) return candidate;
74
+ }
75
+ if (existsSync(resolve(current, ".git"))) return null;
76
+ if (current === stop) return null;
77
+ const parent = dirname(current);
78
+ if (parent === current || parent === lastSeen) return null;
79
+ lastSeen = current;
80
+ current = parent;
81
+ }
82
+ }
83
+ async function importModule(absPath) {
84
+ const lower = absPath.toLowerCase();
85
+ if (!(lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts"))) return import(pathToFileURL(absPath).href);
86
+ const createJiti = extractCreateJiti(await import("jiti"));
87
+ if (!createJiti) throw new ConfigLoadError(["jiti is required to load TypeScript config files but could not be initialised.", "Reinstall the package dependencies (`pnpm install` / `npm install`) — jiti is a runtime dependency of @neon/config."].join(" "));
88
+ return createJiti(pathToFileURL(absPath).href, {
89
+ interopDefault: true,
90
+ moduleCache: false
91
+ }).import(absPath);
92
+ }
93
+ function extractCreateJiti(mod) {
94
+ if (mod === null || typeof mod !== "object") return null;
95
+ const obj = mod;
96
+ if (typeof obj.createJiti === "function") return obj.createJiti;
97
+ const def = obj.default;
98
+ if (def !== null && typeof def === "object") {
99
+ const defObj = def;
100
+ if (typeof defObj.createJiti === "function") return defObj.createJiti;
101
+ }
102
+ return null;
103
+ }
104
+ function extractDefaultExport(mod) {
105
+ if (mod === null || typeof mod !== "object") return mod;
106
+ const obj = mod;
107
+ if ("default" in obj && obj.default !== void 0) return obj.default;
108
+ if (typeof mod === "function") return mod;
109
+ }
110
+ function safeIsFile(path) {
111
+ try {
112
+ return statSync(path).isFile();
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ //#endregion
118
+ export { DEFAULT_CONFIG_FILENAMES, loadConfigFromFile };
119
+
120
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","names":[],"sources":["../../src/lib/loader.ts"],"sourcesContent":["import { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { defineConfig } from \"./define-config.js\";\nimport { ConfigLoadError, isPlatformError } from \"./errors.js\";\nimport type { Config } from \"./types.js\";\n\n/**\n * Default file names tried (in order) when {@link loadConfigFromFile} is called without an\n * explicit path. We accept `.ts` first because that's the documented format; `.mjs` and `.js`\n * fall out for free since jiti handles all of them.\n */\nexport const DEFAULT_CONFIG_FILENAMES = [\n\t\"neon.ts\",\n\t\"neon.mts\",\n\t\"neon.js\",\n\t\"neon.mjs\",\n] as const;\n\nexport interface LoadConfigOptions {\n\t/** Explicit absolute or cwd-relative path to a config file. Takes precedence over the search. */\n\tpath?: string;\n\t/** Starting directory for the upward search. Defaults to `process.cwd()`. */\n\tcwd?: string;\n\t/**\n\t * Hard ceiling for the upward walk — once `current === stopAt` the search returns\n\t * `null` even if no `.git` boundary was hit. Defaults to the OS home directory so\n\t * stray runs from outside any repo never leak into the user's `~` files.\n\t */\n\tstopAt?: string;\n}\n\n/**\n * Load a `neon.ts` (or any other supported extension) and return the validated {@link Config}.\n *\n * Behavior:\n * - When `path` is set, that file is loaded directly. The file must exist and must default-export\n * a value produced by `defineConfig()`.\n * - When `path` is omitted, we walk up from `cwd` picking the **closest** file matching\n * {@link DEFAULT_CONFIG_FILENAMES}. The walk is monorepo-friendly: intermediate\n * `package.json` files do **not** stop it, so a single `neon.ts` lifted to the workspace\n * root keeps working when invoked from inside any sub-package. The walk terminates at the\n * first directory containing `.git`, at `stopAt`, or at the filesystem root.\n *\n * jiti is loaded lazily so that callers who pass an already-resolved `Config` to `pushConfig`\n * never pay the import cost.\n */\nexport async function loadConfigFromFile(\n\toptions: LoadConfigOptions = {},\n): Promise<{\n\tconfig: Config;\n\tresolvedPath: string;\n}> {\n\tconst resolvedPath = options.path\n\t\t? resolveExplicitPath(options.path, options.cwd)\n\t\t: findDefaultConfig(options.cwd, options.stopAt);\n\n\tif (!resolvedPath) {\n\t\tthrow new ConfigLoadError(\n\t\t\t[\n\t\t\t\t`Could not find a Neon config file while walking up from ${resolve(options.cwd ?? process.cwd())}.`,\n\t\t\t\t`Looked for: ${DEFAULT_CONFIG_FILENAMES.join(\", \")} (stopping at the first directory with a \\`.git\\`).`,\n\t\t\t\t`Create one at your repository root (or anywhere on the path from cwd up to .git), or pass an explicit \\`configPath\\` (SDK) / \\`--config <path>\\` (CLI).`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t}\n\n\tlet mod: unknown;\n\ttry {\n\t\tmod = await importModule(resolvedPath);\n\t} catch (cause) {\n\t\t// `defineConfig()` runs at module-eval time, so a config the user got *wrong*\n\t\t// (a bad function slug, an unknown key, an invalid duration, …) throws a\n\t\t// PlatformError from inside this import. That error already pinpoints the exact\n\t\t// field and reason, so surface it verbatim — burying it under the generic\n\t\t// \"this is usually a TypeScript syntax error\" hint sent users hunting for a\n\t\t// syntax bug that isn't there. The hint is reserved for genuine evaluation\n\t\t// failures (syntax errors, missing deps, thrown runtime exceptions).\n\t\tif (isPlatformError(cause)) {\n\t\t\tthrow cause;\n\t\t}\n\t\tthrow new ConfigLoadError(\n\t\t\t[\n\t\t\t\t`Failed to evaluate ${resolvedPath}.`,\n\t\t\t\t`Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t\t\"This is usually a TypeScript syntax error, a missing dependency, or a runtime exception inside the config file. Run the file directly (e.g. `npx tsx neon.ts`) to reproduce.\",\n\t\t\t].join(\"\\n\"),\n\t\t\t{ cause },\n\t\t);\n\t}\n\n\tconst exported = extractDefaultExport(mod);\n\tif (exported === undefined) {\n\t\tthrow new ConfigLoadError(\n\t\t\t[\n\t\t\t\t`${resolvedPath} loaded successfully but did not default-export a config.`,\n\t\t\t\t\"Add `export default defineConfig({ ... })` at the bottom of the file. (Named exports are ignored.)\",\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t}\n\n\t// Run through defineConfig to validate any function the user might have constructed manually.\n\tconst config = defineConfig(exported as Config);\n\treturn { config, resolvedPath };\n}\n\nfunction resolveExplicitPath(input: string, cwd?: string): string {\n\tconst base = resolve(cwd ?? process.cwd());\n\tconst abs = isAbsolute(input) ? input : resolve(base, input);\n\tif (!existsSync(abs)) {\n\t\tthrow new ConfigLoadError(\n\t\t\t`Config file not found at ${abs}. The path was resolved from \\`${input}\\` against ${base}.`,\n\t\t);\n\t}\n\tconst s = statSync(abs);\n\tif (!s.isFile()) {\n\t\tthrow new ConfigLoadError(\n\t\t\t`Config path ${abs} is a directory, not a file. Pass a path to the file itself (e.g. ./neon.ts).`,\n\t\t);\n\t}\n\treturn abs;\n}\n\nfunction findDefaultConfig(\n\tcwd: string | undefined,\n\tstopAt: string | undefined,\n): string | null {\n\tlet current = resolve(cwd ?? process.cwd());\n\tconst stop = resolve(stopAt ?? homedir());\n\tlet lastSeen: string | null = null;\n\n\twhile (true) {\n\t\tfor (const name of DEFAULT_CONFIG_FILENAMES) {\n\t\t\tconst candidate = resolve(current, name);\n\t\t\tif (existsSync(candidate) && safeIsFile(candidate))\n\t\t\t\treturn candidate;\n\t\t}\n\n\t\t// `.git` is the canonical repo-root marker. `package.json` is deliberately *not*\n\t\t// a stop: monorepos lift `neon.ts` above sub-package package.jsons.\n\t\tif (existsSync(resolve(current, \".git\"))) return null;\n\t\tif (current === stop) return null;\n\n\t\tconst parent = dirname(current);\n\t\tif (parent === current || parent === lastSeen) return null;\n\t\tlastSeen = current;\n\t\tcurrent = parent;\n\t}\n}\n\nasync function importModule(absPath: string): Promise<unknown> {\n\tconst lower = absPath.toLowerCase();\n\tconst needsJiti =\n\t\tlower.endsWith(\".ts\") ||\n\t\tlower.endsWith(\".mts\") ||\n\t\tlower.endsWith(\".cts\");\n\n\tif (!needsJiti) {\n\t\treturn import(pathToFileURL(absPath).href);\n\t}\n\n\tconst jitiModule: unknown = await import(\"jiti\");\n\tconst createJiti = extractCreateJiti(jitiModule);\n\tif (!createJiti) {\n\t\tthrow new ConfigLoadError(\n\t\t\t[\n\t\t\t\t\"jiti is required to load TypeScript config files but could not be initialised.\",\n\t\t\t\t\"Reinstall the package dependencies (`pnpm install` / `npm install`) — jiti is a runtime dependency of @neon/config.\",\n\t\t\t].join(\" \"),\n\t\t);\n\t}\n\tconst jiti = createJiti(pathToFileURL(absPath).href, {\n\t\tinteropDefault: true,\n\t\tmoduleCache: false,\n\t});\n\treturn jiti.import(absPath);\n}\n\nfunction extractCreateJiti(\n\tmod: unknown,\n): ((id: string, options?: unknown) => JitiInstance) | null {\n\tif (mod === null || typeof mod !== \"object\") return null;\n\tconst obj = mod as Record<string, unknown>;\n\tif (typeof obj.createJiti === \"function\") {\n\t\treturn obj.createJiti as (\n\t\t\tid: string,\n\t\t\toptions?: unknown,\n\t\t) => JitiInstance;\n\t}\n\tconst def = obj.default;\n\tif (def !== null && typeof def === \"object\") {\n\t\tconst defObj = def as Record<string, unknown>;\n\t\tif (typeof defObj.createJiti === \"function\") {\n\t\t\treturn defObj.createJiti as (\n\t\t\t\tid: string,\n\t\t\t\toptions?: unknown,\n\t\t\t) => JitiInstance;\n\t\t}\n\t}\n\treturn null;\n}\n\ninterface JitiInstance {\n\timport(id: string): Promise<unknown>;\n}\n\nfunction extractDefaultExport(mod: unknown): unknown {\n\tif (mod === null || typeof mod !== \"object\") return mod;\n\tconst obj = mod as Record<string, unknown>;\n\tif (\"default\" in obj && obj.default !== undefined) return obj.default;\n\t// No `default` export. If the module itself is a function, treat it as the config —\n\t// that lets tests and advanced users skip the wrapper.\n\t// Otherwise, return `undefined` so the caller surfaces a clear ConfigLoadError.\n\tif (typeof mod === \"function\") return mod;\n\treturn undefined;\n}\n\nfunction safeIsFile(path: string): boolean {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import { DeployFunctionInput, NeonApi } from "./neon-api.js";
2
+
3
+ //#region src/lib/neon-api-real.d.ts
4
+ interface CreateNeonAuthRestInput {
5
+ auth_provider: "better_auth";
6
+ database_name?: string;
7
+ }
8
+ /**
9
+ * Adapt `@neon/sdk` (raw layer) to the narrow {@link NeonApi} façade used by the rest of
10
+ * this package. Constructs are restricted to whole-object read/write of just the fields we
11
+ * model in {@link Config}; anything else stays untouched on the remote.
12
+ */
13
+ declare function createRealNeonApi(options: {
14
+ apiKey: string;
15
+ baseUrl?: string;
16
+ /**
17
+ * Tuning knob for the built-in 423 retry. Defaults: ~30s of total wait spread across
18
+ * 12 attempts with exponential backoff capped at 5s. Lowering this is mostly useful in
19
+ * tests; raising it is rarely needed because Neon operations are usually sub-second.
20
+ */
21
+ retryOnLocked?: {
22
+ maxAttempts?: number;
23
+ initialDelayMs?: number;
24
+ maxDelayMs?: number;
25
+ };
26
+ }): NeonApi;
27
+ interface RetryConfig {
28
+ maxAttempts: number;
29
+ initialDelayMs: number;
30
+ maxDelayMs: number;
31
+ }
32
+ /**
33
+ * Retry a function whenever it throws an HTTP 423 (Locked) — Neon's signal that a prior
34
+ * mutation on the same resource is still in flight. Uses exponential backoff capped at
35
+ * `maxDelayMs`. Any other error (and the last attempt) propagates.
36
+ *
37
+ * Exported only for tests; production callers go through the wrapped {@link NeonApi}.
38
+ */
39
+ declare function retryOnLocked<T>(fn: () => Promise<T>, config: RetryConfig): Promise<T>;
40
+ /**
41
+ * Whether an error from a Preview-feature read means the feature simply isn't available
42
+ * for this project/branch/region (as opposed to a real, transient failure). Neon signals
43
+ * this a few ways: a 404 "this route does not exist" (the route isn't deployed at all), or
44
+ * a 503/4xx whose message says the platform feature is "not available" / "not enabled".
45
+ *
46
+ * Callers do **not** swallow this into an empty result — touching a Preview feature that
47
+ * isn't available is surfaced as a {@link previewUnavailableError} so `plan` / `status` /
48
+ * `pull` (and `neon dev`) fail clearly instead of, say, planning to create resources the
49
+ * API will refuse to create.
50
+ */
51
+ declare function isPreviewFeatureUnavailable(err: unknown): boolean;
52
+ /**
53
+ * Convert a Preview-feature error into a clear {@link PlatformError} when the feature is
54
+ * unavailable for the project; otherwise pass the original error through unchanged so a
55
+ * genuine failure (auth, transient 5xx, …) keeps its specific code and message.
56
+ *
57
+ * The message names the failing feature, summarizes the response in one short
58
+ * `HTTP <status> <reason>` line, includes the raw Neon API message + request id (valuable
59
+ * signal while the feature is in preview), gives status-specific guidance (see
60
+ * {@link previewUnavailableHint}), and offers removing the feature from the policy as an
61
+ * escape hatch. `status`/`requestId` are also kept on `details` for programmatic consumers.
62
+ */
63
+ declare function previewUnavailableError(err: unknown, featureLabel: string): unknown;
64
+ declare function createNeonAuthRestInput(input: {
65
+ databaseName?: string;
66
+ }): CreateNeonAuthRestInput;
67
+ /**
68
+ * Build the `multipart/form-data` body for a function deployment, matching the public
69
+ * `FunctionDeployRequest` schema (`POST .../functions/{slug}/deployments`):
70
+ *
71
+ * - `zip` — the bundle as a binary part (named `bundle.zip`).
72
+ * - `runtime` — the function runtime.
73
+ * - `environment` — a single JSON-encoded string→string map (multipart can't carry a typed
74
+ * object part), omitted entirely when there are no env vars.
75
+ *
76
+ * Pure (no I/O) so it can be unit-tested against the spec without stubbing `fetch`.
77
+ */
78
+ declare function buildFunctionDeployForm(input: DeployFunctionInput): FormData;
79
+ /**
80
+ * Read a response body as JSON, tolerating non-JSON. Some Neon routes return a plain-text
81
+ * body (e.g. a 404 `"this route does not exist"` for a Preview feature not enabled in the
82
+ * project/region). Parsing that with `JSON.parse` used to throw a cryptic
83
+ * `SyntaxError: Unexpected token …`, which — because parsing happens before the `res.ok`
84
+ * check in {@link request} — masked the real HTTP status. We instead return the raw text
85
+ * wrapped as `{ message }` so the status-based error path in `request` / `wrapNeonError`
86
+ * runs and produces a proper {@link PlatformError} (e.g. `NotFound`), and a non-error body
87
+ * that simply isn't JSON degrades to text rather than crashing.
88
+ */
89
+ declare function readJsonBody(res: Response): Promise<unknown>;
90
+ //#endregion
91
+ export { buildFunctionDeployForm, createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
92
+ //# sourceMappingURL=neon-api-real.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"neon-api-real.d.ts","names":[],"sources":["../../src/lib/neon-api-real.ts"],"mappings":";;;UAiRU,uBAAA;;EAAA,aAAA,CAAA,EAAA,MAAA;AAeV;AA4CC;AAeD;;;;AAES,iBA7DO,iBAAA,CA6DP,OAAA,EAAA;QACE,EAAA,MAAA;SAAR,CAAA,EAAA,MAAA;EAAO;AAk9BV;AAiFA;AA6DA;AAoBA;EAAuC,aAAA,CAAA,EAAA;IAAQ,WAAA,CAAA,EAAA,MAAA;IAAsB,cAAA,CAAA,EAAA,MAAA;IAAQ,UAAA,CAAA,EAAA,MAAA;EAwBvD,CAAA;CAAY,CAAA,EA7rC9B,OA6rC8B;UA5pCxB,WAAA,CA4pC8B;aAAW,EAAA,MAAA;EAAO,cAAA,EAAA,MAAA;;;;;;;;;;iBA/oCpC,2BACX,QAAQ,YACV,cACN,QAAQ;;;;;;;;;;;;iBAk9BK,2BAAA;;;;;;;;;;;;iBAiFA,uBAAA;iBA6DA,uBAAA;;IAEZ;;;;;;;;;;;;iBAkBY,uBAAA,QAA+B,sBAAsB;;;;;;;;;;;iBAwB/C,YAAA,MAAkB,WAAW"}