neon 8.12.2 → 8.13.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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
|
+
import { log } from "../log.js";
|
|
2
3
|
import { codeFromBody, isNeonApiError, retryOnLock } from "../api.js";
|
|
3
4
|
import { stripAnsi } from "../human_table.js";
|
|
4
5
|
import { writer } from "../writer.js";
|
|
@@ -78,6 +79,24 @@ const ORGANIZATION_FIELDS = [
|
|
|
78
79
|
"organization_limit",
|
|
79
80
|
"creator_role"
|
|
80
81
|
];
|
|
82
|
+
const OAUTH_SERVER_FIELDS = [
|
|
83
|
+
"enabled",
|
|
84
|
+
"login_page",
|
|
85
|
+
"consent_page",
|
|
86
|
+
"mcp",
|
|
87
|
+
"resources"
|
|
88
|
+
];
|
|
89
|
+
const OAUTH_CLIENT_FIELDS = [
|
|
90
|
+
"client_id",
|
|
91
|
+
"name",
|
|
92
|
+
"type",
|
|
93
|
+
"redirect_uris",
|
|
94
|
+
"resources",
|
|
95
|
+
"skip_consent",
|
|
96
|
+
"disabled",
|
|
97
|
+
"created_at"
|
|
98
|
+
];
|
|
99
|
+
const OAUTH_CLIENT_SECRET_FIELDS = ["client_id", "client_secret"];
|
|
81
100
|
const WEBHOOK_FIELDS = [
|
|
82
101
|
"enabled",
|
|
83
102
|
"webhook_url",
|
|
@@ -110,7 +129,7 @@ const builder = (argv) => {
|
|
|
110
129
|
default: false
|
|
111
130
|
} }), async (args) => {
|
|
112
131
|
await disable(args);
|
|
113
|
-
}).command("oauth-provider",
|
|
132
|
+
}).command("oauth-provider", `Manage social login providers (${SUPPORTED_OAUTH_PROVIDERS.join(", ")})`, (yargs) => {
|
|
114
133
|
return yargs.usage("$0 neon-auth oauth-provider <sub-command> [options]").command("list", "List OAuth providers", (yargs) => yargs, async (args) => {
|
|
115
134
|
await oauthProviderList(args);
|
|
116
135
|
}).command("add", "Add an OAuth provider", (yargs) => yargs.options({
|
|
@@ -155,6 +174,87 @@ const builder = (argv) => {
|
|
|
155
174
|
} }), async (args) => {
|
|
156
175
|
await oauthProviderDelete(args);
|
|
157
176
|
});
|
|
177
|
+
}).command("oauth-client", "Manage clients of this Neon Auth OAuth server (e.g. MCP clients); configure the server with `config oauth-server`", (yargs) => {
|
|
178
|
+
return yargs.usage("$0 neon-auth oauth-client <sub-command> [options]").command("list", "List OAuth clients", (yargs) => yargs, async (args) => {
|
|
179
|
+
await oauthClientList(args);
|
|
180
|
+
}).command("create", "Create an OAuth client", (yargs) => yargs.options({
|
|
181
|
+
name: {
|
|
182
|
+
describe: "Display name of the client",
|
|
183
|
+
type: "string",
|
|
184
|
+
demandOption: true
|
|
185
|
+
},
|
|
186
|
+
type: {
|
|
187
|
+
describe: "Client type. Confidential clients receive a secret that is shown only once",
|
|
188
|
+
type: "string",
|
|
189
|
+
choices: ["confidential", "public"],
|
|
190
|
+
demandOption: true
|
|
191
|
+
},
|
|
192
|
+
"redirect-uris": {
|
|
193
|
+
describe: "Redirect URIs the client may use (https, or http on loopback when localhost is allowed)",
|
|
194
|
+
type: "string",
|
|
195
|
+
array: true,
|
|
196
|
+
demandOption: true
|
|
197
|
+
},
|
|
198
|
+
resources: {
|
|
199
|
+
describe: "Protected resource URLs to link the client to; each must be configured with `neon-auth config oauth-server resource add`",
|
|
200
|
+
type: "string",
|
|
201
|
+
array: true,
|
|
202
|
+
demandOption: true
|
|
203
|
+
},
|
|
204
|
+
"skip-consent": {
|
|
205
|
+
describe: "Skip the consent prompt (first-party clients only)",
|
|
206
|
+
type: "boolean"
|
|
207
|
+
}
|
|
208
|
+
}).example("$0 neon-auth oauth-client create --name 'My app' --type confidential --redirect-uris https://app.example.com/callback --resources https://api.example.com", "Create a confidential client"), async (args) => {
|
|
209
|
+
await oauthClientCreate(args);
|
|
210
|
+
}).command("get <client-id>", "Get an OAuth client", (yargs) => yargs.usage("$0 neon-auth oauth-client get <client-id> [options]").positional("client-id", {
|
|
211
|
+
describe: "OAuth client ID",
|
|
212
|
+
type: "string",
|
|
213
|
+
demandOption: true
|
|
214
|
+
}), async (args) => {
|
|
215
|
+
await oauthClientGet(args);
|
|
216
|
+
}).command("update <client-id>", "Update an OAuth client (the type cannot be changed)", (yargs) => yargs.usage("$0 neon-auth oauth-client update <client-id> [options]").positional("client-id", {
|
|
217
|
+
describe: "OAuth client ID",
|
|
218
|
+
type: "string",
|
|
219
|
+
demandOption: true
|
|
220
|
+
}).options({
|
|
221
|
+
name: {
|
|
222
|
+
describe: "Display name of the client",
|
|
223
|
+
type: "string"
|
|
224
|
+
},
|
|
225
|
+
"redirect-uris": {
|
|
226
|
+
describe: "Redirect URIs the client may use; replaces the stored list",
|
|
227
|
+
type: "string",
|
|
228
|
+
array: true
|
|
229
|
+
},
|
|
230
|
+
resources: {
|
|
231
|
+
describe: "Protected resource URLs to link the client to; replaces the stored list (at least one)",
|
|
232
|
+
type: "string",
|
|
233
|
+
array: true
|
|
234
|
+
},
|
|
235
|
+
"skip-consent": {
|
|
236
|
+
describe: "Skip the consent prompt (first-party clients only). Use --no-skip-consent or --skip-consent false to turn off",
|
|
237
|
+
type: "boolean"
|
|
238
|
+
},
|
|
239
|
+
disabled: {
|
|
240
|
+
describe: "Disable the client. Use --no-disabled or --disabled false to re-enable",
|
|
241
|
+
type: "boolean"
|
|
242
|
+
}
|
|
243
|
+
}), async (args) => {
|
|
244
|
+
await oauthClientUpdate(args);
|
|
245
|
+
}).command("delete <client-id>", "Delete an OAuth client", (yargs) => yargs.usage("$0 neon-auth oauth-client delete <client-id> [options]").positional("client-id", {
|
|
246
|
+
describe: "OAuth client ID",
|
|
247
|
+
type: "string",
|
|
248
|
+
demandOption: true
|
|
249
|
+
}), async (args) => {
|
|
250
|
+
await oauthClientDelete(args);
|
|
251
|
+
}).command("rotate-secret <client-id>", "Rotate the secret of a confidential client; the old secret stops working immediately", (yargs) => yargs.usage("$0 neon-auth oauth-client rotate-secret <client-id> [options]").positional("client-id", {
|
|
252
|
+
describe: "OAuth client ID",
|
|
253
|
+
type: "string",
|
|
254
|
+
demandOption: true
|
|
255
|
+
}), async (args) => {
|
|
256
|
+
await oauthClientRotateSecret(args);
|
|
257
|
+
});
|
|
158
258
|
}).command("domain", "Manage redirect URI trusted domains", (yargs) => {
|
|
159
259
|
return yargs.usage("$0 neon-auth domain <sub-command> [options]").command("list", "List trusted domains", (yargs) => yargs, async (args) => {
|
|
160
260
|
await domainList(args);
|
|
@@ -283,6 +383,63 @@ const builder = (argv) => {
|
|
|
283
383
|
}), async (args) => {
|
|
284
384
|
await organizationUpdate(args);
|
|
285
385
|
});
|
|
386
|
+
}).command("oauth-server", "Manage the OAuth 2.1 authorization server (API plugin: oauth_provider)", (yargs) => {
|
|
387
|
+
return yargs.usage("$0 neon-auth config oauth-server <sub-command> [options]").command("get", "Get OAuth server config", (yargs) => yargs, async (args) => {
|
|
388
|
+
await oauthServerGet(args);
|
|
389
|
+
}).command("update", "Update OAuth server config", (yargs) => yargs.options({
|
|
390
|
+
enabled: {
|
|
391
|
+
describe: "Enable the OAuth server. Use --no-enabled or --enabled false to disable",
|
|
392
|
+
type: "boolean"
|
|
393
|
+
},
|
|
394
|
+
"login-page": {
|
|
395
|
+
describe: "URL of the page authorization requests redirect to for sign-in",
|
|
396
|
+
type: "string"
|
|
397
|
+
},
|
|
398
|
+
"consent-page": {
|
|
399
|
+
describe: "URL of the page authorization requests redirect to for consent",
|
|
400
|
+
type: "string"
|
|
401
|
+
},
|
|
402
|
+
resources: {
|
|
403
|
+
describe: "Protected resource URLs that clients can be linked to; replaces the stored list. Use `resource add|remove` for incremental changes",
|
|
404
|
+
type: "string",
|
|
405
|
+
array: true
|
|
406
|
+
},
|
|
407
|
+
"mcp-resource": {
|
|
408
|
+
describe: "MCP server URL that MCP clients are linked to. Defaults to the stored value when another --mcp-* flag is given. To remove the MCP configuration, use --no-mcp",
|
|
409
|
+
type: "string"
|
|
410
|
+
},
|
|
411
|
+
mcp: {
|
|
412
|
+
describe: "Remove the MCP configuration (only --no-mcp is accepted)",
|
|
413
|
+
type: "boolean"
|
|
414
|
+
},
|
|
415
|
+
"mcp-client-id-metadata-documents": {
|
|
416
|
+
describe: "Allow MCP clients to identify themselves with a Client ID Metadata Document (CIMD) URL. Use --no-mcp-client-id-metadata-documents to disallow",
|
|
417
|
+
type: "boolean"
|
|
418
|
+
},
|
|
419
|
+
"mcp-dynamic-registration": {
|
|
420
|
+
describe: "Allow MCP clients to register themselves via dynamic client registration (deprecated in favor of CIMD)",
|
|
421
|
+
type: "boolean",
|
|
422
|
+
hidden: true
|
|
423
|
+
}
|
|
424
|
+
}).example("$0 neon-auth config oauth-server update --enabled --mcp-resource https://example.com/mcp --mcp-client-id-metadata-documents", "Enable the OAuth server for an MCP server"), async (args) => {
|
|
425
|
+
await oauthServerUpdate(args);
|
|
426
|
+
}).command("resource", "Manage the protected resources of the OAuth server. Add and remove read the stored list and write back the full list, so concurrent changes by others can be overwritten", (yargs) => yargs.usage("$0 neon-auth config oauth-server resource <sub-command> [options]").command("list", "List protected resources", (yargs) => yargs, async (args) => {
|
|
427
|
+
await oauthResourceList(args);
|
|
428
|
+
}).command("add <url...>", "Add protected resources (reads the stored list and writes back the full list; concurrent changes by others can be overwritten)", (yargs) => yargs.positional("url", {
|
|
429
|
+
describe: "Protected resource URLs to add",
|
|
430
|
+
type: "string",
|
|
431
|
+
array: true,
|
|
432
|
+
demandOption: true
|
|
433
|
+
}), async (args) => {
|
|
434
|
+
await oauthResourceAdd(args);
|
|
435
|
+
}).command("remove <url...>", "Remove protected resources (reads the stored list and writes back the full list; concurrent changes by others can be overwritten)", (yargs) => yargs.positional("url", {
|
|
436
|
+
describe: "Protected resource URLs to remove",
|
|
437
|
+
type: "string",
|
|
438
|
+
array: true,
|
|
439
|
+
demandOption: true
|
|
440
|
+
}), async (args) => {
|
|
441
|
+
await oauthResourceRemove(args);
|
|
442
|
+
}));
|
|
286
443
|
}).command("webhook", "Manage webhook configuration", (yargs) => {
|
|
287
444
|
return yargs.usage("$0 neon-auth config webhook <sub-command> [options]").command("get", "Get webhook config", (yargs) => yargs, async (args) => {
|
|
288
445
|
await webhookGet(args);
|
|
@@ -319,7 +476,7 @@ const builder = (argv) => {
|
|
|
319
476
|
return yargs.usage("$0 neon-auth plugins <sub-command> [options]").command("list", "List all plugin configurations", (yargs) => yargs, async (args) => {
|
|
320
477
|
await pluginsList(args);
|
|
321
478
|
}).command("get <plugin-name>", "Get a specific plugin configuration", (yargs) => yargs.usage("$0 neon-auth plugins get <plugin-name> [options]").positional("plugin-name", {
|
|
322
|
-
describe: "Plugin name (e.g. organization, email_provider, email_and_password, oauth_providers, allow_localhost)",
|
|
479
|
+
describe: "Plugin name (e.g. organization, email_provider, email_and_password, oauth_providers, oauth_provider, allow_localhost)",
|
|
323
480
|
type: "string",
|
|
324
481
|
demandOption: true
|
|
325
482
|
}), async (args) => {
|
|
@@ -714,6 +871,287 @@ const organizationUpdate = async (props) => {
|
|
|
714
871
|
}
|
|
715
872
|
printKvBlock("Organization configuration updated", printOrganizationEntries(data));
|
|
716
873
|
};
|
|
874
|
+
const optionalBool = (v) => v === void 0 ? void 0 : String(v);
|
|
875
|
+
const printOAuthServerEntries = (data) => [
|
|
876
|
+
["Enabled:", String(data.enabled)],
|
|
877
|
+
["Login Page:", data.login_page],
|
|
878
|
+
["Consent Page:", data.consent_page],
|
|
879
|
+
["MCP Resource:", data.mcp?.resource],
|
|
880
|
+
["MCP CIMD:", optionalBool(data.mcp?.client_id_metadata_documents)],
|
|
881
|
+
["MCP Dynamic Reg:", optionalBool(data.mcp?.dynamic_registration)],
|
|
882
|
+
["Resources:", (data.resources ?? []).map((r) => r.identifier).join(", ")]
|
|
883
|
+
];
|
|
884
|
+
/** The API rejects an empty PATCH, so fail before any request with the flags that would do something. */
|
|
885
|
+
const requireUpdateFields = (provided, flags) => {
|
|
886
|
+
if (provided.every((value) => value === void 0)) throw new Error(`Nothing to update. Pass at least one of ${flags.join(", ")}.`);
|
|
887
|
+
};
|
|
888
|
+
const oauthServerGet = async (props) => {
|
|
889
|
+
const branchId = await resolveBranch(props);
|
|
890
|
+
const { data } = await props.apiClient.getNeonAuthOAuthProviderPlugin(props.projectId, branchId);
|
|
891
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
892
|
+
writer(props).end(data, { fields: OAUTH_SERVER_FIELDS });
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
printKvBlock("OAuth server configuration", printOAuthServerEntries(data));
|
|
896
|
+
};
|
|
897
|
+
/**
|
|
898
|
+
* The API requires `mcp.resource` whenever `mcp` is sent and keeps stored booleans that are
|
|
899
|
+
* omitted, so only the resource is filled in from the stored config.
|
|
900
|
+
*/
|
|
901
|
+
const resolveMcpUpdate = async (props, getStoredMcp) => {
|
|
902
|
+
const { mcpResource, mcpDynamicRegistration: dynamicRegistration, mcpClientIdMetadataDocuments: clientIdMetadataDocuments } = props;
|
|
903
|
+
if (mcpResource === void 0 && dynamicRegistration === void 0 && clientIdMetadataDocuments === void 0) return;
|
|
904
|
+
const resource = mcpResource ?? (await getStoredMcp())?.resource;
|
|
905
|
+
if (!resource) throw new Error("No MCP resource is configured yet. Pass --mcp-resource <url> together with the other --mcp-* flags.");
|
|
906
|
+
return {
|
|
907
|
+
resource,
|
|
908
|
+
dynamic_registration: dynamicRegistration,
|
|
909
|
+
client_id_metadata_documents: clientIdMetadataDocuments
|
|
910
|
+
};
|
|
911
|
+
};
|
|
912
|
+
/**
|
|
913
|
+
* Order-preserving dedupe after trimming: padded input would otherwise miss stored values
|
|
914
|
+
* and reach the API as a distinct string, and repeats would be sent as duplicates.
|
|
915
|
+
*/
|
|
916
|
+
const unique = (values) => [...new Set(values.map((value) => value.trim()))];
|
|
917
|
+
/** yargs negation turns `--no-<flag>` on an array option into `[false]`, which bypasses empty-array checks. */
|
|
918
|
+
const isNegatedList = (values) => values === false || Array.isArray(values) && values.includes(false);
|
|
919
|
+
const hasBlank = (values) => values?.some((value) => value.trim() === "") ?? false;
|
|
920
|
+
/** Fails before any request when a list flag was passed with no values or a blank one. */
|
|
921
|
+
const requireNonEmptyLists = (lists) => {
|
|
922
|
+
for (const [flag, values] of lists) {
|
|
923
|
+
if (isNegatedList(values)) throw new Error(`--no-${flag.slice(2)} is not supported. ${flag} must list at least one value.`);
|
|
924
|
+
if (values?.length === 0) throw new Error(`${flag} must list at least one value.`);
|
|
925
|
+
if (hasBlank(values)) throw new Error(`${flag} must not contain empty values.`);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
/**
|
|
929
|
+
* yargs negation turns `--no-<flag>` on a string option into `false`, and `--flag ""` yields an
|
|
930
|
+
* empty string; both would otherwise crash on `.trim()` or be sent to the API.
|
|
931
|
+
*/
|
|
932
|
+
const requireNonBlankString = (flag, value, hint) => {
|
|
933
|
+
if (value === false) throw new Error(`--no-${flag.slice(2)} is not supported. ${hint}`);
|
|
934
|
+
if (typeof value === "string" && value.trim() === "") throw new Error(`${flag} must not be empty.`);
|
|
935
|
+
};
|
|
936
|
+
const requireNonBlankUrls = (urls) => {
|
|
937
|
+
if (hasBlank(urls)) throw new Error("URLs must not be empty.");
|
|
938
|
+
};
|
|
939
|
+
/** The MCP resource is linked to MCP clients on its own, so it cannot also be a protected resource. */
|
|
940
|
+
const rejectMcpResourceCollision = (urls, mcpResource) => {
|
|
941
|
+
if (mcpResource && urls.includes(mcpResource)) throw new Error(`${mcpResource} is the MCP resource and cannot also be a protected resource.`);
|
|
942
|
+
};
|
|
943
|
+
/** Checked before any request so a malformed invocation never reaches the API. */
|
|
944
|
+
const validateOAuthServerUpdate = (props) => {
|
|
945
|
+
requireNonBlankString("--login-page", props.loginPage, "Pass --login-page <url>.");
|
|
946
|
+
requireNonBlankString("--consent-page", props.consentPage, "Pass --consent-page <url>.");
|
|
947
|
+
if (props.mcp === true) throw new Error("--mcp cannot be enabled. Configure MCP with --mcp-resource <url>, or remove it with --no-mcp.");
|
|
948
|
+
if (props.mcp === false && (props.mcpResource !== void 0 || props.mcpClientIdMetadataDocuments !== void 0 || props.mcpDynamicRegistration !== void 0)) throw new Error("--no-mcp cannot be combined with --mcp-resource, --mcp-client-id-metadata-documents or --mcp-dynamic-registration.");
|
|
949
|
+
if (props.mcpResource === false) throw new Error("--no-mcp-resource is not supported. To remove the MCP configuration, use --no-mcp.");
|
|
950
|
+
if (props.mcpResource?.trim() === "") throw new Error("--mcp-resource must not be empty. To remove the MCP configuration, use --no-mcp.");
|
|
951
|
+
if (isNegatedList(props.resources)) throw new Error("--no-resources is not supported. To remove resources, use `neon-auth config oauth-server resource remove <url...>`.");
|
|
952
|
+
if (props.resources?.length === 0) throw new Error("--resources must list at least one URL. To remove resources, use `neon-auth config oauth-server resource remove <url...>`.");
|
|
953
|
+
if (hasBlank(props.resources)) throw new Error("--resources must not contain empty values.");
|
|
954
|
+
};
|
|
955
|
+
const oauthServerUpdate = async (props) => {
|
|
956
|
+
requireUpdateFields([
|
|
957
|
+
props.enabled,
|
|
958
|
+
props.loginPage,
|
|
959
|
+
props.consentPage,
|
|
960
|
+
props.resources,
|
|
961
|
+
props.mcp,
|
|
962
|
+
props.mcpResource,
|
|
963
|
+
props.mcpClientIdMetadataDocuments,
|
|
964
|
+
props.mcpDynamicRegistration
|
|
965
|
+
], [
|
|
966
|
+
"--enabled",
|
|
967
|
+
"--login-page",
|
|
968
|
+
"--consent-page",
|
|
969
|
+
"--resources",
|
|
970
|
+
"--no-mcp",
|
|
971
|
+
"--mcp-resource",
|
|
972
|
+
"--mcp-client-id-metadata-documents"
|
|
973
|
+
]);
|
|
974
|
+
validateOAuthServerUpdate(props);
|
|
975
|
+
const loginPage = props.loginPage?.trim();
|
|
976
|
+
const consentPage = props.consentPage?.trim();
|
|
977
|
+
const mcpResource = props.mcpResource?.trim();
|
|
978
|
+
const branchId = await resolveBranch(props);
|
|
979
|
+
let storedMcp;
|
|
980
|
+
const getStoredMcp = () => storedMcp ??= props.apiClient.getNeonAuthOAuthProviderPlugin(props.projectId, branchId).then(({ data }) => data.mcp);
|
|
981
|
+
const mcp = props.mcp === false ? { resource: "" } : await resolveMcpUpdate({
|
|
982
|
+
...props,
|
|
983
|
+
mcpResource
|
|
984
|
+
}, getStoredMcp);
|
|
985
|
+
const resources = props.resources && unique(props.resources);
|
|
986
|
+
if (resources && props.mcp !== false) rejectMcpResourceCollision(resources, mcp?.resource ?? (await getStoredMcp())?.resource);
|
|
987
|
+
if (props.mcpDynamicRegistration !== void 0) log.warning("--mcp-dynamic-registration is deprecated. Use --mcp-client-id-metadata-documents instead.");
|
|
988
|
+
const { data } = await props.apiClient.updateNeonAuthOAuthProviderPlugin(props.projectId, branchId, {
|
|
989
|
+
enabled: props.enabled,
|
|
990
|
+
login_page: loginPage,
|
|
991
|
+
consent_page: consentPage,
|
|
992
|
+
mcp,
|
|
993
|
+
resources: resources?.map((identifier) => ({ identifier }))
|
|
994
|
+
});
|
|
995
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
996
|
+
writer(props).end(data, { fields: OAUTH_SERVER_FIELDS });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
printKvBlock("OAuth server configuration updated", printOAuthServerEntries(data));
|
|
1000
|
+
};
|
|
1001
|
+
const getStoredOAuthServer = async (props) => {
|
|
1002
|
+
const branchId = await resolveBranch(props);
|
|
1003
|
+
const { data } = await props.apiClient.getNeonAuthOAuthProviderPlugin(props.projectId, branchId);
|
|
1004
|
+
return {
|
|
1005
|
+
branchId,
|
|
1006
|
+
mcpResource: data.mcp?.resource,
|
|
1007
|
+
stored: (data.resources ?? []).map((r) => r.identifier)
|
|
1008
|
+
};
|
|
1009
|
+
};
|
|
1010
|
+
const reportOAuthResources = (props, resources) => {
|
|
1011
|
+
if (resources.length === 0 && props.output === "table") {
|
|
1012
|
+
printMessage("No protected resources are configured");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
writer(props).end(resources, { fields: ["identifier"] });
|
|
1016
|
+
};
|
|
1017
|
+
/** The API replaces the whole list, so every change is a full-list PATCH. */
|
|
1018
|
+
const replaceOAuthResources = async (props, branchId, identifiers) => {
|
|
1019
|
+
const { data } = await props.apiClient.updateNeonAuthOAuthProviderPlugin(props.projectId, branchId, { resources: identifiers.map((identifier) => ({ identifier })) });
|
|
1020
|
+
reportOAuthResources(props, data.resources ?? []);
|
|
1021
|
+
};
|
|
1022
|
+
const oauthResourceList = async (props) => {
|
|
1023
|
+
const { stored } = await getStoredOAuthServer(props);
|
|
1024
|
+
reportOAuthResources(props, stored.map((identifier) => ({ identifier })));
|
|
1025
|
+
};
|
|
1026
|
+
const oauthResourceAdd = async (props) => {
|
|
1027
|
+
requireNonBlankUrls(props.url);
|
|
1028
|
+
const urls = unique(props.url);
|
|
1029
|
+
const { branchId, mcpResource, stored } = await getStoredOAuthServer(props);
|
|
1030
|
+
rejectMcpResourceCollision(urls, mcpResource);
|
|
1031
|
+
const existing = urls.filter((url) => stored.includes(url));
|
|
1032
|
+
if (existing.length > 0) log.warning(`Already configured, skipped: ${existing.join(", ")}`);
|
|
1033
|
+
const added = urls.filter((url) => !stored.includes(url));
|
|
1034
|
+
if (added.length === 0) {
|
|
1035
|
+
reportOAuthResources(props, stored.map((identifier) => ({ identifier })));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
await replaceOAuthResources(props, branchId, [...stored, ...added]);
|
|
1039
|
+
};
|
|
1040
|
+
const oauthResourceRemove = async (props) => {
|
|
1041
|
+
requireNonBlankUrls(props.url);
|
|
1042
|
+
const requested = unique(props.url);
|
|
1043
|
+
const { branchId, stored } = await getStoredOAuthServer(props);
|
|
1044
|
+
const unknown = requested.filter((url) => !stored.includes(url));
|
|
1045
|
+
if (unknown.length > 0) log.warning(`Not configured, skipped: ${unknown.join(", ")}`);
|
|
1046
|
+
const urls = requested.filter((url) => stored.includes(url));
|
|
1047
|
+
if (urls.length === 0) {
|
|
1048
|
+
reportOAuthResources(props, stored.map((identifier) => ({ identifier })));
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
await replaceOAuthResources(props, branchId, stored.filter((url) => !urls.includes(url)));
|
|
1052
|
+
let linked;
|
|
1053
|
+
try {
|
|
1054
|
+
const { data } = await props.apiClient.listNeonAuthOAuthClients(props.projectId, branchId);
|
|
1055
|
+
linked = data.clients.filter((client) => client.resources.some((resource) => urls.includes(resource)));
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
log.debug("Could not look up clients linked to the removed resource: %s", err);
|
|
1058
|
+
log.warning("Removed, but could not check for clients still linked to it.");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (linked.length > 0) log.warning(`Clients still linked to a removed resource: ${linked.map((client) => `"${client.name}" (${client.client_id})`).join(", ")}. Their token requests for that resource may fail until it is added again.`);
|
|
1062
|
+
};
|
|
1063
|
+
const printOAuthClientEntries = (data) => [
|
|
1064
|
+
["Client ID:", data.client_id],
|
|
1065
|
+
["Name:", data.name],
|
|
1066
|
+
["Type:", data.type],
|
|
1067
|
+
["Redirect URIs:", data.redirect_uris.join(", ")],
|
|
1068
|
+
["Resources:", data.resources.join(", ")],
|
|
1069
|
+
["Skip Consent:", String(data.skip_consent)],
|
|
1070
|
+
["Disabled:", String(data.disabled)],
|
|
1071
|
+
["Created At:", data.created_at]
|
|
1072
|
+
];
|
|
1073
|
+
const oauthClientList = async (props) => {
|
|
1074
|
+
const branchId = await resolveBranch(props);
|
|
1075
|
+
const { data } = await props.apiClient.listNeonAuthOAuthClients(props.projectId, branchId);
|
|
1076
|
+
if (data.clients.length === 0 && props.output === "table") {
|
|
1077
|
+
printMessage("No OAuth clients are configured for this branch.");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
writer(props).end(data.clients, { fields: OAUTH_CLIENT_FIELDS });
|
|
1081
|
+
};
|
|
1082
|
+
/**
|
|
1083
|
+
* The secret is shown only once, so human output keeps it on its own last line of stdout
|
|
1084
|
+
* and the notice goes to stderr. Structured output keeps it in the object for scripts.
|
|
1085
|
+
*/
|
|
1086
|
+
const reportOAuthClientSecret = (props, data, fields, entries, title) => {
|
|
1087
|
+
if (props.output === "table") {
|
|
1088
|
+
printKvBlock(title, entries);
|
|
1089
|
+
if (data.client_secret) writeStyled(`${data.client_secret}\n`);
|
|
1090
|
+
} else writer(props).end(data, { fields });
|
|
1091
|
+
if (data.client_secret) log.warning("Store this secret now: it is not shown again.");
|
|
1092
|
+
};
|
|
1093
|
+
const oauthClientCreate = async (props) => {
|
|
1094
|
+
requireNonBlankString("--name", props.name, "Pass --name <name>.");
|
|
1095
|
+
requireNonEmptyLists([["--redirect-uris", props.redirectUris], ["--resources", props.resources]]);
|
|
1096
|
+
const branchId = await resolveBranch(props);
|
|
1097
|
+
const { data } = await props.apiClient.createNeonAuthOAuthClient(props.projectId, branchId, {
|
|
1098
|
+
name: props.name.trim(),
|
|
1099
|
+
type: props.type,
|
|
1100
|
+
redirect_uris: unique(props.redirectUris),
|
|
1101
|
+
resources: unique(props.resources),
|
|
1102
|
+
skip_consent: props.skipConsent
|
|
1103
|
+
});
|
|
1104
|
+
reportOAuthClientSecret(props, data, [...OAUTH_CLIENT_FIELDS, "client_secret"], printOAuthClientEntries(data), "OAuth client");
|
|
1105
|
+
};
|
|
1106
|
+
const oauthClientGet = async (props) => {
|
|
1107
|
+
const branchId = await resolveBranch(props);
|
|
1108
|
+
const { data } = await props.apiClient.getNeonAuthOAuthClient(props.projectId, branchId, props.clientId);
|
|
1109
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
1110
|
+
writer(props).end(data, { fields: OAUTH_CLIENT_FIELDS });
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
printKvBlock("OAuth client", printOAuthClientEntries(data));
|
|
1114
|
+
};
|
|
1115
|
+
const oauthClientUpdate = async (props) => {
|
|
1116
|
+
requireNonBlankString("--name", props.name, "Pass --name <name>.");
|
|
1117
|
+
requireNonEmptyLists([["--redirect-uris", props.redirectUris], ["--resources", props.resources]]);
|
|
1118
|
+
requireUpdateFields([
|
|
1119
|
+
props.name,
|
|
1120
|
+
props.redirectUris,
|
|
1121
|
+
props.resources,
|
|
1122
|
+
props.skipConsent,
|
|
1123
|
+
props.disabled
|
|
1124
|
+
], [
|
|
1125
|
+
"--name",
|
|
1126
|
+
"--redirect-uris",
|
|
1127
|
+
"--resources",
|
|
1128
|
+
"--skip-consent",
|
|
1129
|
+
"--disabled"
|
|
1130
|
+
]);
|
|
1131
|
+
const branchId = await resolveBranch(props);
|
|
1132
|
+
const { data } = await props.apiClient.updateNeonAuthOAuthClient(props.projectId, branchId, props.clientId, {
|
|
1133
|
+
name: props.name?.trim(),
|
|
1134
|
+
redirect_uris: props.redirectUris && unique(props.redirectUris),
|
|
1135
|
+
resources: props.resources && unique(props.resources),
|
|
1136
|
+
skip_consent: props.skipConsent,
|
|
1137
|
+
disabled: props.disabled
|
|
1138
|
+
});
|
|
1139
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
1140
|
+
writer(props).end(data, { fields: OAUTH_CLIENT_FIELDS });
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
printKvBlock("OAuth client updated", printOAuthClientEntries(data));
|
|
1144
|
+
};
|
|
1145
|
+
const oauthClientDelete = async (props) => {
|
|
1146
|
+
const branchId = await resolveBranch(props);
|
|
1147
|
+
await props.apiClient.deleteNeonAuthOAuthClient(props.projectId, branchId, props.clientId);
|
|
1148
|
+
printMessage(`OAuth client "${props.clientId}" deleted`);
|
|
1149
|
+
};
|
|
1150
|
+
const oauthClientRotateSecret = async (props) => {
|
|
1151
|
+
const branchId = await resolveBranch(props);
|
|
1152
|
+
const { data } = await props.apiClient.rotateNeonAuthOAuthClientSecret(props.projectId, branchId, props.clientId);
|
|
1153
|
+
reportOAuthClientSecret(props, data, OAUTH_CLIENT_SECRET_FIELDS, [["Client ID:", data.client_id]], "OAuth client secret");
|
|
1154
|
+
};
|
|
717
1155
|
const printWebhookEntries = (data) => [
|
|
718
1156
|
["Enabled:", String(data.enabled)],
|
|
719
1157
|
["URL:", data.webhook_url ?? ""],
|
package/dist/api.js
CHANGED
|
@@ -895,6 +895,69 @@ const getApiClient = ({ apiKey, apiHost, requestTimeoutMs = REQUEST_TIMEOUT_MS }
|
|
|
895
895
|
},
|
|
896
896
|
body: data
|
|
897
897
|
})),
|
|
898
|
+
getNeonAuthOAuthProviderPlugin: (projectId, branchId) => call(() => raw.getNeonAuthOAuthProviderPlugin({
|
|
899
|
+
client,
|
|
900
|
+
path: {
|
|
901
|
+
project_id: projectId,
|
|
902
|
+
branch_id: branchId
|
|
903
|
+
}
|
|
904
|
+
})),
|
|
905
|
+
updateNeonAuthOAuthProviderPlugin: (projectId, branchId, data) => call(() => raw.updateNeonAuthOAuthProviderPlugin({
|
|
906
|
+
client,
|
|
907
|
+
path: {
|
|
908
|
+
project_id: projectId,
|
|
909
|
+
branch_id: branchId
|
|
910
|
+
},
|
|
911
|
+
body: data
|
|
912
|
+
})),
|
|
913
|
+
listNeonAuthOAuthClients: (projectId, branchId) => call(() => raw.listNeonAuthOAuthClients({
|
|
914
|
+
client,
|
|
915
|
+
path: {
|
|
916
|
+
project_id: projectId,
|
|
917
|
+
branch_id: branchId
|
|
918
|
+
}
|
|
919
|
+
})),
|
|
920
|
+
createNeonAuthOAuthClient: (projectId, branchId, data) => call(() => raw.createNeonAuthOAuthClient({
|
|
921
|
+
client,
|
|
922
|
+
path: {
|
|
923
|
+
project_id: projectId,
|
|
924
|
+
branch_id: branchId
|
|
925
|
+
},
|
|
926
|
+
body: data
|
|
927
|
+
})),
|
|
928
|
+
getNeonAuthOAuthClient: (projectId, branchId, clientId) => call(() => raw.getNeonAuthOAuthClient({
|
|
929
|
+
client,
|
|
930
|
+
path: {
|
|
931
|
+
project_id: projectId,
|
|
932
|
+
branch_id: branchId,
|
|
933
|
+
client_id: clientId
|
|
934
|
+
}
|
|
935
|
+
})),
|
|
936
|
+
updateNeonAuthOAuthClient: (projectId, branchId, clientId, data) => call(() => raw.updateNeonAuthOAuthClient({
|
|
937
|
+
client,
|
|
938
|
+
path: {
|
|
939
|
+
project_id: projectId,
|
|
940
|
+
branch_id: branchId,
|
|
941
|
+
client_id: clientId
|
|
942
|
+
},
|
|
943
|
+
body: data
|
|
944
|
+
})),
|
|
945
|
+
deleteNeonAuthOAuthClient: (projectId, branchId, clientId) => call(() => raw.deleteNeonAuthOAuthClient({
|
|
946
|
+
client,
|
|
947
|
+
path: {
|
|
948
|
+
project_id: projectId,
|
|
949
|
+
branch_id: branchId,
|
|
950
|
+
client_id: clientId
|
|
951
|
+
}
|
|
952
|
+
})),
|
|
953
|
+
rotateNeonAuthOAuthClientSecret: (projectId, branchId, clientId) => call(() => raw.rotateNeonAuthOAuthClientSecret({
|
|
954
|
+
client,
|
|
955
|
+
path: {
|
|
956
|
+
project_id: projectId,
|
|
957
|
+
branch_id: branchId,
|
|
958
|
+
client_id: clientId
|
|
959
|
+
}
|
|
960
|
+
})),
|
|
898
961
|
getNeonAuthWebhookConfig: (projectId, branchId) => call(() => raw.getNeonAuthWebhookConfig({
|
|
899
962
|
client,
|
|
900
963
|
path: {
|
package/dist/commands/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { a as init_exports } from "../_chunks/init-BkA6RilG.js";
|
|
|
29
29
|
import { o as inspect_exports } from "../_chunks/inspect-g0wekylj.js";
|
|
30
30
|
import { a as ip_allow_exports } from "../_chunks/ip_allow-5lLFvIuE.js";
|
|
31
31
|
import { l as logs_exports } from "../_chunks/logs-DWkpjzIH.js";
|
|
32
|
-
import { a as neon_auth_exports } from "../_chunks/neon_auth-
|
|
32
|
+
import { a as neon_auth_exports } from "../_chunks/neon_auth-DpF83S24.js";
|
|
33
33
|
import { a as open_exports } from "../_chunks/open-CX3BT3Tp.js";
|
|
34
34
|
import { s as operations_exports } from "../_chunks/operations-D4I-AxVs.js";
|
|
35
35
|
import { o as orgs_exports } from "../_chunks/orgs-xGfANwHQ.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as builder, n as command, r as describe, i as handler } from "../_chunks/neon_auth-
|
|
1
|
+
export { t as builder, n as command, r as describe, i as handler } from "../_chunks/neon_auth-DpF83S24.js";
|
|
@@ -48,14 +48,15 @@ const test = test$1.extend({
|
|
|
48
48
|
let startedServer;
|
|
49
49
|
await use(async (mockDir, onRequest) => {
|
|
50
50
|
const app = express();
|
|
51
|
+
app.use(express.json());
|
|
51
52
|
if (onRequest) app.use((req, _res, next) => {
|
|
52
53
|
onRequest({
|
|
53
54
|
method: req.method,
|
|
54
|
-
path: req.path
|
|
55
|
+
path: req.path,
|
|
56
|
+
body: req.body
|
|
55
57
|
});
|
|
56
58
|
next();
|
|
57
59
|
});
|
|
58
|
-
app.use(express.json());
|
|
59
60
|
app.use("/", emocks(join(process.cwd(), "mocks", mockDir), { "404": (_req, res) => res.status(404).send({ message: "Not Found" }) }));
|
|
60
61
|
const server = await new Promise((resolve) => {
|
|
61
62
|
const s = app.listen(0, () => {
|
|
@@ -76,7 +77,7 @@ const test = test$1.extend({
|
|
|
76
77
|
},
|
|
77
78
|
testCliCommand: async ({ runMockServer }, use) => {
|
|
78
79
|
await use(async (args, options = {}) => {
|
|
79
|
-
const apiHost = options.unreachableHost ? await reserveClosedPort() : `http://localhost:${(await runMockServer(options.mockDir || "main")).address().port}`;
|
|
80
|
+
const apiHost = options.unreachableHost ? await reserveClosedPort() : `http://localhost:${(await runMockServer(options.mockDir || "main", options.onRequest)).address().port}`;
|
|
80
81
|
let output = "";
|
|
81
82
|
let error = "";
|
|
82
83
|
const cp = fork(join(process.cwd(), "./dist/index.js"), [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.0",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"yaml": "^2.9.0",
|
|
73
73
|
"yargs": "17.7.2",
|
|
74
74
|
"yoctocolors": "^2.1.2",
|
|
75
|
-
"@neon/config": "1.
|
|
76
|
-
"@neon/config-runtime": "1.10.3",
|
|
75
|
+
"@neon/config-runtime": "1.10.4",
|
|
77
76
|
"@neon/realtime": "0.1.2",
|
|
78
|
-
"@neon/
|
|
77
|
+
"@neon/config": "1.9.5",
|
|
78
|
+
"@neon/sdk": "7.6.0"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
81
|
"@napi-rs/keyring": "1.3.0",
|
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
"typescript": "^5.9.0",
|
|
113
113
|
"vitest": "^3.0.9",
|
|
114
114
|
"@neon-internals/cli-core": "0.0.0",
|
|
115
|
-
"@neon-internals/env-core": "0.0.
|
|
116
|
-
"@neon/
|
|
117
|
-
"@neon/
|
|
118
|
-
"@neon/
|
|
115
|
+
"@neon-internals/env-core": "0.0.36",
|
|
116
|
+
"@neon/e2e-harness": "0.0.0",
|
|
117
|
+
"@neon/env": "1.6.6",
|
|
118
|
+
"@neon/functions": "0.11.0"
|
|
119
119
|
},
|
|
120
120
|
"publishConfig": {
|
|
121
121
|
"access": "public",
|