@superblocksteam/shared 0.9615.3 → 0.9616.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.
- package/dist/database-lifecycle/appDatabaseImport.d.ts +12 -4
- package/dist/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
- package/dist/database-lifecycle/appDatabaseImport.js +21 -3
- package/dist/database-lifecycle/appDatabaseImport.js.map +1 -1
- package/dist/database-lifecycle/appDatabaseImport.test.js +17 -0
- package/dist/database-lifecycle/appDatabaseImport.test.js.map +1 -1
- package/dist/database-lifecycle/index.d.ts +4 -2
- package/dist/database-lifecycle/index.d.ts.map +1 -1
- package/dist/database-lifecycle/index.js +6 -2
- package/dist/database-lifecycle/index.js.map +1 -1
- package/dist/database-lifecycle/index.test.js +1 -0
- package/dist/database-lifecycle/index.test.js.map +1 -1
- package/dist/plugins/templates/shared/auth.d.ts +17 -1
- package/dist/plugins/templates/shared/auth.d.ts.map +1 -1
- package/dist/plugins/templates/shared/auth.js +139 -2
- package/dist/plugins/templates/shared/auth.js.map +1 -1
- package/dist/plugins/templates/shared/auth.test.js +86 -0
- package/dist/plugins/templates/shared/auth.test.js.map +1 -1
- package/dist/plugins/templates/shared/openapi.d.ts +6 -1
- package/dist/plugins/templates/shared/openapi.d.ts.map +1 -1
- package/dist/plugins/templates/shared/openapi.js +7 -3
- package/dist/plugins/templates/shared/openapi.js.map +1 -1
- package/dist/plugins/templates/shared/restapiintegration.d.ts +1 -0
- package/dist/plugins/templates/shared/restapiintegration.d.ts.map +1 -1
- package/dist/plugins/templates/shared/restapiintegration.js +3 -1
- package/dist/plugins/templates/shared/restapiintegration.js.map +1 -1
- package/dist/plugins/templates/zendesk.d.ts +1 -0
- package/dist/plugins/templates/zendesk.d.ts.map +1 -1
- package/dist/plugins/templates/zendesk.js +41 -3
- package/dist/plugins/templates/zendesk.js.map +1 -1
- package/dist/plugins/templates/zendesk.test.d.ts +2 -0
- package/dist/plugins/templates/zendesk.test.d.ts.map +1 -0
- package/dist/plugins/templates/zendesk.test.js +79 -0
- package/dist/plugins/templates/zendesk.test.js.map +1 -0
- package/dist/socket/protocol.d.ts +23 -0
- package/dist/socket/protocol.d.ts.map +1 -1
- package/dist/socket/socketWithClientAuth.d.ts +3 -0
- package/dist/socket/socketWithClientAuth.d.ts.map +1 -1
- package/dist/socket/socketWithClientAuth.js +12 -1
- package/dist/socket/socketWithClientAuth.js.map +1 -1
- package/dist/socket/socketWithClientAuth.test.d.ts +2 -0
- package/dist/socket/socketWithClientAuth.test.d.ts.map +1 -0
- package/dist/socket/socketWithClientAuth.test.js +106 -0
- package/dist/socket/socketWithClientAuth.test.js.map +1 -0
- package/dist/types/datasource/auth.d.ts +2 -0
- package/dist/types/datasource/auth.d.ts.map +1 -1
- package/dist/types/datasource/auth.js.map +1 -1
- package/dist/types/datasource/index.d.ts +15 -0
- package/dist/types/datasource/index.d.ts.map +1 -1
- package/dist/types/datasource/index.js +31 -0
- package/dist/types/datasource/index.js.map +1 -1
- package/dist/types/datasource/index.test.js +20 -0
- package/dist/types/datasource/index.test.js.map +1 -1
- package/dist/types/deployment/index.d.ts +2 -0
- package/dist/types/deployment/index.d.ts.map +1 -1
- package/dist/types/event/index.d.ts +3 -0
- package/dist/types/event/index.d.ts.map +1 -1
- package/dist/types/event/index.js +3 -0
- package/dist/types/event/index.js.map +1 -1
- package/dist/types/observability/index.d.ts +1 -1
- package/dist/types/observability/index.d.ts.map +1 -1
- package/dist/types/observability/index.js.map +1 -1
- package/dist/types/plugin/form.d.ts +31 -0
- package/dist/types/plugin/form.d.ts.map +1 -1
- package/dist/types/plugin/form.js +39 -1
- package/dist/types/plugin/form.js.map +1 -1
- package/dist/types/plugin/form.test.js +24 -0
- package/dist/types/plugin/form.test.js.map +1 -1
- package/dist/types/plugin/integration.d.ts +32 -0
- package/dist/types/plugin/integration.d.ts.map +1 -1
- package/dist/types/plugin/integration.js +58 -1
- package/dist/types/plugin/integration.js.map +1 -1
- package/dist/types/plugin/integration.test.d.ts +2 -0
- package/dist/types/plugin/integration.test.d.ts.map +1 -0
- package/dist/types/plugin/integration.test.js +39 -0
- package/dist/types/plugin/integration.test.js.map +1 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts +12 -4
- package/dist-esm/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseImport.js +21 -3
- package/dist-esm/database-lifecycle/appDatabaseImport.js.map +1 -1
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js +17 -0
- package/dist-esm/database-lifecycle/appDatabaseImport.test.js.map +1 -1
- package/dist-esm/database-lifecycle/index.d.ts +4 -2
- package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/index.js +5 -1
- package/dist-esm/database-lifecycle/index.js.map +1 -1
- package/dist-esm/database-lifecycle/index.test.js +1 -0
- package/dist-esm/database-lifecycle/index.test.js.map +1 -1
- package/dist-esm/plugins/templates/shared/auth.d.ts +17 -1
- package/dist-esm/plugins/templates/shared/auth.d.ts.map +1 -1
- package/dist-esm/plugins/templates/shared/auth.js +140 -3
- package/dist-esm/plugins/templates/shared/auth.js.map +1 -1
- package/dist-esm/plugins/templates/shared/auth.test.js +87 -1
- package/dist-esm/plugins/templates/shared/auth.test.js.map +1 -1
- package/dist-esm/plugins/templates/shared/openapi.d.ts +6 -1
- package/dist-esm/plugins/templates/shared/openapi.d.ts.map +1 -1
- package/dist-esm/plugins/templates/shared/openapi.js +7 -3
- package/dist-esm/plugins/templates/shared/openapi.js.map +1 -1
- package/dist-esm/plugins/templates/shared/restapiintegration.d.ts +1 -0
- package/dist-esm/plugins/templates/shared/restapiintegration.d.ts.map +1 -1
- package/dist-esm/plugins/templates/shared/restapiintegration.js +3 -1
- package/dist-esm/plugins/templates/shared/restapiintegration.js.map +1 -1
- package/dist-esm/plugins/templates/zendesk.d.ts +1 -0
- package/dist-esm/plugins/templates/zendesk.d.ts.map +1 -1
- package/dist-esm/plugins/templates/zendesk.js +42 -4
- package/dist-esm/plugins/templates/zendesk.js.map +1 -1
- package/dist-esm/plugins/templates/zendesk.test.d.ts +2 -0
- package/dist-esm/plugins/templates/zendesk.test.d.ts.map +1 -0
- package/dist-esm/plugins/templates/zendesk.test.js +77 -0
- package/dist-esm/plugins/templates/zendesk.test.js.map +1 -0
- package/dist-esm/socket/protocol.d.ts +23 -0
- package/dist-esm/socket/protocol.d.ts.map +1 -1
- package/dist-esm/socket/socketWithClientAuth.d.ts +3 -0
- package/dist-esm/socket/socketWithClientAuth.d.ts.map +1 -1
- package/dist-esm/socket/socketWithClientAuth.js +12 -1
- package/dist-esm/socket/socketWithClientAuth.js.map +1 -1
- package/dist-esm/socket/socketWithClientAuth.test.d.ts +2 -0
- package/dist-esm/socket/socketWithClientAuth.test.d.ts.map +1 -0
- package/dist-esm/socket/socketWithClientAuth.test.js +101 -0
- package/dist-esm/socket/socketWithClientAuth.test.js.map +1 -0
- package/dist-esm/types/datasource/auth.d.ts +2 -0
- package/dist-esm/types/datasource/auth.d.ts.map +1 -1
- package/dist-esm/types/datasource/auth.js.map +1 -1
- package/dist-esm/types/datasource/index.d.ts +15 -0
- package/dist-esm/types/datasource/index.d.ts.map +1 -1
- package/dist-esm/types/datasource/index.js +29 -0
- package/dist-esm/types/datasource/index.js.map +1 -1
- package/dist-esm/types/datasource/index.test.js +21 -1
- package/dist-esm/types/datasource/index.test.js.map +1 -1
- package/dist-esm/types/deployment/index.d.ts +2 -0
- package/dist-esm/types/deployment/index.d.ts.map +1 -1
- package/dist-esm/types/event/index.d.ts +3 -0
- package/dist-esm/types/event/index.d.ts.map +1 -1
- package/dist-esm/types/event/index.js +3 -0
- package/dist-esm/types/event/index.js.map +1 -1
- package/dist-esm/types/observability/index.d.ts +1 -1
- package/dist-esm/types/observability/index.d.ts.map +1 -1
- package/dist-esm/types/observability/index.js.map +1 -1
- package/dist-esm/types/plugin/form.d.ts +31 -0
- package/dist-esm/types/plugin/form.d.ts.map +1 -1
- package/dist-esm/types/plugin/form.js +36 -0
- package/dist-esm/types/plugin/form.js.map +1 -1
- package/dist-esm/types/plugin/form.test.js +25 -1
- package/dist-esm/types/plugin/form.test.js.map +1 -1
- package/dist-esm/types/plugin/integration.d.ts +32 -0
- package/dist-esm/types/plugin/integration.d.ts.map +1 -1
- package/dist-esm/types/plugin/integration.js +54 -0
- package/dist-esm/types/plugin/integration.js.map +1 -1
- package/dist-esm/types/plugin/integration.test.d.ts +2 -0
- package/dist-esm/types/plugin/integration.test.d.ts.map +1 -0
- package/dist-esm/types/plugin/integration.test.js +37 -0
- package/dist-esm/types/plugin/integration.test.js.map +1 -0
- package/package.json +1 -1
- package/src/database-lifecycle/appDatabaseImport.test.ts +21 -0
- package/src/database-lifecycle/appDatabaseImport.ts +37 -4
- package/src/database-lifecycle/index.test.ts +1 -0
- package/src/database-lifecycle/index.ts +7 -1
- package/src/plugins/templates/shared/auth.test.ts +119 -1
- package/src/plugins/templates/shared/auth.ts +165 -2
- package/src/plugins/templates/shared/openapi.ts +15 -3
- package/src/plugins/templates/shared/restapiintegration.ts +3 -1
- package/src/plugins/templates/zendesk.test.ts +98 -0
- package/src/plugins/templates/zendesk.ts +44 -4
- package/src/socket/protocol.ts +19 -0
- package/src/socket/socketWithClientAuth.test.ts +117 -0
- package/src/socket/socketWithClientAuth.ts +15 -1
- package/src/types/datasource/auth.ts +4 -0
- package/src/types/datasource/index.test.ts +24 -1
- package/src/types/datasource/index.ts +31 -0
- package/src/types/deployment/index.ts +2 -0
- package/src/types/event/index.ts +3 -0
- package/src/types/observability/index.ts +2 -1
- package/src/types/plugin/form.test.ts +32 -1
- package/src/types/plugin/form.ts +54 -0
- package/src/types/plugin/integration.test.ts +45 -0
- package/src/types/plugin/integration.ts +62 -0
|
@@ -44,6 +44,7 @@ export const LIFECYCLE_OPERATIONS = [
|
|
|
44
44
|
'complete_import_upload',
|
|
45
45
|
'prepare_import_prefix_upload',
|
|
46
46
|
'inspect_import_prefix',
|
|
47
|
+
'restore_import_data',
|
|
47
48
|
'describe_schema'
|
|
48
49
|
] as const;
|
|
49
50
|
export const GENERIC_IMPORT_OPERATIONS = ['prepare_import_upload', 'complete_import_upload'] as const;
|
|
@@ -53,7 +54,10 @@ export const GENERIC_IMPORT_OPERATIONS = ['prepare_import_upload', 'complete_imp
|
|
|
53
54
|
export const POOL_SCOPED_LIFECYCLE_OPERATIONS = ['ensure_physical_database_instance', 'retire_physical_database_instance'] as const;
|
|
54
55
|
export const DATABASE_LIFECYCLE_MANAGED_BY = 'database_lifecycle';
|
|
55
56
|
export const NATIVE_DB_AUTH_DESCRIPTOR_VERSION = 3 as const;
|
|
56
|
-
|
|
57
|
+
// Cloud-prem connector roles are named superblocks-app-db-connector-<agent>
|
|
58
|
+
// (terraform-aws-superblocks modules/app-db-prereqs). User-supplied integrations
|
|
59
|
+
// may not use that prefix.
|
|
60
|
+
export const APP_DB_CONNECTOR_ROLE_NAME_PREFIX = 'superblocks-app-db-connector';
|
|
57
61
|
export const NATIVE_DB_IAM_AUTH_MODE = 'aws_iam_role' as const;
|
|
58
62
|
export const NATIVE_DB_IDENTIFIER_HASH_DOMAIN = 'superblocks-native-db:v1';
|
|
59
63
|
export const DATABASE_LIFECYCLE_CAPABILITY_MANAGED_IAM_V1 = 'managed-IAM-v1';
|
|
@@ -224,6 +228,8 @@ export type LifecycleImportPayload = {
|
|
|
224
228
|
kind?: LifecycleImportKind;
|
|
225
229
|
objectKey?: string;
|
|
226
230
|
parts?: LifecycleImportPart[];
|
|
231
|
+
prefixChecksumSha256?: string;
|
|
232
|
+
prefixSizeBytes?: number;
|
|
227
233
|
partSizeBytes?: number;
|
|
228
234
|
sha256?: string;
|
|
229
235
|
sizeBytes?: number;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getRowItemsFromSectionItem,
|
|
9
9
|
IntegrationAuthType
|
|
10
10
|
} from '../../../types/index.js';
|
|
11
|
-
import { authSections } from './auth.js';
|
|
11
|
+
import { authSections, ClientAuthMethod } from './auth.js';
|
|
12
12
|
|
|
13
13
|
const buildAuthFields = (pkceStartVersion?: string, confidentialPkceStartVersion?: string): FormItem[] =>
|
|
14
14
|
authSections({
|
|
@@ -127,6 +127,25 @@ describe('PKCE dropdown option', () => {
|
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
describe('client credentials client authentication', () => {
|
|
131
|
+
test('offers a Basic-vs-body dropdown for the client credentials grant, defaulting to the Basic header', () => {
|
|
132
|
+
const clientAuthMethod = buildAuthFields().find(
|
|
133
|
+
(field) =>
|
|
134
|
+
field.name === 'authConfig.clientAuthMethod' && field.display?.show?.authType?.includes(IntegrationAuthType.OAUTH2_CLIENT_CREDS)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(clientAuthMethod?.componentType).toBe(FormComponentType.DROPDOWN);
|
|
138
|
+
// Existing client credentials integrations authenticate with the Basic
|
|
139
|
+
// header, so the default must stay Basic to leave them unchanged.
|
|
140
|
+
expect(clientAuthMethod?.initialValue).toBe(ClientAuthMethod.BASIC);
|
|
141
|
+
// The client credentials grant has no PKCE variant, so the dropdown must
|
|
142
|
+
// not leak into the authorization code sections.
|
|
143
|
+
expect(clientAuthMethod?.display?.show?.authType).toEqual([IntegrationAuthType.OAUTH2_CLIENT_CREDS]);
|
|
144
|
+
const options = clientAuthMethod && 'options' in clientAuthMethod ? clientAuthMethod.options : [];
|
|
145
|
+
expect(options.map((option) => option.value)).toEqual(expect.arrayContaining([ClientAuthMethod.BASIC, ClientAuthMethod.POST]));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
130
149
|
describe('OAuth connect button', () => {
|
|
131
150
|
test('sends the PKCE marker along with the rest of the auth config', () => {
|
|
132
151
|
// No shipped plugin has both the connect button and PKCE today: the button
|
|
@@ -160,3 +179,102 @@ describe('OAuth connect button', () => {
|
|
|
160
179
|
expect(forwardedValues).not.toContain('authConfig.usePkceClientSecret');
|
|
161
180
|
});
|
|
162
181
|
});
|
|
182
|
+
|
|
183
|
+
describe('OAuth client credentials method', () => {
|
|
184
|
+
const buildFields = (enabledMethods: Parameters<typeof authSections>[0]['enabledMethods']): FormItem[] =>
|
|
185
|
+
authSections({
|
|
186
|
+
startVersion: '0.0.1',
|
|
187
|
+
pluginId: ExtendedIntegrationPluginId.REST_API,
|
|
188
|
+
pluginName: 'Provider',
|
|
189
|
+
enabledMethods
|
|
190
|
+
}).flatMap((section) => section.items.flatMap((item) => getRowItemsFromSectionItem(item)));
|
|
191
|
+
|
|
192
|
+
test('keeps the token URL template-owned and offers no Connect', () => {
|
|
193
|
+
// The orchestrator posts the client secret to whatever tokenUrl is stored, so the value has
|
|
194
|
+
// to come from the template rather than the form, and there is no user to send anywhere.
|
|
195
|
+
const fields = buildFields({
|
|
196
|
+
oauth2ClientCredentials: { tokenUrl: 'https://provider.example.com/token', startVersion: '0.0.18' }
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(fields).toContainEqual(
|
|
200
|
+
expect.objectContaining({ name: 'authConfig.tokenUrl', hidden: true, initialValue: 'https://provider.example.com/token' })
|
|
201
|
+
);
|
|
202
|
+
expect(fields.filter((field) => field.componentType === FormComponentType.BUTTON)).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('withholds the method and every field of it from agents older than its start version', () => {
|
|
206
|
+
// An agent below the start version mints the token but never puts it on the request, so the
|
|
207
|
+
// option must not be selectable there and nothing of the method may render; the form refuses
|
|
208
|
+
// a save whose selected option the agent does not support.
|
|
209
|
+
const fields = buildFields({ oauth2ClientCredentials: { tokenUrl: 'https://provider.example.com/token', startVersion: '0.0.18' } });
|
|
210
|
+
const authType = fields.find((field) => field.name === 'authType');
|
|
211
|
+
|
|
212
|
+
expect(authType).toEqual(
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
options: expect.arrayContaining([
|
|
215
|
+
expect.objectContaining({ value: IntegrationAuthType.OAUTH2_CLIENT_CREDS, startVersion: '0.0.18' })
|
|
216
|
+
])
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
const methodFields = fields.filter(
|
|
220
|
+
(field) =>
|
|
221
|
+
field.display?.show?.authType?.includes(IntegrationAuthType.OAUTH2_CLIENT_CREDS) || field.name === 'authConfig.sendBearerHeader'
|
|
222
|
+
);
|
|
223
|
+
expect(methodFields.length).toBeGreaterThan(0);
|
|
224
|
+
expect(methodFields.every((field) => field.startVersion === '0.0.18')).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('keys its token by configuration from the form onward', () => {
|
|
228
|
+
// A pre-save Test Connection sends the form's auth config straight to the orchestrator, before
|
|
229
|
+
// the server can pin anything. Without a datasource scope in it, the token lands under the
|
|
230
|
+
// client-id key, which every tenant that chose the same identifier shares.
|
|
231
|
+
const fields = buildFields({ oauth2ClientCredentials: { tokenUrl: 'https://provider.example.com/token', startVersion: '0.0.18' } });
|
|
232
|
+
const tokenScope = fields.find((field) => field.name === 'authConfig.tokenScope');
|
|
233
|
+
|
|
234
|
+
expect(tokenScope).toEqual(
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
hidden: true,
|
|
237
|
+
initialValue: 'datasource',
|
|
238
|
+
display: { show: { authType: [IntegrationAuthType.OAUTH2_CLIENT_CREDS] } }
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('marks the token for the Authorization header, which the legacy oauth method never does', () => {
|
|
244
|
+
// The orchestrator sees a native OpenAPI step as a plain REST API step, so the template has
|
|
245
|
+
// to say which of the two it is. It reads the marker as a boolean; a string 'true' is unset.
|
|
246
|
+
expect(
|
|
247
|
+
buildFields({ oauth2ClientCredentials: { tokenUrl: 'https://provider.example.com/token', startVersion: '0.0.18' } })
|
|
248
|
+
).toContainEqual(expect.objectContaining({ name: 'authConfig.sendBearerHeader', hidden: true, initialValue: true }));
|
|
249
|
+
expect(buildFields({ oauth: true }).map((field) => field.name)).not.toContain('authConfig.sendBearerHeader');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const authorizationCodeMethod = {
|
|
253
|
+
tokenUrl: 'https://provider.example.com/token',
|
|
254
|
+
authorizationUrl: 'https://provider.example.com/authorize',
|
|
255
|
+
revokeTokenUrl: 'https://provider.example.com/revoke',
|
|
256
|
+
iconUrl: 'https://provider.example.com/icon.png'
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
test.each([
|
|
260
|
+
['oauth2BringYourOwn', { oauth2BringYourOwn: authorizationCodeMethod }],
|
|
261
|
+
['oauth2SuperblocksClient', { oauth2SuperblocksClient: { ...authorizationCodeMethod, clientId: 'hosted-client' } }]
|
|
262
|
+
])('refuses a template whose %s method disagrees with it on the token URL', (_name, authorizationCode) => {
|
|
263
|
+
// Every OAuth method stores its endpoint in the same field. Let them differ and whichever
|
|
264
|
+
// hidden item the form reads last decides where the client secret is sent.
|
|
265
|
+
expect(() =>
|
|
266
|
+
buildFields({
|
|
267
|
+
...authorizationCode,
|
|
268
|
+
oauth2ClientCredentials: { tokenUrl: 'https://provider.example.com/other-token', startVersion: '0.0.18' }
|
|
269
|
+
})
|
|
270
|
+
).toThrow('different token URLs');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('refuses a template that also offers the grant through the legacy oauth method', () => {
|
|
274
|
+
// Both methods put the same auth type in the dropdown and gate the same fields on it, so the
|
|
275
|
+
// form would show two of everything.
|
|
276
|
+
expect(() =>
|
|
277
|
+
buildFields({ oauth: true, oauth2ClientCredentials: { tokenUrl: 'https://provider.example.com/token', startVersion: '0.0.18' } })
|
|
278
|
+
).toThrow('twice');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
IntegrationAuthType,
|
|
13
13
|
NewAuthType,
|
|
14
14
|
OAUTH_CALLBACK_PATH,
|
|
15
|
+
RestApiFields,
|
|
15
16
|
TokenScope
|
|
16
17
|
} from '../../../types/index.js';
|
|
17
18
|
import { RestApiIntegrationPluginVersions } from './restapiintegration.js';
|
|
@@ -54,6 +55,7 @@ export type AuthMethods = {
|
|
|
54
55
|
tokenPrefixed?: boolean | { prefix: string };
|
|
55
56
|
oauth2BringYourOwn?: OAuth2Config;
|
|
56
57
|
oauth2SuperblocksClient?: OAuth2HostedClientConfig;
|
|
58
|
+
oauth2ClientCredentials?: OAuth2ClientCredentialsConfig;
|
|
57
59
|
oauth2TokenExchange?: boolean;
|
|
58
60
|
// controls proto defined auth components
|
|
59
61
|
passwordGrantFlow?: boolean;
|
|
@@ -84,6 +86,21 @@ type OAuth2Config = {
|
|
|
84
86
|
|
|
85
87
|
type OAuth2HostedClientConfig = OAuth2Config & { clientId: string };
|
|
86
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The client credentials grant needs nothing but a token endpoint: there is no authorization
|
|
91
|
+
* step, no user, nothing to revoke and no Connect button. The orchestrator mints the token from
|
|
92
|
+
* the client's own secret at execution time, so a template on this method serves executions that
|
|
93
|
+
* run with nobody signed in, such as workflows and scheduled jobs.
|
|
94
|
+
*/
|
|
95
|
+
type OAuth2ClientCredentialsConfig = {
|
|
96
|
+
tokenUrl: string;
|
|
97
|
+
defaultScope?: string;
|
|
98
|
+
// The first plugin version whose agent puts the minted token on the Authorization header for
|
|
99
|
+
// this method. Below it the option and its fields are hidden and a save is refused, because an
|
|
100
|
+
// older agent would mint the token and then send the request without it.
|
|
101
|
+
startVersion: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
87
104
|
/**
|
|
88
105
|
* Every value `authConfig.usePkce` can hold while PKCE is off. `display.show`
|
|
89
106
|
* matches on the stringified form value, so a spelling missing from this list
|
|
@@ -114,6 +131,7 @@ const AUTH_METHODS_TO_TYPES = {
|
|
|
114
131
|
tokenPrefixed: IntegrationAuthType.TOKEN_PREFIXED,
|
|
115
132
|
oauth2BringYourOwn: IntegrationAuthType.OAUTH2_CODE,
|
|
116
133
|
oauth2SuperblocksClient: IntegrationAuthType.OAUTH2_CODE,
|
|
134
|
+
oauth2ClientCredentials: IntegrationAuthType.OAUTH2_CLIENT_CREDS,
|
|
117
135
|
// those matches proto types auth.method.case
|
|
118
136
|
passwordGrantFlow: NewAuthType.OAUTH2_PASSWORD_GRANT_FLOW,
|
|
119
137
|
authorizationCodeFlow: NewAuthType.OAUTH2_AUTH_CODE_FLOW,
|
|
@@ -134,7 +152,8 @@ export const authSections = ({
|
|
|
134
152
|
fieldNamesToHide = [],
|
|
135
153
|
isProto = false,
|
|
136
154
|
// allow caller to provide optional customized auth items
|
|
137
|
-
prependItems = []
|
|
155
|
+
prependItems = [],
|
|
156
|
+
authOptionsLdFlag
|
|
138
157
|
}: {
|
|
139
158
|
startVersion: string;
|
|
140
159
|
pkceStartVersion?: string;
|
|
@@ -147,6 +166,8 @@ export const authSections = ({
|
|
|
147
166
|
allowNone?: boolean;
|
|
148
167
|
fieldNamesToHide?: string[];
|
|
149
168
|
isProto?: boolean;
|
|
169
|
+
// Rollout gate on the Authentication dropdown: see DropdownFormItem.optionsLdFlag.
|
|
170
|
+
authOptionsLdFlag?: { ldFlag: string; keep: string[] };
|
|
150
171
|
prependItems?: FormItem[];
|
|
151
172
|
}): FormSection[] => {
|
|
152
173
|
// Extract display name override before filtering methods
|
|
@@ -200,6 +221,7 @@ export const authSections = ({
|
|
|
200
221
|
componentType: FormComponentType.DROPDOWN,
|
|
201
222
|
initialValue: authTypeInitialValue,
|
|
202
223
|
options: authTypeOptions({ enabledMethods, allowNone, apiKeyFormDisplayName, pkceStartVersion }),
|
|
224
|
+
...(authOptionsLdFlag ? { optionsLdFlag: authOptionsLdFlag } : {}),
|
|
203
225
|
disabled: !allowNone && enabledMethodKeys.length === 1
|
|
204
226
|
}
|
|
205
227
|
],
|
|
@@ -924,6 +946,21 @@ export const authSections = ({
|
|
|
924
946
|
}
|
|
925
947
|
}
|
|
926
948
|
} as FormItem,
|
|
949
|
+
{
|
|
950
|
+
label: 'Client authentication',
|
|
951
|
+
name: 'authConfig.clientAuthMethod',
|
|
952
|
+
startVersion: startVersion,
|
|
953
|
+
componentType: FormComponentType.DROPDOWN,
|
|
954
|
+
// Existing client credentials integrations authenticate with the
|
|
955
|
+
// Basic header, so the default stays Basic to leave them unchanged.
|
|
956
|
+
initialValue: ClientAuthMethod.BASIC,
|
|
957
|
+
options: CLIENT_AUTH_METHOD_OPTIONS,
|
|
958
|
+
display: {
|
|
959
|
+
show: {
|
|
960
|
+
authType: [IntegrationAuthType.OAUTH2_CLIENT_CREDS]
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
},
|
|
927
964
|
{
|
|
928
965
|
label: 'Share username/password across all users',
|
|
929
966
|
name: 'authConfig.useFixedPasswordCreds',
|
|
@@ -1268,6 +1305,119 @@ export const authSections = ({
|
|
|
1268
1305
|
fieldNamesToHide
|
|
1269
1306
|
);
|
|
1270
1307
|
}
|
|
1308
|
+
if (enabledMethods.oauth2ClientCredentials) {
|
|
1309
|
+
// The legacy `oauth` method already offers this grant, with a user-entered token URL, under
|
|
1310
|
+
// the same auth type. Offering both would put two options with one key in the dropdown and
|
|
1311
|
+
// two of every field behind it.
|
|
1312
|
+
if (enabledMethods.oauth) {
|
|
1313
|
+
throw new Error(`Plugin ${pluginId} offers the client credentials grant twice`);
|
|
1314
|
+
}
|
|
1315
|
+
// Every OAuth method stores its endpoint in the same field, so a template that offers more
|
|
1316
|
+
// than one has to mean the same endpoint by it. Disagreeing would leave whichever hidden item
|
|
1317
|
+
// the form reads last in charge of where the client secret is sent.
|
|
1318
|
+
const otherTokenUrls = [enabledMethods.oauth2BringYourOwn?.tokenUrl, enabledMethods.oauth2SuperblocksClient?.tokenUrl].filter(
|
|
1319
|
+
(url): url is string => url !== undefined
|
|
1320
|
+
);
|
|
1321
|
+
if (otherTokenUrls.some((url) => url !== enabledMethods.oauth2ClientCredentials?.tokenUrl)) {
|
|
1322
|
+
throw new Error(`Plugin ${pluginId} gives its OAuth methods different token URLs`);
|
|
1323
|
+
}
|
|
1324
|
+
const clientCredentialsStartVersion = enabledMethods.oauth2ClientCredentials.startVersion;
|
|
1325
|
+
addField(
|
|
1326
|
+
authFields,
|
|
1327
|
+
[
|
|
1328
|
+
{
|
|
1329
|
+
label: 'Access token URL',
|
|
1330
|
+
name: 'authConfig.tokenUrl',
|
|
1331
|
+
startVersion: clientCredentialsStartVersion,
|
|
1332
|
+
componentType: FormComponentType.INPUT_TEXT,
|
|
1333
|
+
hidden: true,
|
|
1334
|
+
initialValue: enabledMethods.oauth2ClientCredentials.tokenUrl
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
// A native OpenAPI step reaches the orchestrator as a plain REST API step with no plugin
|
|
1338
|
+
// id, so this is how the template says the token belongs on the Authorization header.
|
|
1339
|
+
// The generic REST API integration's legacy `oauth` method leaves it unset: its users
|
|
1340
|
+
// place `{{ oauth.token }}` in a header of their own choosing.
|
|
1341
|
+
label: 'Send bearer header',
|
|
1342
|
+
name: 'authConfig.sendBearerHeader',
|
|
1343
|
+
startVersion: clientCredentialsStartVersion,
|
|
1344
|
+
componentType: FormComponentType.CHECKBOX,
|
|
1345
|
+
hidden: true,
|
|
1346
|
+
initialValue: true
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
// The shared token cache keys a client-credentials token by configuration only under this
|
|
1350
|
+
// scope; a client-id key would be shared by every tenant that chose the same identifier.
|
|
1351
|
+
// The server pins the stored value as well, but a pre-save Test Connection sends the
|
|
1352
|
+
// form's values straight to the orchestrator, so the form has to carry it too.
|
|
1353
|
+
label: 'Share access token across all users',
|
|
1354
|
+
name: 'authConfig.tokenScope',
|
|
1355
|
+
startVersion: clientCredentialsStartVersion,
|
|
1356
|
+
componentType: FormComponentType.INPUT_TEXT,
|
|
1357
|
+
hidden: true,
|
|
1358
|
+
initialValue: TokenScope.DATASOURCE,
|
|
1359
|
+
display: {
|
|
1360
|
+
show: {
|
|
1361
|
+
authType: [IntegrationAuthType.OAUTH2_CLIENT_CREDS]
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
label: 'Client ID',
|
|
1367
|
+
name: 'authConfig.clientId',
|
|
1368
|
+
startVersion: clientCredentialsStartVersion,
|
|
1369
|
+
componentType: FormComponentType.DYNAMIC_INPUT_TEXT,
|
|
1370
|
+
placeholder: `Client ID`,
|
|
1371
|
+
rules: [{ required: true, message: 'Client ID is required' }],
|
|
1372
|
+
tooltip: {
|
|
1373
|
+
markdownText: `The client identifier issued to the client during the application registration process.`
|
|
1374
|
+
},
|
|
1375
|
+
singleLine: true,
|
|
1376
|
+
display: {
|
|
1377
|
+
show: {
|
|
1378
|
+
authType: [IntegrationAuthType.OAUTH2_CLIENT_CREDS]
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
label: 'Client secret',
|
|
1384
|
+
name: 'authConfig.clientSecret',
|
|
1385
|
+
startVersion: clientCredentialsStartVersion,
|
|
1386
|
+
componentType: FormComponentType.INPUT_TEXT,
|
|
1387
|
+
rules: [{ required: true, message: 'Client secret is required' }],
|
|
1388
|
+
placeholder: `Client Secret`,
|
|
1389
|
+
dataType: InputDataType.PASSWORD,
|
|
1390
|
+
tooltip: {
|
|
1391
|
+
markdownText: `The client secret issued to the client during the application registration process. This grant is for confidential clients only: the token is minted from the secret alone, with no user involved.`
|
|
1392
|
+
},
|
|
1393
|
+
singleLine: false,
|
|
1394
|
+
display: {
|
|
1395
|
+
show: {
|
|
1396
|
+
authType: [IntegrationAuthType.OAUTH2_CLIENT_CREDS]
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
label: 'Scope',
|
|
1402
|
+
name: 'authConfig.scope',
|
|
1403
|
+
startVersion: clientCredentialsStartVersion,
|
|
1404
|
+
componentType: FormComponentType.DYNAMIC_INPUT_TEXT,
|
|
1405
|
+
placeholder: `e.g. read write`,
|
|
1406
|
+
tooltip: {
|
|
1407
|
+
markdownText: `The scope of access request. It may have multiple space-separated values.`
|
|
1408
|
+
},
|
|
1409
|
+
singleLine: true,
|
|
1410
|
+
initialValue: enabledMethods.oauth2ClientCredentials.defaultScope,
|
|
1411
|
+
display: {
|
|
1412
|
+
show: {
|
|
1413
|
+
authType: [IntegrationAuthType.OAUTH2_CLIENT_CREDS]
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
],
|
|
1418
|
+
fieldNamesToHide
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1271
1421
|
if (enabledMethods.oauth2SuperblocksClient) {
|
|
1272
1422
|
addField(
|
|
1273
1423
|
authFields,
|
|
@@ -1469,6 +1619,9 @@ export const authSections = ({
|
|
|
1469
1619
|
'authConfig.refreshTokenFromServer',
|
|
1470
1620
|
'authConfig.clientAuthMethod',
|
|
1471
1621
|
'authConfig.usePkce',
|
|
1622
|
+
// Resolves the tenant placeholder in the URLs above for providers that host each
|
|
1623
|
+
// customer on their own subdomain.
|
|
1624
|
+
RestApiFields.OPENAPI_TENANT_NAME,
|
|
1472
1625
|
'dynamicWorkflowConfiguration.enabled',
|
|
1473
1626
|
'dynamicWorkflowConfiguration.workflowId'
|
|
1474
1627
|
],
|
|
@@ -1500,7 +1653,9 @@ export const authSections = ({
|
|
|
1500
1653
|
'authConfig.tokenUrl',
|
|
1501
1654
|
'authConfig.authorizationUrl',
|
|
1502
1655
|
'authConfig.refreshTokenFromServer',
|
|
1503
|
-
'authConfig.revokeTokenUrl'
|
|
1656
|
+
'authConfig.revokeTokenUrl',
|
|
1657
|
+
// See the connect button.
|
|
1658
|
+
RestApiFields.OPENAPI_TENANT_NAME
|
|
1504
1659
|
],
|
|
1505
1660
|
display: {
|
|
1506
1661
|
show: {
|
|
@@ -1683,6 +1838,14 @@ function authTypeOptions({
|
|
|
1683
1838
|
key: IntegrationAuthType.OAUTH2_CODE
|
|
1684
1839
|
});
|
|
1685
1840
|
}
|
|
1841
|
+
if (enabledMethods.oauth2ClientCredentials) {
|
|
1842
|
+
options.push({
|
|
1843
|
+
displayName: `OAuth2.0 - Client Credentials`,
|
|
1844
|
+
value: IntegrationAuthType.OAUTH2_CLIENT_CREDS,
|
|
1845
|
+
key: IntegrationAuthType.OAUTH2_CLIENT_CREDS,
|
|
1846
|
+
startVersion: enabledMethods.oauth2ClientCredentials.startVersion
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1686
1849
|
|
|
1687
1850
|
// following is only used by proto based plugins
|
|
1688
1851
|
// the keys are proto oneof case options
|
|
@@ -41,7 +41,9 @@ export const getNativeOpenApiPlugin = ({
|
|
|
41
41
|
openApiSpecURL,
|
|
42
42
|
headers,
|
|
43
43
|
tenantInput,
|
|
44
|
-
isStreamable
|
|
44
|
+
isStreamable,
|
|
45
|
+
defaultAuthMethod,
|
|
46
|
+
authOptionsLdFlag
|
|
45
47
|
}: {
|
|
46
48
|
id: ExtendedIntegrationPluginId;
|
|
47
49
|
name: string;
|
|
@@ -53,6 +55,12 @@ export const getNativeOpenApiPlugin = ({
|
|
|
53
55
|
headers?: KVPair[];
|
|
54
56
|
tenantInput?: TenantInput;
|
|
55
57
|
isStreamable?: boolean;
|
|
58
|
+
// Which of `authMethods` a new integration starts on. Without it the initial value falls to
|
|
59
|
+
// whichever key happens to come first in the `authMethods` literal, which is not something a
|
|
60
|
+
// template should depend on.
|
|
61
|
+
defaultAuthMethod?: keyof AuthMethods;
|
|
62
|
+
// Rollout gate on the Authentication dropdown: see DropdownFormItem.optionsLdFlag.
|
|
63
|
+
authOptionsLdFlag?: { ldFlag: string; keep: string[] };
|
|
56
64
|
}): Plugin => {
|
|
57
65
|
if (authMethods.oauth2BringYourOwn) {
|
|
58
66
|
oauth2BringYourOwnPluginIds.add(id);
|
|
@@ -89,7 +97,9 @@ export const getNativeOpenApiPlugin = ({
|
|
|
89
97
|
// more.
|
|
90
98
|
startVersion: RestApiIntegrationPluginVersions.V1, // TODO: Should really be a REST version bump.
|
|
91
99
|
componentType: FormComponentType.DYNAMIC_INPUT_TEXT,
|
|
92
|
-
|
|
100
|
+
// The message is what the form shows when Create is blocked on this field. Left empty, the
|
|
101
|
+
// save fails with nothing on screen; a method with no Connect step hits this first.
|
|
102
|
+
rules: [{ required: true, message: `${tenantInput?.label ?? 'Tenant'} is required` }]
|
|
93
103
|
});
|
|
94
104
|
}
|
|
95
105
|
|
|
@@ -128,7 +138,9 @@ export const getNativeOpenApiPlugin = ({
|
|
|
128
138
|
pluginId: id,
|
|
129
139
|
pluginName: name,
|
|
130
140
|
enabledMethods: authMethods,
|
|
131
|
-
|
|
141
|
+
defaultMethod: defaultAuthMethod,
|
|
142
|
+
allowNone: false,
|
|
143
|
+
authOptionsLdFlag
|
|
132
144
|
})
|
|
133
145
|
]
|
|
134
146
|
},
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { getRowItemsFromSectionItem, IntegrationAuthType, resolveOpenApiTenantUrl, RestApiFields } from '../../types/index.js';
|
|
4
|
+
import { RestApiIntegrationPluginVersions } from './shared/index.js';
|
|
5
|
+
import { ZENDESK_OAUTH_ROLLOUT_FLAG, ZendeskPlugin } from './zendesk.js';
|
|
6
|
+
|
|
7
|
+
const datasourceItems = ZendeskPlugin.datasourceTemplate?.sections.flatMap((section) => section.items.flatMap(getRowItemsFromSectionItem));
|
|
8
|
+
|
|
9
|
+
const findDatasourceItem = (name: string) => datasourceItems?.find((item) => item.name === name);
|
|
10
|
+
|
|
11
|
+
const resolveForTenant = (name: string) => resolveOpenApiTenantUrl(findDatasourceItem(name)?.initialValue as string, 'acme');
|
|
12
|
+
|
|
13
|
+
describe('Zendesk datasource template', () => {
|
|
14
|
+
it('offers OAuth without withdrawing basic auth', () => {
|
|
15
|
+
// Zendesk does not switch API tokens off until 2027-04-30, so integrations already
|
|
16
|
+
// authenticating with one have to keep working alongside the new method.
|
|
17
|
+
expect(findDatasourceItem('authType')).toEqual(
|
|
18
|
+
expect.objectContaining({
|
|
19
|
+
options: expect.arrayContaining([
|
|
20
|
+
expect.objectContaining({ value: IntegrationAuthType.BASIC }),
|
|
21
|
+
expect.objectContaining({ value: IntegrationAuthType.OAUTH2_CODE })
|
|
22
|
+
])
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('gates every method but the API token behind the rollout flag, as an allowlist', () => {
|
|
28
|
+
// Off, the form offers only basic auth and starts there; an auth method added later is
|
|
29
|
+
// hidden by construction rather than by remembering to list it.
|
|
30
|
+
expect(ZENDESK_OAUTH_ROLLOUT_FLAG).toBe('integrations.zendesk.oauth');
|
|
31
|
+
expect(findDatasourceItem('authType')).toEqual(
|
|
32
|
+
expect.objectContaining({ optionsLdFlag: { ldFlag: ZENDESK_OAUTH_ROLLOUT_FLAG, keep: [IntegrationAuthType.BASIC] } })
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('offers client credentials with only the inputs that grant needs', () => {
|
|
37
|
+
// Workflows and scheduled jobs run with nobody signed in, so they cannot click Connect. The
|
|
38
|
+
// grant is minted from the client's own secret, so the form asks for the client, its secret
|
|
39
|
+
// and a scope, and nothing that belongs to the authorization-code flow: no Connect or Revoke
|
|
40
|
+
// button, no callback URL, no token-sharing choice.
|
|
41
|
+
expect(findDatasourceItem('authType')).toEqual(
|
|
42
|
+
expect.objectContaining({
|
|
43
|
+
options: expect.arrayContaining([
|
|
44
|
+
// Gated on the first agent that puts the token on the Authorization header itself.
|
|
45
|
+
expect.objectContaining({ value: IntegrationAuthType.OAUTH2_CLIENT_CREDS, startVersion: RestApiIntegrationPluginVersions.V18 })
|
|
46
|
+
])
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
const clientCredentialsItems = datasourceItems
|
|
50
|
+
?.filter((item) => !item.hidden && item.display?.show?.authType?.includes(IntegrationAuthType.OAUTH2_CLIENT_CREDS))
|
|
51
|
+
.map((item) => item.name);
|
|
52
|
+
expect(clientCredentialsItems).toEqual(['authConfig.clientId', 'authConfig.clientSecret', 'authConfig.scope']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('tells the user when the tenant is missing', () => {
|
|
56
|
+
// Every OAuth URL hangs off the tenant, so a save without one has to fail, and the form shows
|
|
57
|
+
// the rule's message as the reason. With client credentials there is no Connect step to catch
|
|
58
|
+
// the omission earlier, so an empty message leaves Create failing with nothing on screen.
|
|
59
|
+
expect(findDatasourceItem(RestApiFields.OPENAPI_TENANT_NAME)).toEqual(
|
|
60
|
+
expect.objectContaining({ rules: [{ required: true, message: 'Zendesk tenant is required' }] })
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('starts a new integration on OAuth', () => {
|
|
65
|
+
// Zendesk stops issuing new API tokens on 2026-10-27, so basic auth is not a usable
|
|
66
|
+
// starting point for anyone configuring Zendesk from here on.
|
|
67
|
+
expect(findDatasourceItem('authType')?.initialValue).toBe(IntegrationAuthType.OAUTH2_CODE);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('sends the tenant with the events that act on those endpoints', () => {
|
|
71
|
+
// The connect and revoke buttons carry a declared list of form values into their events, and
|
|
72
|
+
// the handlers resolve the placeholder from it. Without the tenant in that list every URL
|
|
73
|
+
// above reaches its handler unresolved.
|
|
74
|
+
const buttonNames = ['oauth-connect-button', 'oauth-revoke-shared-tokens-button'];
|
|
75
|
+
|
|
76
|
+
for (const name of buttonNames) {
|
|
77
|
+
expect(findDatasourceItem(name), name).toEqual(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
valuesFromContext: expect.arrayContaining([RestApiFields.OPENAPI_TENANT_NAME])
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('points its OAuth endpoints at the configured tenant', () => {
|
|
86
|
+
// Every URL is per-subdomain, and the grant endpoint (/oauth/tokens) is a different path
|
|
87
|
+
// from the token management API (/api/v2/oauth/tokens) that revocation uses.
|
|
88
|
+
expect({
|
|
89
|
+
authorizationUrl: resolveForTenant('authConfig.authorizationUrl'),
|
|
90
|
+
tokenUrl: resolveForTenant('authConfig.tokenUrl'),
|
|
91
|
+
revokeTokenUrl: resolveForTenant('authConfig.revokeTokenUrl')
|
|
92
|
+
}).toEqual({
|
|
93
|
+
authorizationUrl: 'https://acme.zendesk.com/oauth/authorizations/new',
|
|
94
|
+
tokenUrl: 'https://acme.zendesk.com/oauth/tokens',
|
|
95
|
+
revokeTokenUrl: 'https://acme.zendesk.com/api/v2/oauth/tokens/current'
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -1,13 +1,53 @@
|
|
|
1
|
-
import { ExtendedIntegrationPluginId, Plugin, SUPERBLOCKS_OPENAPI_TENANT_KEYWORD } from '../../types/index.js';
|
|
2
|
-
import {
|
|
1
|
+
import { ExtendedIntegrationPluginId, IntegrationAuthType, Plugin, SUPERBLOCKS_OPENAPI_TENANT_KEYWORD } from '../../types/index.js';
|
|
2
|
+
import { PUBLIC_INTEGRATIONS_LOGO_URL } from './constants.js';
|
|
3
|
+
import { ClientAuthMethod, getNativeOpenApiPlugin, RestApiIntegrationPluginVersions } from './shared/index.js';
|
|
4
|
+
|
|
5
|
+
// Zendesk gives every account its own subdomain, and its OAuth endpoints live under that
|
|
6
|
+
// subdomain rather than a shared host. These URLs therefore carry the tenant placeholder: the
|
|
7
|
+
// server resolves them into the stored configuration on save, and the browser resolves them
|
|
8
|
+
// for a Connect that happens before the first save. Every other OAuth template here can
|
|
9
|
+
// hardcode its endpoints.
|
|
10
|
+
const ZENDESK_HOST = `https://${SUPERBLOCKS_OPENAPI_TENANT_KEYWORD}.zendesk.com`;
|
|
11
|
+
|
|
12
|
+
// Kill switch for the OAuth rollout, read by the integration form only. Off, the Authentication
|
|
13
|
+
// dropdown offers just the API token method and a new integration starts on it; execution never
|
|
14
|
+
// consults it, so an integration already connected with OAuth keeps working. The UI flag table
|
|
15
|
+
// (Flags.ts, INTEGRATIONS_ZENDESK_OAUTH) carries the same key.
|
|
16
|
+
export const ZENDESK_OAUTH_ROLLOUT_FLAG = 'integrations.zendesk.oauth';
|
|
3
17
|
|
|
4
18
|
export const ZendeskPlugin: Plugin = getNativeOpenApiPlugin({
|
|
5
19
|
name: 'Zendesk',
|
|
6
20
|
id: ExtendedIntegrationPluginId.ZENDESK,
|
|
7
21
|
authMethods: {
|
|
8
|
-
|
|
22
|
+
// Retained because Zendesk does not switch off API tokens until 2027-04-30 and existing
|
|
23
|
+
// integrations authenticate with one. New integrations default to OAuth, since Zendesk
|
|
24
|
+
// stops issuing API tokens well before then.
|
|
25
|
+
basic: true,
|
|
26
|
+
oauth2BringYourOwn: {
|
|
27
|
+
authorizationUrl: `${ZENDESK_HOST}/oauth/authorizations/new`,
|
|
28
|
+
tokenUrl: `${ZENDESK_HOST}/oauth/tokens`,
|
|
29
|
+
revokeTokenUrl: `${ZENDESK_HOST}/api/v2/oauth/tokens/current`,
|
|
30
|
+
// Zendesk reads client_id/client_secret from the token request body.
|
|
31
|
+
clientAuthMethod: ClientAuthMethod.POST,
|
|
32
|
+
iconUrl: `${PUBLIC_INTEGRATIONS_LOGO_URL}/zendesk.png`,
|
|
33
|
+
defaultScope: 'read write',
|
|
34
|
+
authorizationExtraParams: {
|
|
35
|
+
responseType: 'code'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
// Workflows and scheduled jobs run with nobody signed in, so they need a token no one has to
|
|
39
|
+
// click Connect for. Zendesk issues one to a confidential client from its own secret, with
|
|
40
|
+
// no refresh token, from the same tenant endpoint the authorization-code exchange uses.
|
|
41
|
+
oauth2ClientCredentials: {
|
|
42
|
+
tokenUrl: `${ZENDESK_HOST}/oauth/tokens`,
|
|
43
|
+
defaultScope: 'read write',
|
|
44
|
+
// Agents before 0.0.18 mint the token but never put it on the request.
|
|
45
|
+
startVersion: RestApiIntegrationPluginVersions.V18
|
|
46
|
+
}
|
|
9
47
|
},
|
|
10
|
-
|
|
48
|
+
defaultAuthMethod: 'oauth2BringYourOwn',
|
|
49
|
+
authOptionsLdFlag: { ldFlag: ZENDESK_OAUTH_ROLLOUT_FLAG, keep: [IntegrationAuthType.BASIC] },
|
|
50
|
+
serverURL: ZENDESK_HOST,
|
|
11
51
|
tenantInput: {
|
|
12
52
|
label: 'Zendesk tenant',
|
|
13
53
|
placeholder: 'acme',
|
package/src/socket/protocol.ts
CHANGED
|
@@ -200,6 +200,25 @@ export interface ServerMethods {
|
|
|
200
200
|
};
|
|
201
201
|
v2: {
|
|
202
202
|
application: {
|
|
203
|
+
getRemediationFindings: ServerMethodSchema<
|
|
204
|
+
{ applicationId: string; reviewFindingIds: string[]; includeDetails?: boolean },
|
|
205
|
+
{
|
|
206
|
+
findings: Array<{
|
|
207
|
+
id: string;
|
|
208
|
+
reviewRunId: string;
|
|
209
|
+
sourcePhase: string | null;
|
|
210
|
+
title?: string;
|
|
211
|
+
severity?: string;
|
|
212
|
+
primaryLocation?: { path: string; line: number | null };
|
|
213
|
+
suggestedFix?: string;
|
|
214
|
+
technicalSummary?: string;
|
|
215
|
+
advisoryId?: string;
|
|
216
|
+
packageName?: string;
|
|
217
|
+
installedVersion?: string;
|
|
218
|
+
fixedVersion?: string;
|
|
219
|
+
}>;
|
|
220
|
+
}
|
|
221
|
+
>;
|
|
203
222
|
create: ServerMethodSchema<PostApplicationCreateRequestBody, IApplicationV2Dto>;
|
|
204
223
|
clone: ServerMethodSchema<{ applicationId: string }, PostApplicationCloneResponseBody>;
|
|
205
224
|
createBranch: ServerMethodSchema<
|