From f4cf308c212f3abc5b285c66b399d9987b442d89 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 9 Jun 2022 12:44:36 -0500 Subject: [PATCH 1/2] superficial change based on generator tweak --- libs/api/__generated__/Api.ts | 160 +++++++++++++++++----------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/libs/api/__generated__/Api.ts b/libs/api/__generated__/Api.ts index 0950397d03..74d61b0727 100644 --- a/libs/api/__generated__/Api.ts +++ b/libs/api/__generated__/Api.ts @@ -2621,7 +2621,7 @@ export class Api extends HttpClient { this.request({ path: `/hardware/racks`, method: 'GET', - query: query, + query, ...params, }), @@ -2645,7 +2645,7 @@ export class Api extends HttpClient { this.request({ path: `/hardware/sleds`, method: 'GET', - query: query, + query, ...params, }), @@ -2669,7 +2669,7 @@ export class Api extends HttpClient { this.request({ path: `/images`, method: 'GET', - query: query, + query, ...params, }), @@ -2678,13 +2678,13 @@ export class Api extends HttpClient { */ imagesPost: ( query: ImagesPostParams, - data: GlobalImageCreate, + body: GlobalImageCreate, params: RequestParams = {} ) => this.request({ path: `/images`, method: 'POST', - body: data, + body, ...params, }), @@ -2711,11 +2711,11 @@ export class Api extends HttpClient { ...params, }), - spoofLogin: (query: SpoofLoginParams, data: LoginParams, params: RequestParams = {}) => + spoofLogin: (query: SpoofLoginParams, body: LoginParams, params: RequestParams = {}) => this.request({ path: `/login`, method: 'POST', - body: data, + body, ...params, }), @@ -2733,7 +2733,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations`, method: 'GET', - query: query, + query, ...params, }), @@ -2742,13 +2742,13 @@ export class Api extends HttpClient { */ organizationsPost: ( query: OrganizationsPostParams, - data: OrganizationCreate, + body: OrganizationCreate, params: RequestParams = {} ) => this.request({ path: `/organizations`, method: 'POST', - body: data, + body, ...params, }), @@ -2770,13 +2770,13 @@ export class Api extends HttpClient { */ organizationsPutOrganization: ( { orgName }: OrganizationsPutOrganizationParams, - data: OrganizationUpdate, + body: OrganizationUpdate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}`, method: 'PUT', - body: data, + body, ...params, }), @@ -2811,13 +2811,13 @@ export class Api extends HttpClient { */ organizationPutPolicy: ( { orgName }: OrganizationPutPolicyParams, - data: OrganizationRolesPolicy, + body: OrganizationRolesPolicy, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/policy`, method: 'PUT', - body: data, + body, ...params, }), @@ -2831,7 +2831,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects`, method: 'GET', - query: query, + query, ...params, }), @@ -2840,13 +2840,13 @@ export class Api extends HttpClient { */ organizationProjectsPost: ( { orgName }: OrganizationProjectsPostParams, - data: ProjectCreate, + body: ProjectCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects`, method: 'POST', - body: data, + body, ...params, }), @@ -2868,13 +2868,13 @@ export class Api extends HttpClient { */ organizationProjectsPutProject: ( { orgName, projectName }: OrganizationProjectsPutProjectParams, - data: ProjectUpdate, + body: ProjectUpdate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}`, method: 'PUT', - body: data, + body, ...params, }), @@ -2901,7 +2901,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/disks`, method: 'GET', - query: query, + query, ...params, }), @@ -2910,13 +2910,13 @@ export class Api extends HttpClient { */ projectDisksPost: ( { orgName, projectName }: ProjectDisksPostParams, - data: DiskCreate, + body: DiskCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/disks`, method: 'POST', - body: data, + body, ...params, }), @@ -2956,7 +2956,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/images`, method: 'GET', - query: query, + query, ...params, }), @@ -2965,13 +2965,13 @@ export class Api extends HttpClient { */ projectImagesPost: ( { orgName, projectName }: ProjectImagesPostParams, - data: ImageCreate, + body: ImageCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/images`, method: 'POST', - body: data, + body, ...params, }), @@ -3011,7 +3011,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances`, method: 'GET', - query: query, + query, ...params, }), @@ -3020,13 +3020,13 @@ export class Api extends HttpClient { */ projectInstancesPost: ( { orgName, projectName }: ProjectInstancesPostParams, - data: InstanceCreate, + body: InstanceCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances`, method: 'POST', - body: data, + body, ...params, }), @@ -3066,31 +3066,31 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances/${instanceName}/disks`, method: 'GET', - query: query, + query, ...params, }), instanceDisksAttach: ( { instanceName, orgName, projectName }: InstanceDisksAttachParams, - data: DiskIdentifier, + body: DiskIdentifier, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances/${instanceName}/disks/attach`, method: 'POST', - body: data, + body, ...params, }), instanceDisksDetach: ( { instanceName, orgName, projectName }: InstanceDisksDetachParams, - data: DiskIdentifier, + body: DiskIdentifier, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances/${instanceName}/disks/detach`, method: 'POST', - body: data, + body, ...params, }), @@ -3099,13 +3099,13 @@ export class Api extends HttpClient { */ projectInstancesMigrateInstance: ( { instanceName, orgName, projectName }: ProjectInstancesMigrateInstanceParams, - data: InstanceMigrate, + body: InstanceMigrate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances/${instanceName}/migrate`, method: 'POST', - body: data, + body, ...params, }), @@ -3119,7 +3119,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances/${instanceName}/network-interfaces`, method: 'GET', - query: query, + query, ...params, }), @@ -3128,13 +3128,13 @@ export class Api extends HttpClient { */ instanceNetworkInterfacesPost: ( { instanceName, orgName, projectName }: InstanceNetworkInterfacesPostParams, - data: NetworkInterfaceCreate, + body: NetworkInterfaceCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/instances/${instanceName}/network-interfaces`, method: 'POST', - body: data, + body, ...params, }), @@ -3231,13 +3231,13 @@ export class Api extends HttpClient { */ organizationProjectsPutProjectPolicy: ( { orgName, projectName }: OrganizationProjectsPutProjectPolicyParams, - data: ProjectRolesPolicy, + body: ProjectRolesPolicy, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/policy`, method: 'PUT', - body: data, + body, ...params, }), @@ -3251,7 +3251,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/snapshots`, method: 'GET', - query: query, + query, ...params, }), @@ -3260,13 +3260,13 @@ export class Api extends HttpClient { */ projectSnapshotsPost: ( { orgName, projectName }: ProjectSnapshotsPostParams, - data: SnapshotCreate, + body: SnapshotCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/snapshots`, method: 'POST', - body: data, + body, ...params, }), @@ -3306,7 +3306,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs`, method: 'GET', - query: query, + query, ...params, }), @@ -3315,13 +3315,13 @@ export class Api extends HttpClient { */ projectVpcsPost: ( { orgName, projectName }: ProjectVpcsPostParams, - data: VpcCreate, + body: VpcCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs`, method: 'POST', - body: data, + body, ...params, }), @@ -3343,13 +3343,13 @@ export class Api extends HttpClient { */ projectVpcsPutVpc: ( { orgName, projectName, vpcName }: ProjectVpcsPutVpcParams, - data: VpcUpdate, + body: VpcUpdate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}`, method: 'PUT', - body: data, + body, ...params, }), @@ -3384,13 +3384,13 @@ export class Api extends HttpClient { */ vpcFirewallRulesPut: ( { orgName, projectName, vpcName }: VpcFirewallRulesPutParams, - data: VpcFirewallRuleUpdateParams, + body: VpcFirewallRuleUpdateParams, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/firewall/rules`, method: 'PUT', - body: data, + body, ...params, }), @@ -3404,7 +3404,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/routers`, method: 'GET', - query: query, + query, ...params, }), @@ -3413,13 +3413,13 @@ export class Api extends HttpClient { */ vpcRoutersPost: ( { orgName, projectName, vpcName }: VpcRoutersPostParams, - data: VpcRouterCreate, + body: VpcRouterCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/routers`, method: 'POST', - body: data, + body, ...params, }), @@ -3441,13 +3441,13 @@ export class Api extends HttpClient { */ vpcRoutersPutRouter: ( { orgName, projectName, routerName, vpcName }: VpcRoutersPutRouterParams, - data: VpcRouterUpdate, + body: VpcRouterUpdate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/routers/${routerName}`, method: 'PUT', - body: data, + body, ...params, }), @@ -3474,7 +3474,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/routers/${routerName}/routes`, method: 'GET', - query: query, + query, ...params, }), @@ -3483,13 +3483,13 @@ export class Api extends HttpClient { */ routersRoutesPost: ( { orgName, projectName, routerName, vpcName }: RoutersRoutesPostParams, - data: RouterRouteCreateParams, + body: RouterRouteCreateParams, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/routers/${routerName}/routes`, method: 'POST', - body: data, + body, ...params, }), @@ -3511,13 +3511,13 @@ export class Api extends HttpClient { */ routersRoutesPutRoute: ( { orgName, projectName, routeName, routerName, vpcName }: RoutersRoutesPutRouteParams, - data: RouterRouteUpdateParams, + body: RouterRouteUpdateParams, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/routers/${routerName}/routes/${routeName}`, method: 'PUT', - body: data, + body, ...params, }), @@ -3550,7 +3550,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/subnets`, method: 'GET', - query: query, + query, ...params, }), @@ -3559,13 +3559,13 @@ export class Api extends HttpClient { */ vpcSubnetsPost: ( { orgName, projectName, vpcName }: VpcSubnetsPostParams, - data: VpcSubnetCreate, + body: VpcSubnetCreate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/subnets`, method: 'POST', - body: data, + body, ...params, }), @@ -3587,13 +3587,13 @@ export class Api extends HttpClient { */ vpcSubnetsPutSubnet: ( { orgName, projectName, subnetName, vpcName }: VpcSubnetsPutSubnetParams, - data: VpcSubnetUpdate, + body: VpcSubnetUpdate, params: RequestParams = {} ) => this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/subnets/${subnetName}`, method: 'PUT', - body: data, + body, ...params, }), @@ -3626,7 +3626,7 @@ export class Api extends HttpClient { this.request({ path: `/organizations/${orgName}/projects/${projectName}/vpcs/${vpcName}/subnets/${subnetName}/network-interfaces`, method: 'GET', - query: query, + query, ...params, }), @@ -3645,13 +3645,13 @@ export class Api extends HttpClient { */ policyPut: ( query: PolicyPutParams, - data: FleetRolesPolicy, + body: FleetRolesPolicy, params: RequestParams = {} ) => this.request({ path: `/policy`, method: 'PUT', - body: data, + body, ...params, }), @@ -3662,7 +3662,7 @@ export class Api extends HttpClient { this.request({ path: `/roles`, method: 'GET', - query: query, + query, ...params, }), @@ -3683,7 +3683,7 @@ export class Api extends HttpClient { this.request({ path: `/sagas`, method: 'GET', - query: query, + query, ...params, }), @@ -3714,7 +3714,7 @@ export class Api extends HttpClient { this.request({ path: `/session/me/sshkeys`, method: 'GET', - query: query, + query, ...params, }), @@ -3723,13 +3723,13 @@ export class Api extends HttpClient { */ sshkeysPost: ( query: SshkeysPostParams, - data: SshKeyCreate, + body: SshKeyCreate, params: RequestParams = {} ) => this.request({ path: `/session/me/sshkeys`, method: 'POST', - body: data, + body, ...params, }), @@ -3760,18 +3760,18 @@ export class Api extends HttpClient { this.request({ path: `/silos`, method: 'GET', - query: query, + query, ...params, }), /** * Create a new silo. */ - silosPost: (query: SilosPostParams, data: SiloCreate, params: RequestParams = {}) => + silosPost: (query: SilosPostParams, body: SiloCreate, params: RequestParams = {}) => this.request({ path: `/silos`, method: 'POST', - body: data, + body, ...params, }), @@ -3813,13 +3813,13 @@ export class Api extends HttpClient { */ silosPutSiloPolicy: ( { siloName }: SilosPutSiloPolicyParams, - data: SiloRolesPolicy, + body: SiloRolesPolicy, params: RequestParams = {} ) => this.request({ path: `/silos/${siloName}/policy`, method: 'PUT', - body: data, + body, ...params, }), @@ -3830,7 +3830,7 @@ export class Api extends HttpClient { this.request({ path: `/timeseries/schema`, method: 'GET', - query: query, + query, ...params, }), @@ -3851,7 +3851,7 @@ export class Api extends HttpClient { this.request({ path: `/users`, method: 'GET', - query: query, + query, ...params, }), From 5bc15af727aa798da743f94414675fa5ea5a4a10 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 9 Jun 2022 12:46:04 -0500 Subject: [PATCH 2/2] update API spec with SAML stuff --- OMICRON_VERSION | 2 +- libs/api/__generated__/Api.ts | 260 ++++++++++++++++++++++++- libs/api/__generated__/OMICRON_VERSION | 2 +- 3 files changed, 256 insertions(+), 8 deletions(-) diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 42e639c48f..cd31bdf3f6 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -01764e05d443f803a24920e842751fb5ab38135e \ No newline at end of file +c45136595220c8566aabd77cf6f12d29b1ca5a91 \ No newline at end of file diff --git a/libs/api/__generated__/Api.ts b/libs/api/__generated__/Api.ts index 74d61b0727..12f60eae43 100644 --- a/libs/api/__generated__/Api.ts +++ b/libs/api/__generated__/Api.ts @@ -23,6 +23,17 @@ export type DatumType = | 'histogram_i64' | 'histogram_f64' +export type DerEncodedKeyPair = { + /** + * request signing private key (base64 encoded der file) + */ + privateKey: string + /** + * request signing public certificate (base64 encoded der file) + */ + publicCert: string +} + export type Digest = { type: 'sha256'; value: string } /** @@ -271,11 +282,61 @@ export type GlobalImageResultsPage = { nextPage?: string | null } +/** + * Client view of an {@link IdentityProvider} + */ +export type IdentityProvider = { + /** + * human-readable free-form text about a resource + */ + description: string + /** + * unique, immutable, system-controlled identifier for each resource + */ + id: string + /** + * unique, mutable, user-controlled identifier for each resource + */ + name: Name + /** + * Identity provider type + */ + providerType: IdentityProviderType + /** + * timestamp when this resource was created + */ + timeCreated: Date + /** + * timestamp when this resource was last modified + */ + timeModified: Date +} + +/** + * A single page of results + */ +export type IdentityProviderResultsPage = { + /** + * list of items on this page of results + */ + items: IdentityProvider[] + /** + * token used to fetch the next page of results (if any) + */ + nextPage?: string | null +} + +export type IdentityProviderType = 'saml' + /** * Describes what kind of identity is described by an id */ export type IdentityType = 'silo_user' +export type IdpMetadataSource = + | { type: 'url'; url: string } + | { data: string; type: 'base64_encoded_xml' } + /** * Client view of project Images */ @@ -465,7 +526,7 @@ export type InstanceDiskAttachment = * Migration parameters for an {@link Instance} */ export type InstanceMigrate = { - dstSledUuid: string + dstSledId: string } /** @@ -535,10 +596,6 @@ export type L4PortRange = string /** Regex pattern for validating L4PortRange */ export const l4PortRangePattern = '^[0-9]{1,5}(-[0-9]{1,5})?$' -export type LoginParams = { - username: string -} - /** * A Media Access Control address, in EUI-48 format */ @@ -1001,6 +1058,92 @@ export type SagaState = | { state: 'succeeded' } | { errorInfo: SagaErrorInfo; errorNodeName: string; state: 'failed' } +/** + * Identity-related metadata that's included in nearly all public API objects + */ +export type SamlIdentityProvider = { + /** + * service provider endpoint where the response will be sent + */ + acsUrl: string + /** + * human-readable free-form text about a resource + */ + description: string + /** + * unique, immutable, system-controlled identifier for each resource + */ + id: string + /** + * idp's entity id + */ + idpEntityId: string + /** + * unique, mutable, user-controlled identifier for each resource + */ + name: Name + /** + * optional request signing public certificate (base64 encoded der file) + */ + publicCert?: string | null + /** + * service provider endpoint where the idp should send log out requests + */ + sloUrl: string + /** + * sp's client id + */ + spClientId: string + /** + * customer's technical contact for saml configuration + */ + technicalContactEmail: string + /** + * timestamp when this resource was created + */ + timeCreated: Date + /** + * timestamp when this resource was last modified + */ + timeModified: Date +} + +/** + * Create-time identity-related parameters + */ +export type SamlIdentityProviderCreate = { + /** + * service provider endpoint where the response will be sent + */ + acsUrl: string + description: string + /** + * idp's entity id + */ + idpEntityId: string + /** + * the source of an identity provider metadata descriptor + */ + idpMetadataSource: IdpMetadataSource + name: Name + /** + * optional request signing key pair + */ + signingKeypair?: DerEncodedKeyPair | null + /** + * service provider endpoint where the idp should send log out requests + */ + sloUrl: string + /** + * sp's client id + */ + spClientId: string + /** + * customer's technical contact for saml configuration + */ + technicalContactEmail: string +} + /** * Client view of currently authed user. */ @@ -1182,6 +1325,10 @@ export type SnapshotResultsPage = { nextPage?: string | null } +export type SpoofLoginBody = { + username: string +} + /** * Client view of a {@link SshKey} */ @@ -1758,6 +1905,18 @@ export interface ImagesDeleteImageParams { export interface SpoofLoginParams {} +export interface LoginParams { + providerName: Name + + siloName: Name +} + +export interface ConsumeCredentialsParams { + providerName: Name + + siloName: Name +} + export interface LogoutParams {} export interface OrganizationsGetParams { @@ -2378,6 +2537,16 @@ export interface SilosDeleteSiloParams { siloName: Name } +export interface SilosGetIdentityProvidersParams { + siloName: Name + + limit?: number | null + + pageToken?: string | null + + sortBy?: NameSortMode +} + export interface SilosGetSiloPolicyParams { siloName: Name } @@ -2386,6 +2555,16 @@ export interface SilosPutSiloPolicyParams { siloName: Name } +export interface SiloSamlIdpCreateParams { + siloName: Name +} + +export interface SiloSamlIdpFetchParams { + providerName: Name + + siloName: Name +} + export interface TimeseriesSchemaGetParams { limit?: number | null @@ -2711,7 +2890,11 @@ export class Api extends HttpClient { ...params, }), - spoofLogin: (query: SpoofLoginParams, body: LoginParams, params: RequestParams = {}) => + spoofLogin: ( + query: SpoofLoginParams, + body: SpoofLoginBody, + params: RequestParams = {} + ) => this.request({ path: `/login`, method: 'POST', @@ -2719,6 +2902,29 @@ export class Api extends HttpClient { ...params, }), + /** + * Ask the user to login to their identity provider + */ + login: ({ providerName, siloName }: LoginParams, params: RequestParams = {}) => + this.request({ + path: `/login/${siloName}/${providerName}`, + method: 'GET', + ...params, + }), + + /** + * Consume some sort of credentials, and authenticate a user. + */ + consumeCredentials: ( + { providerName, siloName }: ConsumeCredentialsParams, + params: RequestParams = {} + ) => + this.request({ + path: `/login/${siloName}/${providerName}`, + method: 'POST', + ...params, + }), + logout: (query: LogoutParams, params: RequestParams = {}) => this.request({ path: `/logout`, @@ -3795,6 +4001,20 @@ export class Api extends HttpClient { ...params, }), + /** + * List Silo identity providers + */ + silosGetIdentityProviders: ( + { siloName, ...query }: SilosGetIdentityProvidersParams, + params: RequestParams = {} + ) => + this.request({ + path: `/silos/${siloName}/identity_providers`, + method: 'GET', + query, + ...params, + }), + /** * Fetch the IAM policy for this Silo */ @@ -3823,6 +4043,34 @@ export class Api extends HttpClient { ...params, }), + /** + * Create a new SAML identity provider for a silo. + */ + siloSamlIdpCreate: ( + { siloName }: SiloSamlIdpCreateParams, + body: SamlIdentityProviderCreate, + params: RequestParams = {} + ) => + this.request({ + path: `/silos/${siloName}/saml_identity_providers`, + method: 'POST', + body, + ...params, + }), + + /** + * GET a silo's SAML identity provider + */ + siloSamlIdpFetch: ( + { providerName, siloName }: SiloSamlIdpFetchParams, + params: RequestParams = {} + ) => + this.request({ + path: `/silos/${siloName}/saml_identity_providers/${providerName}`, + method: 'GET', + ...params, + }), + /** * List all timeseries schema */ diff --git a/libs/api/__generated__/OMICRON_VERSION b/libs/api/__generated__/OMICRON_VERSION index c8787a297d..45f25d83d7 100644 --- a/libs/api/__generated__/OMICRON_VERSION +++ b/libs/api/__generated__/OMICRON_VERSION @@ -1,2 +1,2 @@ # generated file. do not update manually. see docs/update-pinned-api.md -01764e05d443f803a24920e842751fb5ab38135e +c45136595220c8566aabd77cf6f12d29b1ca5a91