@neon/config 0.0.0 → 0.9.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.
Files changed (60) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +148 -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,957 @@
1
+ import { ErrorCode, PlatformError } from "./errors.js";
2
+ import { formatSuspendTimeout, parseSuspendTimeout } from "./duration.js";
3
+ import { wrapNeonError } from "./wrap-neon-error.js";
4
+ import { z } from "zod";
5
+ import { createNeonClient } from "@neon/sdk";
6
+ import { createProject, createProjectBranch, createProjectBranchDataApi, getConnectionUri, getNeonAuth, getProject, getProjectBranchDataApi, listProjectBranchDatabases, listProjectBranchRoles, listProjectBranches, listProjectEndpoints, listProjects, updateProject, updateProjectBranch, updateProjectBranchDataApi, updateProjectEndpoint } from "@neon/sdk/raw";
7
+ //#region src/lib/neon-api-real.ts
8
+ const DEFAULT_NEON_API_BASE_URL = "https://console.neon.tech/api/v2";
9
+ /**
10
+ * Unwrap a `@neon/sdk` raw `{ data, error, response }` result into the bare body, throwing
11
+ * on a non-2xx response. The thrown shape (`{ response: { status, data } }`) deliberately
12
+ * matches what the package's REST fallbacks already throw, so {@link wrapNeonError} and the
13
+ * 423 {@link retryOnLocked} retry keep working unchanged across both transports.
14
+ */
15
+ function unwrap(result) {
16
+ const response = result.response;
17
+ if (!response || !response.ok) throw { response: {
18
+ status: response?.status,
19
+ data: result.error
20
+ } };
21
+ return result.data;
22
+ }
23
+ const neonAuthResponseSchema = z.object({
24
+ auth_provider_project_id: z.string(),
25
+ pub_client_key: z.string().optional(),
26
+ secret_server_key: z.string().optional(),
27
+ jwks_url: z.string(),
28
+ base_url: z.string().optional()
29
+ });
30
+ /** Map our camelCase {@link DataApiSettings} onto the Neon API's snake_case `DataAPISettings`. */
31
+ function dataApiSettingsToApi(settings) {
32
+ const out = {};
33
+ if (settings.dbAggregatesEnabled !== void 0) out.db_aggregates_enabled = settings.dbAggregatesEnabled;
34
+ if (settings.dbAnonRole !== void 0) out.db_anon_role = settings.dbAnonRole;
35
+ if (settings.dbExtraSearchPath !== void 0) out.db_extra_search_path = settings.dbExtraSearchPath;
36
+ if (settings.dbMaxRows !== void 0) out.db_max_rows = settings.dbMaxRows;
37
+ if (settings.dbSchemas !== void 0) out.db_schemas = settings.dbSchemas;
38
+ if (settings.jwtRoleClaimKey !== void 0) out.jwt_role_claim_key = settings.jwtRoleClaimKey;
39
+ if (settings.jwtCacheMaxLifetime !== void 0) out.jwt_cache_max_lifetime = settings.jwtCacheMaxLifetime;
40
+ if (settings.openapiMode !== void 0) out.openapi_mode = settings.openapiMode;
41
+ if (settings.serverCorsAllowedOrigins !== void 0) out.server_cors_allowed_origins = settings.serverCorsAllowedOrigins;
42
+ if (settings.serverTimingEnabled !== void 0) out.server_timing_enabled = settings.serverTimingEnabled;
43
+ return out;
44
+ }
45
+ /** Narrow the API's free-form `openapi_mode` string to our literal union (else drop it). */
46
+ function normalizeOpenapiMode(value) {
47
+ return value === "ignore-privileges" || value === "disabled" ? value : void 0;
48
+ }
49
+ /** Map the Neon API's snake_case `DataAPISettings` back to our camelCase {@link DataApiSettings}. */
50
+ function dataApiSettingsFromApi(settings) {
51
+ if (!settings) return void 0;
52
+ const out = {};
53
+ if (settings.db_aggregates_enabled !== void 0) out.dbAggregatesEnabled = settings.db_aggregates_enabled;
54
+ if (settings.db_anon_role !== void 0) out.dbAnonRole = settings.db_anon_role;
55
+ if (settings.db_extra_search_path !== void 0) out.dbExtraSearchPath = settings.db_extra_search_path;
56
+ if (settings.db_max_rows !== void 0) out.dbMaxRows = settings.db_max_rows;
57
+ if (settings.db_schemas !== void 0) out.dbSchemas = settings.db_schemas;
58
+ if (settings.jwt_role_claim_key !== void 0) out.jwtRoleClaimKey = settings.jwt_role_claim_key;
59
+ if (settings.jwt_cache_max_lifetime !== void 0) out.jwtCacheMaxLifetime = settings.jwt_cache_max_lifetime;
60
+ if (settings.openapi_mode !== void 0) {
61
+ const mode = normalizeOpenapiMode(settings.openapi_mode);
62
+ if (mode !== void 0) out.openapiMode = mode;
63
+ }
64
+ if (settings.server_cors_allowed_origins !== void 0) out.serverCorsAllowedOrigins = settings.server_cors_allowed_origins;
65
+ if (settings.server_timing_enabled !== void 0) out.serverTimingEnabled = settings.server_timing_enabled;
66
+ return Object.keys(out).length > 0 ? out : void 0;
67
+ }
68
+ /** Build the Neon API `DataAPICreateRequest` from our {@link EnableDataApiInput}. */
69
+ function dataApiCreateRequest(input) {
70
+ const req = {};
71
+ if (!input) return req;
72
+ if (input.authProvider !== void 0) req.auth_provider = input.authProvider === "neon" ? "neon_auth" : "external";
73
+ if (input.jwksUrl !== void 0) req.jwks_url = input.jwksUrl;
74
+ if (input.providerName !== void 0) req.provider_name = input.providerName;
75
+ if (input.jwtAudience !== void 0) req.jwt_audience = input.jwtAudience;
76
+ if (input.settings) {
77
+ const settings = dataApiSettingsToApi(input.settings);
78
+ if (Object.keys(settings).length > 0) req.settings = settings;
79
+ }
80
+ return req;
81
+ }
82
+ /** Map a `DataAPIReponse` (GET) onto our {@link NeonDataApiSnapshot}. */
83
+ function dataApiSnapshotFromResponse(data) {
84
+ const snapshot = { url: data.url };
85
+ if (data.status !== void 0) snapshot.status = data.status;
86
+ const settings = dataApiSettingsFromApi(data.settings);
87
+ if (settings) snapshot.settings = settings;
88
+ return snapshot;
89
+ }
90
+ const bucketSchema = z.object({
91
+ name: z.string(),
92
+ access_level: z.string().optional()
93
+ });
94
+ const bucketResponseSchema = z.object({ bucket: bucketSchema });
95
+ const bucketsListResponseSchema = z.object({ buckets: z.array(bucketSchema) });
96
+ const branchStorageSchema = z.object({
97
+ enabled: z.boolean().optional(),
98
+ s3_endpoint: z.string(),
99
+ region: z.string(),
100
+ force_path_style: z.boolean()
101
+ });
102
+ const functionDeploymentSchema = z.object({
103
+ id: z.number(),
104
+ status: z.string()
105
+ });
106
+ const neonFunctionSchema = z.object({
107
+ id: z.string(),
108
+ slug: z.string(),
109
+ name: z.string(),
110
+ invocation_url: z.string(),
111
+ active_deployment: functionDeploymentSchema.optional()
112
+ });
113
+ const functionsListResponseSchema = z.object({ functions: z.array(neonFunctionSchema) });
114
+ const functionDeploymentResponseSchema = z.object({ deployment: functionDeploymentSchema });
115
+ const credentialScopeSchema = z.enum([
116
+ "storage:read",
117
+ "storage:write",
118
+ "ai_gateway:invoke",
119
+ "functions:invoke"
120
+ ]);
121
+ const createCredentialResponseSchema = z.object({
122
+ token_id: z.string(),
123
+ token_id_short: z.string(),
124
+ name: z.string().optional(),
125
+ api_token: z.string(),
126
+ s3_secret_access_key: z.string(),
127
+ scopes: z.array(credentialScopeSchema),
128
+ branch_id: z.string(),
129
+ created_at: z.string(),
130
+ expires_at: z.string().optional()
131
+ });
132
+ const credentialMetaSchema = z.object({
133
+ token_id: z.string(),
134
+ token_id_short: z.string(),
135
+ name: z.string().optional(),
136
+ scopes: z.array(credentialScopeSchema),
137
+ principal_type: z.enum(["user", "function"]),
138
+ function_id: z.string().optional(),
139
+ branch_id: z.string().optional(),
140
+ created_at: z.string(),
141
+ last_used_at: z.string().optional(),
142
+ revoked_at: z.string().optional(),
143
+ expires_at: z.string().optional()
144
+ });
145
+ const listCredentialsResponseSchema = z.object({ credentials: z.array(credentialMetaSchema) });
146
+ /**
147
+ * Adapt `@neon/sdk` (raw layer) to the narrow {@link NeonApi} façade used by the rest of
148
+ * this package. Constructs are restricted to whole-object read/write of just the fields we
149
+ * model in {@link Config}; anything else stays untouched on the remote.
150
+ */
151
+ function createRealNeonApi(options) {
152
+ if (!options.apiKey || options.apiKey.trim() === "") throw new PlatformError(ErrorCode.MissingApiKey, ["createRealNeonApi requires a non-empty `apiKey`.", "Generate one at https://console.neon.tech/app/settings/api-keys and pass it as { apiKey: process.env.NEON_API_KEY }."].join(" "));
153
+ const client = createNeonClient({
154
+ apiKey: options.apiKey,
155
+ retries: 0,
156
+ ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
157
+ }).client;
158
+ return new RealNeonApi(client, {
159
+ maxAttempts: options.retryOnLocked?.maxAttempts ?? 12,
160
+ initialDelayMs: options.retryOnLocked?.initialDelayMs ?? 250,
161
+ maxDelayMs: options.retryOnLocked?.maxDelayMs ?? 5e3
162
+ }, {
163
+ apiKey: options.apiKey,
164
+ baseUrl: options.baseUrl ?? DEFAULT_NEON_API_BASE_URL
165
+ });
166
+ }
167
+ /**
168
+ * Retry a function whenever it throws an HTTP 423 (Locked) — Neon's signal that a prior
169
+ * mutation on the same resource is still in flight. Uses exponential backoff capped at
170
+ * `maxDelayMs`. Any other error (and the last attempt) propagates.
171
+ *
172
+ * Exported only for tests; production callers go through the wrapped {@link NeonApi}.
173
+ */
174
+ async function retryOnLocked(fn, config) {
175
+ let delay = config.initialDelayMs;
176
+ let lastError;
177
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt++) try {
178
+ return await fn();
179
+ } catch (err) {
180
+ lastError = err;
181
+ if (readHttpStatusFromError(err) !== 423 || attempt === config.maxAttempts) throw err;
182
+ await sleep(delay);
183
+ delay = Math.min(delay * 2, config.maxDelayMs);
184
+ }
185
+ throw lastError;
186
+ }
187
+ function readHttpStatusFromError(err) {
188
+ if (err === null || typeof err !== "object") return void 0;
189
+ const response = err.response;
190
+ if (response === null || typeof response !== "object") return void 0;
191
+ const status = response.status;
192
+ return typeof status === "number" ? status : void 0;
193
+ }
194
+ function sleep(ms) {
195
+ return new Promise((resolve) => setTimeout(resolve, ms));
196
+ }
197
+ var RealNeonApi = class {
198
+ client;
199
+ retryConfig;
200
+ restConfig;
201
+ constructor(client, retryConfig, restConfig) {
202
+ this.client = client;
203
+ this.retryConfig = retryConfig;
204
+ this.restConfig = restConfig;
205
+ }
206
+ retry(fn) {
207
+ return retryOnLocked(fn, this.retryConfig);
208
+ }
209
+ async call(op, fn, options = {}) {
210
+ try {
211
+ return options.mutating ? await this.retry(fn) : await fn();
212
+ } catch (err) {
213
+ throw wrapNeonError(err, options.projectId ? {
214
+ op,
215
+ projectId: options.projectId
216
+ } : { op });
217
+ }
218
+ }
219
+ async listProjects(filter) {
220
+ return this.call(filter.orgId ? `listProjects(org=${filter.orgId})` : "listProjects", async () => {
221
+ const projects = [];
222
+ let cursor;
223
+ while (true) {
224
+ const body = unwrap(await listProjects({
225
+ client: this.client,
226
+ query: {
227
+ ...filter.orgId ? { org_id: filter.orgId } : {},
228
+ ...cursor ? { cursor } : {},
229
+ limit: 100
230
+ }
231
+ }));
232
+ projects.push(...body.projects);
233
+ const next = body.pagination?.next;
234
+ if (!next || next === cursor) break;
235
+ cursor = next;
236
+ }
237
+ return projects.map(projectToSnapshot);
238
+ });
239
+ }
240
+ async getProject(projectId) {
241
+ return this.call(`getProject(${projectId})`, async () => {
242
+ return projectToSnapshot(unwrap(await getProject({
243
+ client: this.client,
244
+ path: { project_id: projectId }
245
+ })).project);
246
+ }, { projectId });
247
+ }
248
+ async createProject(input) {
249
+ const body = { project: {
250
+ name: input.name,
251
+ region_id: input.regionId,
252
+ ...input.pgVersion !== void 0 ? { pg_version: input.pgVersion } : {},
253
+ ...input.orgId ? { org_id: input.orgId } : {},
254
+ ...input.defaultEndpointSettings ? { default_endpoint_settings: computeSettingsToDefaults(input.defaultEndpointSettings) } : {},
255
+ ...input.defaultBranchName ? { branch: { name: input.defaultBranchName } } : {}
256
+ } };
257
+ return this.call(`createProject(${input.name})`, async () => {
258
+ return projectToSnapshot(unwrap(await createProject({
259
+ client: this.client,
260
+ body
261
+ })).project);
262
+ }, { mutating: true });
263
+ }
264
+ async updateProject(projectId, input) {
265
+ const body = { project: {
266
+ ...input.name !== void 0 ? { name: input.name } : {},
267
+ ...input.defaultEndpointSettings ? { default_endpoint_settings: computeSettingsToDefaults(input.defaultEndpointSettings) } : {}
268
+ } };
269
+ return this.call(`updateProject(${projectId})`, async () => {
270
+ return projectToSnapshot(unwrap(await updateProject({
271
+ client: this.client,
272
+ path: { project_id: projectId },
273
+ body
274
+ })).project);
275
+ }, {
276
+ projectId,
277
+ mutating: true
278
+ });
279
+ }
280
+ async listBranches(projectId) {
281
+ return this.call(`listBranches(${projectId})`, async () => {
282
+ const branches = [];
283
+ let cursor;
284
+ while (true) {
285
+ const body = unwrap(await listProjectBranches({
286
+ client: this.client,
287
+ path: { project_id: projectId },
288
+ query: {
289
+ limit: 100,
290
+ ...cursor ? { cursor } : {}
291
+ }
292
+ }));
293
+ branches.push(...body.branches);
294
+ const next = body.pagination?.next;
295
+ if (!next || next === cursor) break;
296
+ cursor = next;
297
+ }
298
+ return branches.map(branchToSnapshot);
299
+ }, { projectId });
300
+ }
301
+ async createBranch(projectId, input) {
302
+ const endpointOptions = input.computeSettings ? {
303
+ type: "read_write",
304
+ ...computeSettingsToEndpointOptions(input.computeSettings)
305
+ } : { type: "read_write" };
306
+ const body = {
307
+ branch: {
308
+ name: input.name,
309
+ ...input.parentId ? { parent_id: input.parentId } : {},
310
+ ...input.expiresAt ? { expires_at: input.expiresAt } : {},
311
+ ...input.protected !== void 0 ? { protected: input.protected } : {}
312
+ },
313
+ endpoints: [endpointOptions]
314
+ };
315
+ return this.call(`createBranch(${projectId}/${input.name})`, async () => {
316
+ const data = unwrap(await createProjectBranch({
317
+ client: this.client,
318
+ path: { project_id: projectId },
319
+ body
320
+ }));
321
+ return {
322
+ branch: branchToSnapshot(data.branch),
323
+ endpoints: (data.endpoints ?? []).map(endpointToSnapshot)
324
+ };
325
+ }, {
326
+ projectId,
327
+ mutating: true
328
+ });
329
+ }
330
+ async updateBranch(projectId, branchId, input) {
331
+ const branch = {};
332
+ if (input.name !== void 0) branch.name = input.name;
333
+ if (input.expiresAt !== void 0) branch.expires_at = input.expiresAt;
334
+ if (input.protected !== void 0) branch.protected = input.protected;
335
+ return this.call(`updateBranch(${projectId}/${branchId})`, async () => {
336
+ return branchToSnapshot(unwrap(await updateProjectBranch({
337
+ client: this.client,
338
+ path: {
339
+ project_id: projectId,
340
+ branch_id: branchId
341
+ },
342
+ body: { branch }
343
+ })).branch);
344
+ }, {
345
+ projectId,
346
+ mutating: true
347
+ });
348
+ }
349
+ async listEndpoints(projectId) {
350
+ return this.call(`listEndpoints(${projectId})`, async () => {
351
+ return unwrap(await listProjectEndpoints({
352
+ client: this.client,
353
+ path: { project_id: projectId }
354
+ })).endpoints.map(endpointToSnapshot);
355
+ }, { projectId });
356
+ }
357
+ async updateEndpoint(projectId, endpointId, settings) {
358
+ const endpoint = computeSettingsToEndpointOptions(settings);
359
+ return this.call(`updateEndpoint(${projectId}/${endpointId})`, async () => {
360
+ return endpointToSnapshot(unwrap(await updateProjectEndpoint({
361
+ client: this.client,
362
+ path: {
363
+ project_id: projectId,
364
+ endpoint_id: endpointId
365
+ },
366
+ body: { endpoint }
367
+ })).endpoint);
368
+ }, {
369
+ projectId,
370
+ mutating: true
371
+ });
372
+ }
373
+ async listBranchRoles(projectId, branchId) {
374
+ return this.call(`listBranchRoles(${projectId}/${branchId})`, async () => {
375
+ return unwrap(await listProjectBranchRoles({
376
+ client: this.client,
377
+ path: {
378
+ project_id: projectId,
379
+ branch_id: branchId
380
+ }
381
+ })).roles.map(roleToSnapshot);
382
+ }, { projectId });
383
+ }
384
+ async listBranchDatabases(projectId, branchId) {
385
+ return this.call(`listBranchDatabases(${projectId}/${branchId})`, async () => {
386
+ return unwrap(await listProjectBranchDatabases({
387
+ client: this.client,
388
+ path: {
389
+ project_id: projectId,
390
+ branch_id: branchId
391
+ }
392
+ })).databases.map(databaseToSnapshot);
393
+ }, { projectId });
394
+ }
395
+ async getConnectionUri(projectId, input) {
396
+ const op = `getConnectionUri(${projectId}/${input.databaseName}@${input.roleName}${input.pooled ? " pooled" : ""})`;
397
+ const pooled = input.pooled === true;
398
+ return this.call(op, async () => {
399
+ return { uri: unwrap(await getConnectionUri({
400
+ client: this.client,
401
+ path: { project_id: projectId },
402
+ query: {
403
+ database_name: input.databaseName,
404
+ role_name: input.roleName,
405
+ ...input.branchId ? { branch_id: input.branchId } : {},
406
+ ...input.endpointId ? { endpoint_id: input.endpointId } : {},
407
+ pooled
408
+ }
409
+ })).uri };
410
+ }, { projectId });
411
+ }
412
+ async getNeonAuth(projectId, branchId) {
413
+ try {
414
+ return await this.call(`getNeonAuth(${projectId}/${branchId})`, async () => {
415
+ const data = unwrap(await getNeonAuth({
416
+ client: this.client,
417
+ path: {
418
+ project_id: projectId,
419
+ branch_id: branchId
420
+ }
421
+ }));
422
+ return neonAuthResponseToSnapshot(neonAuthResponseSchema.parse(data));
423
+ }, { projectId });
424
+ } catch (err) {
425
+ if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
426
+ throw err;
427
+ }
428
+ }
429
+ async enableNeonAuth(projectId, branchId, input = {}) {
430
+ try {
431
+ return await this.call(`enableNeonAuth(${projectId}/${branchId})`, async () => {
432
+ const data = await this.postJson(`/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/auth`, createNeonAuthRestInput(input));
433
+ return neonAuthResponseToSnapshot(neonAuthResponseSchema.parse(data));
434
+ }, {
435
+ projectId,
436
+ mutating: true
437
+ });
438
+ } catch (err) {
439
+ if (err instanceof PlatformError && err.code === ErrorCode.Conflict) {
440
+ const existing = await this.getNeonAuth(projectId, branchId);
441
+ if (existing) return existing;
442
+ }
443
+ throw err;
444
+ }
445
+ }
446
+ async postJson(path, body) {
447
+ return this.request("POST", path, {
448
+ headers: { "Content-Type": "application/json" },
449
+ body: JSON.stringify(body)
450
+ });
451
+ }
452
+ async getJson(path) {
453
+ return this.request("GET", path);
454
+ }
455
+ async deleteJson(path) {
456
+ return this.request("DELETE", path);
457
+ }
458
+ /**
459
+ * Upload a built function bundle via `multipart/form-data` to the deploy endpoint
460
+ * (`POST .../functions/{slug}/deployments`). Body shape lives in the pure
461
+ * {@link buildFunctionDeployForm} helper so it can be unit-tested against the spec.
462
+ */
463
+ async postMultipart(path, input) {
464
+ return this.request("POST", path, { body: buildFunctionDeployForm(input) });
465
+ }
466
+ async request(method, path, init = {}) {
467
+ const url = `${this.restConfig.baseUrl.replace(/\/+$/, "")}${path}`;
468
+ const res = await fetch(url, {
469
+ method,
470
+ headers: {
471
+ Authorization: `Bearer ${this.restConfig.apiKey}`,
472
+ ...init.headers ?? {}
473
+ },
474
+ ...init.body !== void 0 ? { body: init.body } : {}
475
+ });
476
+ const data = await readJsonBody(res);
477
+ if (!res.ok) throw { response: {
478
+ status: res.status,
479
+ data
480
+ } };
481
+ return data;
482
+ }
483
+ async getNeonDataApi(projectId, branchId, databaseName) {
484
+ try {
485
+ return await this.call(`getNeonDataApi(${projectId}/${branchId}/${databaseName})`, async () => dataApiSnapshotFromResponse(unwrap(await getProjectBranchDataApi({
486
+ client: this.client,
487
+ path: {
488
+ project_id: projectId,
489
+ branch_id: branchId,
490
+ database_name: databaseName
491
+ }
492
+ }))), { projectId });
493
+ } catch (err) {
494
+ if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
495
+ throw err;
496
+ }
497
+ }
498
+ async enableProjectBranchDataApi(projectId, branchId, databaseName, input) {
499
+ try {
500
+ return await this.call(`enableProjectBranchDataApi(${projectId}/${branchId}/${databaseName})`, async () => {
501
+ return { url: unwrap(await createProjectBranchDataApi({
502
+ client: this.client,
503
+ path: {
504
+ project_id: projectId,
505
+ branch_id: branchId,
506
+ database_name: databaseName
507
+ },
508
+ body: dataApiCreateRequest(input)
509
+ })).url };
510
+ }, {
511
+ projectId,
512
+ mutating: true
513
+ });
514
+ } catch (err) {
515
+ if (err instanceof PlatformError && err.code === ErrorCode.Conflict) {
516
+ const existing = await this.getNeonDataApi(projectId, branchId, databaseName);
517
+ if (existing) return existing;
518
+ }
519
+ throw err;
520
+ }
521
+ }
522
+ async updateProjectBranchDataApi(projectId, branchId, databaseName, settings) {
523
+ return await this.call(`updateProjectBranchDataApi(${projectId}/${branchId}/${databaseName})`, async () => {
524
+ unwrap(await updateProjectBranchDataApi({
525
+ client: this.client,
526
+ path: {
527
+ project_id: projectId,
528
+ branch_id: branchId,
529
+ database_name: databaseName
530
+ },
531
+ body: { settings: dataApiSettingsToApi(settings) }
532
+ }));
533
+ return dataApiSnapshotFromResponse(unwrap(await getProjectBranchDataApi({
534
+ client: this.client,
535
+ path: {
536
+ project_id: projectId,
537
+ branch_id: branchId,
538
+ database_name: databaseName
539
+ }
540
+ })));
541
+ }, {
542
+ projectId,
543
+ mutating: true
544
+ });
545
+ }
546
+ async listBranchBuckets(projectId, branchId) {
547
+ try {
548
+ return await this.call(`listBranchBuckets(${projectId}/${branchId})`, async () => {
549
+ const data = await this.getJson(branchPreviewPath(projectId, branchId, "buckets"));
550
+ return bucketsListResponseSchema.parse(data).buckets.map(bucketToSnapshot);
551
+ }, { projectId });
552
+ } catch (err) {
553
+ throw previewUnavailableError(err, "Object storage (buckets)");
554
+ }
555
+ }
556
+ async createBranchBucket(projectId, branchId, input) {
557
+ return this.call(`createBranchBucket(${projectId}/${branchId}/${input.name})`, async () => {
558
+ const data = await this.postJson(branchPreviewPath(projectId, branchId, "buckets"), {
559
+ name: input.name,
560
+ ...input.accessLevel ? { access_level: input.accessLevel } : {}
561
+ });
562
+ return bucketToSnapshot(bucketResponseSchema.parse(data).bucket);
563
+ }, {
564
+ projectId,
565
+ mutating: true
566
+ });
567
+ }
568
+ async deleteBranchBucket(projectId, branchId, bucketName) {
569
+ await this.call(`deleteBranchBucket(${projectId}/${branchId}/${bucketName})`, async () => {
570
+ await this.deleteJson(`${branchPreviewPath(projectId, branchId, "buckets")}/${encodeURIComponent(bucketName)}`);
571
+ }, {
572
+ projectId,
573
+ mutating: true
574
+ });
575
+ }
576
+ async getProjectBranchStorage(projectId, branchId) {
577
+ try {
578
+ return await this.call(`getProjectBranchStorage(${projectId}/${branchId})`, async () => {
579
+ const data = await this.getJson(`/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/storage`);
580
+ const parsed = branchStorageSchema.parse(data);
581
+ return {
582
+ s3Endpoint: parsed.s3_endpoint,
583
+ region: parsed.region,
584
+ forcePathStyle: parsed.force_path_style
585
+ };
586
+ }, { projectId });
587
+ } catch (err) {
588
+ if (err instanceof PlatformError && err.code === ErrorCode.NotFound) return null;
589
+ throw previewUnavailableError(err, "Object storage");
590
+ }
591
+ }
592
+ async listBranchFunctions(projectId, branchId) {
593
+ try {
594
+ return await this.call(`listBranchFunctions(${projectId}/${branchId})`, async () => {
595
+ const data = await this.getJson(branchPreviewPath(projectId, branchId, "functions"));
596
+ return functionsListResponseSchema.parse(data).functions.map(functionToSnapshot);
597
+ }, { projectId });
598
+ } catch (err) {
599
+ throw previewUnavailableError(err, "Functions");
600
+ }
601
+ }
602
+ async deleteBranchFunction(projectId, branchId, slug) {
603
+ await this.call(`deleteBranchFunction(${projectId}/${branchId}/${slug})`, async () => {
604
+ await this.deleteJson(`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}`);
605
+ }, {
606
+ projectId,
607
+ mutating: true
608
+ });
609
+ }
610
+ async deployBranchFunction(projectId, branchId, slug, input) {
611
+ return this.call(`deployBranchFunction(${projectId}/${branchId}/${slug})`, async () => {
612
+ const data = await this.postMultipart(`${branchPreviewPath(projectId, branchId, "functions")}/${encodeURIComponent(slug)}/deployments`, input);
613
+ return deploymentToSnapshot(functionDeploymentResponseSchema.parse(data).deployment);
614
+ }, {
615
+ projectId,
616
+ mutating: true
617
+ });
618
+ }
619
+ async createCredential(projectId, branchId, input) {
620
+ try {
621
+ return await this.call(`createCredential(${projectId}/${branchId})`, async () => {
622
+ const data = await this.postJson(credentialsPath(projectId, branchId), {
623
+ scopes: input.scopes,
624
+ principal_type: input.principalType,
625
+ ...input.functionId ? { function_id: input.functionId } : {},
626
+ ...input.name ? { name: input.name } : {}
627
+ });
628
+ return createCredentialToSnapshot(createCredentialResponseSchema.parse(data));
629
+ }, {
630
+ projectId,
631
+ mutating: true
632
+ });
633
+ } catch (err) {
634
+ throw previewUnavailableError(err, "Branch credentials");
635
+ }
636
+ }
637
+ async listCredentials(projectId, branchId) {
638
+ try {
639
+ return await this.call(`listCredentials(${projectId}/${branchId})`, async () => {
640
+ const data = await this.getJson(credentialsPath(projectId, branchId));
641
+ return listCredentialsResponseSchema.parse(data).credentials.map(credentialMetaToSnapshot);
642
+ }, { projectId });
643
+ } catch (err) {
644
+ throw previewUnavailableError(err, "Branch credentials");
645
+ }
646
+ }
647
+ async revokeCredential(projectId, branchId, tokenId) {
648
+ await this.call(`revokeCredential(${projectId}/${branchId}/${tokenId})`, async () => {
649
+ await this.deleteJson(`${credentialsPath(projectId, branchId)}/${encodeURIComponent(tokenId)}`);
650
+ }, {
651
+ projectId,
652
+ mutating: true
653
+ });
654
+ }
655
+ };
656
+ function branchPreviewPath(projectId, branchId, resource) {
657
+ return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/${resource}`;
658
+ }
659
+ function credentialsPath(projectId, branchId) {
660
+ return `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/credentials`;
661
+ }
662
+ function createCredentialToSnapshot(data) {
663
+ const snapshot = {
664
+ tokenId: data.token_id,
665
+ tokenIdShort: data.token_id_short,
666
+ apiToken: data.api_token,
667
+ s3SecretAccessKey: data.s3_secret_access_key,
668
+ scopes: data.scopes,
669
+ branchId: data.branch_id,
670
+ createdAt: data.created_at
671
+ };
672
+ if (data.name !== void 0) snapshot.name = data.name;
673
+ if (data.expires_at !== void 0) snapshot.expiresAt = data.expires_at;
674
+ return snapshot;
675
+ }
676
+ function credentialMetaToSnapshot(data) {
677
+ const snapshot = {
678
+ tokenId: data.token_id,
679
+ tokenIdShort: data.token_id_short,
680
+ scopes: data.scopes,
681
+ principalType: data.principal_type,
682
+ createdAt: data.created_at
683
+ };
684
+ if (data.name !== void 0) snapshot.name = data.name;
685
+ if (data.function_id !== void 0) snapshot.functionId = data.function_id;
686
+ if (data.branch_id !== void 0) snapshot.branchId = data.branch_id;
687
+ if (data.last_used_at !== void 0) snapshot.lastUsedAt = data.last_used_at;
688
+ if (data.revoked_at !== void 0) snapshot.revokedAt = data.revoked_at;
689
+ if (data.expires_at !== void 0) snapshot.expiresAt = data.expires_at;
690
+ return snapshot;
691
+ }
692
+ function bucketToSnapshot(bucket) {
693
+ return {
694
+ name: bucket.name,
695
+ accessLevel: normalizeBucketAccessLevel(bucket.access_level)
696
+ };
697
+ }
698
+ /**
699
+ * The Neon API returns `access_level` as a free-form string (per the API guidelines:
700
+ * responses use plain strings, not enums). Map the known values onto our union and treat
701
+ * anything else as `private` — the safe default for an unrecognised access level.
702
+ */
703
+ function normalizeBucketAccessLevel(value) {
704
+ return value === "public_read" ? "public_read" : "private";
705
+ }
706
+ function functionToSnapshot(fn) {
707
+ const snapshot = {
708
+ id: fn.id,
709
+ slug: fn.slug,
710
+ name: fn.name,
711
+ invocationUrl: fn.invocation_url
712
+ };
713
+ if (fn.active_deployment) snapshot.activeDeploymentId = fn.active_deployment.id;
714
+ return snapshot;
715
+ }
716
+ function deploymentToSnapshot(deployment) {
717
+ return {
718
+ id: deployment.id,
719
+ status: normalizeDeploymentStatus(deployment.status)
720
+ };
721
+ }
722
+ function normalizeDeploymentStatus(value) {
723
+ switch (value) {
724
+ case "pending":
725
+ case "building":
726
+ case "completed":
727
+ case "failed": return value;
728
+ default: return "pending";
729
+ }
730
+ }
731
+ /**
732
+ * Whether an error from a Preview-feature read means the feature simply isn't available
733
+ * for this project/branch/region (as opposed to a real, transient failure). Neon signals
734
+ * this a few ways: a 404 "this route does not exist" (the route isn't deployed at all), or
735
+ * a 503/4xx whose message says the platform feature is "not available" / "not enabled".
736
+ *
737
+ * Callers do **not** swallow this into an empty result — touching a Preview feature that
738
+ * isn't available is surfaced as a {@link previewUnavailableError} so `plan` / `status` /
739
+ * `pull` (and `neon dev`) fail clearly instead of, say, planning to create resources the
740
+ * API will refuse to create.
741
+ */
742
+ function isPreviewFeatureUnavailable(err) {
743
+ if (!(err instanceof PlatformError)) return false;
744
+ const status = err.details.status;
745
+ const message = typeof err.details.neonMessage === "string" ? err.details.neonMessage.toLowerCase() : "";
746
+ return (message.includes("not available") || message.includes("does not exist") || message.includes("not enabled")) && (status === 503 || status === 404 || status === 501);
747
+ }
748
+ /**
749
+ * Reason phrase for the handful of HTTP statuses a Preview-feature read can surface as
750
+ * "unavailable". Used to print a short `HTTP <status> <reason>` line (not a stack trace),
751
+ * so the message reads like the API response the user would see in a tool like curl.
752
+ */
753
+ const HTTP_STATUS_TEXT = {
754
+ 401: "Unauthorized",
755
+ 403: "Forbidden",
756
+ 404: "Not Found",
757
+ 500: "Internal Server Error",
758
+ 501: "Not Implemented",
759
+ 503: "Service Unavailable"
760
+ };
761
+ /**
762
+ * Per-status guidance for a Preview feature that came back "unavailable". A preview can be
763
+ * gated several different ways and the HTTP status is the best signal for which, so we tailor
764
+ * the next step instead of emitting one catch-all — valuable while these features are in
765
+ * preview and rolling out region by region:
766
+ *
767
+ * - 404 / 501 — the route isn't deployed for this project's region (or the account isn't in
768
+ * the private preview): create a project in a region where the preview is enabled, and
769
+ * confirm your account has preview access.
770
+ * - 503 — the route exists but is refusing right now: either the preview is still coming up,
771
+ * or Neon is having a transient incident. Retry; if it persists it's likely an incident.
772
+ * - anything else — generic "not enabled for your account/region; request access".
773
+ *
774
+ * Only statuses {@link isPreviewFeatureUnavailable} accepts (404/501/503) actually reach
775
+ * this, so there is intentionally no 401/403 branch — those never classify as "unavailable".
776
+ */
777
+ function previewUnavailableHint(status) {
778
+ switch (status) {
779
+ case 404:
780
+ case 501: return "This usually means the preview isn't available in your project's region yet, or your Neon account isn't in the private preview: create a project in a region where the preview is enabled, and make sure your account has access to the preview.";
781
+ case 503: return "The endpoint is reachable but refused the request — the preview may still be coming up, or Neon may be having a transient incident. Retry shortly; if it keeps failing, check https://neonstatus.com and report it to Neon support.";
782
+ default: return "This usually means the preview isn't enabled for your Neon account or the project's region. Request access to the preview, or use a project in a region where it's available.";
783
+ }
784
+ }
785
+ /**
786
+ * Convert a Preview-feature error into a clear {@link PlatformError} when the feature is
787
+ * unavailable for the project; otherwise pass the original error through unchanged so a
788
+ * genuine failure (auth, transient 5xx, …) keeps its specific code and message.
789
+ *
790
+ * The message names the failing feature, summarizes the response in one short
791
+ * `HTTP <status> <reason>` line, includes the raw Neon API message + request id (valuable
792
+ * signal while the feature is in preview), gives status-specific guidance (see
793
+ * {@link previewUnavailableHint}), and offers removing the feature from the policy as an
794
+ * escape hatch. `status`/`requestId` are also kept on `details` for programmatic consumers.
795
+ */
796
+ function previewUnavailableError(err, featureLabel) {
797
+ if (!isPreviewFeatureUnavailable(err)) return err;
798
+ const details = err instanceof PlatformError ? err.details : {};
799
+ const status = typeof details.status === "number" ? details.status : void 0;
800
+ const neonMessage = typeof details.neonMessage === "string" ? details.neonMessage : void 0;
801
+ const requestId = typeof details.requestId === "string" ? details.requestId : void 0;
802
+ const statusText = status ? HTTP_STATUS_TEXT[status] : void 0;
803
+ const apiParts = [
804
+ status ? `HTTP ${status}${statusText ? ` ${statusText}` : ""}` : void 0,
805
+ neonMessage ? `Neon API said: "${neonMessage}"` : void 0,
806
+ requestId ? `request id ${requestId}` : void 0
807
+ ].filter((part) => part !== void 0);
808
+ const apiContext = apiParts.length > 0 ? ` (${apiParts.join("; ")})` : "";
809
+ return new PlatformError(ErrorCode.FeatureUnavailable, [
810
+ `${featureLabel} is a Preview feature and isn't available for this Neon project${apiContext}.`,
811
+ previewUnavailableHint(status),
812
+ "If you don't need it, remove the corresponding feature from the `preview` block of your neon.ts and re-run."
813
+ ].join(" "), {
814
+ cause: err,
815
+ details: {
816
+ feature: featureLabel,
817
+ ...status !== void 0 ? { status } : {},
818
+ ...requestId !== void 0 ? { requestId } : {}
819
+ }
820
+ });
821
+ }
822
+ function neonAuthResponseToSnapshot(data) {
823
+ const snapshot = {
824
+ projectId: data.auth_provider_project_id,
825
+ jwksUrl: data.jwks_url
826
+ };
827
+ if (data.pub_client_key !== void 0) snapshot.publishableClientKey = data.pub_client_key;
828
+ if (data.secret_server_key !== void 0) snapshot.secretServerKey = data.secret_server_key;
829
+ if (data.base_url) snapshot.baseUrl = data.base_url;
830
+ return snapshot;
831
+ }
832
+ function createNeonAuthRestInput(input) {
833
+ return {
834
+ auth_provider: "better_auth",
835
+ ...input.databaseName ? { database_name: input.databaseName } : {}
836
+ };
837
+ }
838
+ /**
839
+ * Build the `multipart/form-data` body for a function deployment, matching the public
840
+ * `FunctionDeployRequest` schema (`POST .../functions/{slug}/deployments`):
841
+ *
842
+ * - `zip` — the bundle as a binary part (named `bundle.zip`).
843
+ * - `runtime` — the function runtime.
844
+ * - `environment` — a single JSON-encoded string→string map (multipart can't carry a typed
845
+ * object part), omitted entirely when there are no env vars.
846
+ *
847
+ * Pure (no I/O) so it can be unit-tested against the spec without stubbing `fetch`.
848
+ */
849
+ function buildFunctionDeployForm(input) {
850
+ const form = new FormData();
851
+ form.set("zip", new Blob([input.bundle], { type: "application/zip" }), "bundle.zip");
852
+ form.set("runtime", input.runtime);
853
+ if (Object.keys(input.environment).length > 0) form.set("environment", JSON.stringify(input.environment));
854
+ return form;
855
+ }
856
+ /**
857
+ * Read a response body as JSON, tolerating non-JSON. Some Neon routes return a plain-text
858
+ * body (e.g. a 404 `"this route does not exist"` for a Preview feature not enabled in the
859
+ * project/region). Parsing that with `JSON.parse` used to throw a cryptic
860
+ * `SyntaxError: Unexpected token …`, which — because parsing happens before the `res.ok`
861
+ * check in {@link request} — masked the real HTTP status. We instead return the raw text
862
+ * wrapped as `{ message }` so the status-based error path in `request` / `wrapNeonError`
863
+ * runs and produces a proper {@link PlatformError} (e.g. `NotFound`), and a non-error body
864
+ * that simply isn't JSON degrades to text rather than crashing.
865
+ */
866
+ async function readJsonBody(res) {
867
+ const text = await res.text();
868
+ if (text.trim() === "") return {};
869
+ try {
870
+ return JSON.parse(text);
871
+ } catch {
872
+ return { message: text.trim() };
873
+ }
874
+ }
875
+ function projectToSnapshot(project) {
876
+ const defaults = project.default_endpoint_settings;
877
+ const snapshot = {
878
+ id: project.id,
879
+ name: project.name,
880
+ regionId: project.region_id,
881
+ pgVersion: project.pg_version
882
+ };
883
+ if (project.org_id) snapshot.orgId = project.org_id;
884
+ if (defaults) {
885
+ const compute = defaultsToComputeSettings(defaults);
886
+ if (compute) snapshot.defaultEndpointSettings = compute;
887
+ }
888
+ return snapshot;
889
+ }
890
+ function branchToSnapshot(branch) {
891
+ const snapshot = {
892
+ id: branch.id,
893
+ name: branch.name,
894
+ isDefault: branch.default,
895
+ protected: branch.protected === true
896
+ };
897
+ if (branch.parent_id) snapshot.parentId = branch.parent_id;
898
+ if (branch.expires_at) snapshot.expiresAt = branch.expires_at;
899
+ return snapshot;
900
+ }
901
+ function endpointToSnapshot(endpoint) {
902
+ return {
903
+ id: endpoint.id,
904
+ branchId: endpoint.branch_id,
905
+ type: endpoint.type === "read_only" ? "read_only" : "read_write",
906
+ autoscalingLimitMinCu: endpoint.autoscaling_limit_min_cu,
907
+ autoscalingLimitMaxCu: endpoint.autoscaling_limit_max_cu,
908
+ suspendTimeout: formatSuspendTimeout(endpoint.suspend_timeout_seconds)
909
+ };
910
+ }
911
+ function roleToSnapshot(role) {
912
+ return {
913
+ name: role.name,
914
+ branchId: role.branch_id,
915
+ protected: role.protected ?? false
916
+ };
917
+ }
918
+ function databaseToSnapshot(database) {
919
+ return {
920
+ name: database.name,
921
+ branchId: database.branch_id,
922
+ ownerName: database.owner_name
923
+ };
924
+ }
925
+ function computeSettingsToDefaults(settings) {
926
+ const out = {};
927
+ if (settings.autoscalingLimitMinCu !== void 0) out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
928
+ if (settings.autoscalingLimitMaxCu !== void 0) out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
929
+ if (settings.suspendTimeout !== void 0) {
930
+ const parsed = parseSuspendTimeout(settings.suspendTimeout);
931
+ if ("error" in parsed) throw new PlatformError(ErrorCode.InvalidConfig, `Invalid suspendTimeout: ${parsed.error}`);
932
+ out.suspend_timeout_seconds = parsed.seconds;
933
+ }
934
+ return out;
935
+ }
936
+ function computeSettingsToEndpointOptions(settings) {
937
+ const out = {};
938
+ if (settings.autoscalingLimitMinCu !== void 0) out.autoscaling_limit_min_cu = settings.autoscalingLimitMinCu;
939
+ if (settings.autoscalingLimitMaxCu !== void 0) out.autoscaling_limit_max_cu = settings.autoscalingLimitMaxCu;
940
+ if (settings.suspendTimeout !== void 0) {
941
+ const parsed = parseSuspendTimeout(settings.suspendTimeout);
942
+ if ("error" in parsed) throw new PlatformError(ErrorCode.InvalidConfig, `Invalid suspendTimeout: ${parsed.error}`);
943
+ out.suspend_timeout_seconds = parsed.seconds;
944
+ }
945
+ return out;
946
+ }
947
+ function defaultsToComputeSettings(defaults) {
948
+ const out = {};
949
+ if (defaults.autoscaling_limit_min_cu !== void 0) out.autoscalingLimitMinCu = defaults.autoscaling_limit_min_cu;
950
+ if (defaults.autoscaling_limit_max_cu !== void 0) out.autoscalingLimitMaxCu = defaults.autoscaling_limit_max_cu;
951
+ if (defaults.suspend_timeout_seconds !== void 0) out.suspendTimeout = formatSuspendTimeout(defaults.suspend_timeout_seconds);
952
+ return Object.keys(out).length > 0 ? out : void 0;
953
+ }
954
+ //#endregion
955
+ export { buildFunctionDeployForm, createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
956
+
957
+ //# sourceMappingURL=neon-api-real.js.map