Skip to content

Commit

Permalink
feat: add webhook signing secrets to legacy and plain client interfac…
Browse files Browse the repository at this point in the history
…es [EXT-4817] (#2029)

* feat: add webhook signing secrets plain client interface

* feat: add plain client methods for webhook signing secrets

* test: add unit tests for webhook signing secrets

* docs: add new types to exported types for docs

* refactor: make upsertSigningSecret payload required

* feat: add webhook signing secrets to legacy client interface

* fix: docs typos
  • Loading branch information
t-col committed Nov 7, 2023
1 parent 1dca868 commit 7d40528
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 2 deletions.
37 changes: 36 additions & 1 deletion lib/adapters/REST/endpoints/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
GetWebhookParams,
QueryParams,
} from '../../../common-types'
import { CreateWebhooksProps, WebhookProps } from '../../../entities/webhook'
import {
CreateWebhooksProps,
UpsertWebhookSigningSecretPayload,
WebhookProps,
WebhookSigningSecretProps,
} from '../../../entities/webhook'
import { RestEndpoint } from '../types'
import * as raw from './raw'
import { normalizeSelect } from './utils'
Expand All @@ -29,6 +34,12 @@ const getWebhookCallDetailsUrl = (params: GetWebhookCallDetailsUrl) =>
const getWebhookHealthUrl = (params: GetWebhookParams) =>
`${getWebhookCallBaseUrl(params)}/${params.webhookDefinitionId}/health`

const getWebhookSettingsUrl = (params: GetSpaceParams) =>
`/spaces/${params.spaceId}/webhook_settings`

const getWebhookSigningSecretUrl = (params: GetSpaceParams) =>
`${getWebhookSettingsUrl(params)}/signing_secret`

export const get: RestEndpoint<'Webhook', 'get'> = (
http: AxiosInstance,
params: GetWebhookParams
Expand Down Expand Up @@ -68,6 +79,13 @@ export const getMany: RestEndpoint<'Webhook', 'getMany'> = (
})
}

export const getSigningSecret: RestEndpoint<'Webhook', 'getSigningSecret'> = (
http: AxiosInstance,
params: GetSpaceParams
) => {
return raw.get(http, getWebhookSigningSecretUrl(params))
}

export const create: RestEndpoint<'Webhook', 'create'> = (
http: AxiosInstance,
params: GetSpaceParams,
Expand Down Expand Up @@ -108,9 +126,26 @@ export const update: RestEndpoint<'Webhook', 'update'> = async (
})
}

export const upsertSigningSecret: RestEndpoint<'Webhook', 'upsertSigningSecret'> = async (
http: AxiosInstance,
params: GetSpaceParams,
rawData: UpsertWebhookSigningSecretPayload
) => {
const data = copy(rawData)

return raw.put<WebhookSigningSecretProps>(http, getWebhookSigningSecretUrl(params), data)
}

export const del: RestEndpoint<'Webhook', 'delete'> = (
http: AxiosInstance,
params: GetWebhookParams
) => {
return raw.del(http, getWebhookUrl(params))
}

export const deleteSigningSecret: RestEndpoint<'Webhook', 'deleteSigningSecret'> = async (
http: AxiosInstance,
params: GetSpaceParams
) => {
return raw.del<void>(http, getWebhookSigningSecretUrl(params))
}
12 changes: 12 additions & 0 deletions lib/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ import { UsageProps } from './entities/usage'
import { UserProps } from './entities/user'
import {
CreateWebhooksProps,
UpsertWebhookSigningSecretPayload,
WebhookCallDetailsProps,
WebhookCallOverviewProps,
WebhookHealthProps,
WebhookProps,
WebhookSigningSecretProps,
} from './entities/webhook'
import { AssetKeyProps, CreateAssetKeyProps } from './entities/asset-key'
import { AppUploadProps } from './entities/app-upload'
Expand Down Expand Up @@ -660,10 +662,13 @@ type MRInternal<UA extends boolean> = {
(opts: MROpts<'Webhook', 'getCallDetails', UA>): MRReturn<'Webhook', 'getCallDetails'>
(opts: MROpts<'Webhook', 'getHealthStatus', UA>): MRReturn<'Webhook', 'getHealthStatus'>
(opts: MROpts<'Webhook', 'getManyCallDetails', UA>): MRReturn<'Webhook', 'getManyCallDetails'>
(opts: MROpts<'Webhook', 'getSigningSecret', UA>): MRReturn<'Webhook', 'getSigningSecret'>
(opts: MROpts<'Webhook', 'create', UA>): MRReturn<'Webhook', 'create'>
(opts: MROpts<'Webhook', 'createWithId', UA>): MRReturn<'Webhook', 'createWithId'>
(opts: MROpts<'Webhook', 'update', UA>): MRReturn<'Webhook', 'update'>
(opts: MROpts<'Webhook', 'upsertSigningSecret', UA>): MRReturn<'Webhook', 'upsertSigningSecret'>
(opts: MROpts<'Webhook', 'delete', UA>): MRReturn<'Webhook', 'delete'>
(opts: MROpts<'Webhook', 'deleteSigningSecret', UA>): MRReturn<'Webhook', 'deleteSigningSecret'>

(opts: MROpts<'WorkflowDefinition', 'get', UA>): MRReturn<'WorkflowDefinition', 'get'>
(opts: MROpts<'WorkflowDefinition', 'getMany', UA>): MRReturn<'WorkflowDefinition', 'getMany'>
Expand Down Expand Up @@ -1704,6 +1709,7 @@ export type MRActions = {
params: GetWebhookParams & QueryParams
return: CollectionProp<WebhookCallOverviewProps>
}
getSigningSecret: { params: GetSpaceParams; return: WebhookSigningSecretProps }
create: {
params: GetSpaceParams
payload: CreateWebhooksProps
Expand All @@ -1717,7 +1723,13 @@ export type MRActions = {
return: WebhookProps
}
update: { params: GetWebhookParams; payload: WebhookProps; return: WebhookProps }
upsertSigningSecret: {
params: GetSpaceParams
payload: UpsertWebhookSigningSecretPayload
return: WebhookSigningSecretProps
}
delete: { params: GetWebhookParams; return: void }
deleteSigningSecret: { params: GetSpaceParams; return: void }
}
WorkflowDefinition: {
get: {
Expand Down
82 changes: 81 additions & 1 deletion lib/create-space-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ScheduledActionProps, ScheduledActionQueryOptions } from './entities/sc
import { SpaceProps } from './entities/space'
import { CreateSpaceMembershipProps } from './entities/space-membership'
import { CreateTeamSpaceMembershipProps } from './entities/team-space-membership'
import { CreateWebhooksProps } from './entities/webhook'
import { CreateWebhooksProps, UpsertWebhookSigningSecretPayload } from './entities/webhook'

/**
* @private
Expand Down Expand Up @@ -266,6 +266,31 @@ export default function createSpaceApi(makeRequest: MakeRequest) {
}).then((data) => wrapWebhookCollection(makeRequest, data))
},

/**
* Fetch a webhook signing secret
* @returns Promise for the redacted webhook signing secret in this space
* @example ```javascript
* const contentful = require('contentful-management')
*
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
*
* client.getSpace('<space_id>')
* .then((space) => space.getWebhookSigningSecret())
* .then((response) => console.log(response.redactedValue))
* .catch(console.error)
* ```
*/
getWebhookSigningSecret: function getSigningSecret() {
const raw = this.toPlainObject() as SpaceProps
return makeRequest({
entityType: 'Webhook',
action: 'getSigningSecret',
params: { spaceId: raw.sys.id },
})
},

/**
* Creates a Webhook
* @param data - Object representation of the Webhook to be created
Expand Down Expand Up @@ -330,6 +355,61 @@ export default function createSpaceApi(makeRequest: MakeRequest) {
payload: data,
}).then((data) => wrapWebhook(makeRequest, data))
},

/**
* Create or update the webhook signing secret for this space
* @param data 64 character string that will be used to sign the webhook calls
* @returns Promise for the redacted webhook signing secret that was created or updated
* @example ```javascript
* const contentful = require('contentful-management')
* const crypto = require('crypto')
*
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
*
* const signingSecret = client.getSpace('<space_id>')
* .then((space) => space.upsertWebhookSigningSecret({
* value: crypto.randomBytes(32).toString('hex')
* }))
* .then((response) => console.log(response.redactedValue))
* .catch(console.error)
* ```
*/
upsertWebhookSigningSecret: function getSigningSecret(data: UpsertWebhookSigningSecretPayload) {
const raw = this.toPlainObject() as SpaceProps
return makeRequest({
entityType: 'Webhook',
action: 'upsertSigningSecret',
params: { spaceId: raw.sys.id },
payload: data,
})
},

/**
* Delete the webhook signing secret for this space
* @returns Promise<void>
* @example ```javascript
* const contentful = require('contentful-management')
*
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
*
* client.getSpace('<space_id>')
* .then((space) => space.deleteWebhookSigningSecret())
* .then(() => console.log("success"))
* .catch(console.error)
* ```
*/
deleteWebhookSigningSecret: function getSigningSecret() {
const raw = this.toPlainObject() as SpaceProps
return makeRequest({
entityType: 'Webhook',
action: 'deleteSigningSecret',
params: { spaceId: raw.sys.id },
})
},
/**
* Gets a Role
* @param id - Role ID
Expand Down
11 changes: 11 additions & 0 deletions lib/entities/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export type UpdateWebhookProps = SetOptional<
'headers' | 'name' | 'topics' | 'url' | 'active'
>

export type UpsertWebhookSigningSecretPayload = {
value: string
}

export type WebhookCallDetailsProps = {
/**
* System metadata
Expand Down Expand Up @@ -137,6 +141,13 @@ export type WebhookHealthProps = {
calls: WebhookCalls
}

export type WebhookSigningSecretSys = Except<BasicMetaSysProps, 'version'>

export type WebhookSigningSecretProps = {
sys: WebhookSigningSecretSys & { space: { sys: MetaLinkProps } }
redactedValue: string
}

export type WebhookProps = {
/**
* System metadata
Expand Down
2 changes: 2 additions & 0 deletions lib/export-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export type {
WebhookProps,
WebHooks,
WebhookTransformation,
UpsertWebhookSigningSecretPayload,
WebhookSigningSecretProps,
} from './entities/webhook'
export type {
// General typings (props, params, options)
Expand Down
48 changes: 48 additions & 0 deletions lib/plain/entities/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
} from '../../common-types'
import {
CreateWebhooksProps,
UpsertWebhookSigningSecretPayload,
WebhookCallDetailsProps,
WebhookCallOverviewProps,
WebhookHealthProps,
WebhookProps,
WebhookSigningSecretProps,
} from '../../entities/webhook'
import { OptionalDefaults } from '../wrappers/wrap'

Expand Down Expand Up @@ -92,6 +94,18 @@ export type WebhookPlainClientAPI = {
getManyCallDetails(
params: OptionalDefaults<GetWebhookParams & QueryParams>
): Promise<CollectionProp<WebhookCallOverviewProps>>
/**
* Fetch the redacted webhook signing secret for a given Space
* @param params entity IDs to identify the Space which the signing secret belongs to
* @returns the last 4 characters of the redacted signing secret
* @throws if the request fails, the Space is not found, or the signing secret does not exist
* @example
* ```javascript
* const signingSecret = await client.webhook.getSigningSecret({
* spaceId: '<space_id>',
* });
*/
getSigningSecret(params: OptionalDefaults<GetSpaceParams>): Promise<WebhookSigningSecretProps>
/**
* Creates a Webhook
* @param params entity IDs to identify the Space to create the Webhook in
Expand All @@ -117,6 +131,28 @@ export type WebhookPlainClientAPI = {
rawData: CreateWebhooksProps,
headers?: RawAxiosRequestHeaders
): Promise<WebhookProps>
/**
* Creates or updates the webhook signing secret for a given Space
* @param params entity IDs to identify the Space which the signing secret belongs to
* @param rawData (optional) the updated 64 character signing secret value if the secret already exists
* @returns the last 4 characters of the created or updated signing secret
* @throws if the request fails, the Space is not found, or the payload is malformed
* @example
* ```javascript
* const crypto = require('crypto')
*
* const signingSecret = await client.webhook.upsertSigningSecret({
* spaceId: '<space_id>',
* },
* {
* value: crypto.randomBytes(32).toString("hex"),
* });
* ```
*/
upsertSigningSecret(
params: OptionalDefaults<GetSpaceParams>,
rawData: UpsertWebhookSigningSecretPayload
): Promise<WebhookSigningSecretProps>
/**
* Creates the Webhook
* @param params entity IDs to identify the Webhook to update
Expand Down Expand Up @@ -154,4 +190,16 @@ export type WebhookPlainClientAPI = {
* ```
*/
delete(params: OptionalDefaults<GetWebhookParams>): Promise<any>
/**
* Removes the webhook signing secret for a given Space
* @param params entity IDs to identify the Space which the signing secret belongs to
* @throws if the request fails, the Space is not found, or the signing secret does not exist
* @example
* ```javascript
* await client.webhook.deleteSigningSecret({
* spaceId: '<space_id>',
* });
* ```
*/
deleteSigningSecret(params: OptionalDefaults<GetSpaceParams>): Promise<void>
}
3 changes: 3 additions & 0 deletions lib/plain/plain-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,10 +361,13 @@ export const createPlainClient = (
getMany: wrap(wrapParams, 'Webhook', 'getMany'),
getHealthStatus: wrap(wrapParams, 'Webhook', 'getHealthStatus'),
getCallDetails: wrap(wrapParams, 'Webhook', 'getCallDetails'),
getSigningSecret: wrap(wrapParams, 'Webhook', 'getSigningSecret'),
getManyCallDetails: wrap(wrapParams, 'Webhook', 'getManyCallDetails'),
create: wrap(wrapParams, 'Webhook', 'create'),
update: wrap(wrapParams, 'Webhook', 'update'),
upsertSigningSecret: wrap(wrapParams, 'Webhook', 'upsertSigningSecret'),
delete: wrap(wrapParams, 'Webhook', 'delete'),
deleteSigningSecret: wrap(wrapParams, 'Webhook', 'deleteSigningSecret'),
},
snapshot: {
getManyForEntry: wrap(wrapParams, 'Snapshot', 'getManyForEntry'),
Expand Down
50 changes: 50 additions & 0 deletions test/unit/plain/webhook-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect } from 'chai'
import { describe, test } from 'mocha'
import sinon from 'sinon'
import { createClient } from '../../../lib/contentful-management'
import setupRestAdapter from '../adapters/REST/helpers/setupRestAdapter'
import crypto from 'crypto'

describe('Webhook', () => {
const spaceId = 'space-id'

test('getSigningSecret', async () => {
const { httpMock, adapterMock } = setupRestAdapter(
Promise.resolve({ data: { redactedValue: 'abcd' } })
)
const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' })
const response = await plainClient.webhook.getSigningSecret({ spaceId })

expect(response).to.be.an('object')
expect(response.redactedValue).to.equal('abcd')

sinon.assert.calledWith(httpMock.get, `/spaces/space-id/webhook_settings/signing_secret`)
})

test('upsertSigningSecret', async () => {
const { httpMock, adapterMock } = setupRestAdapter(
Promise.resolve({ data: { redactedValue: 'abcd' } })
)
const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' })

const payload = { value: crypto.randomBytes(32).toString('hex') }
const response = await plainClient.webhook.upsertSigningSecret({ spaceId }, payload)

expect(response).to.be.an('object')
expect(response.redactedValue).to.equal('abcd')

sinon.assert.calledWith(
httpMock.put,
`/spaces/space-id/webhook_settings/signing_secret`,
payload
)
})

test('deleteSigningSecret', async () => {
const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: '' }))
const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' })
await plainClient.webhook.deleteSigningSecret({ spaceId })

sinon.assert.calledWith(httpMock.delete, `/spaces/space-id/webhook_settings/signing_secret`)
})
})

0 comments on commit 7d40528

Please sign in to comment.