@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.
Files changed (176) hide show
  1. package/dist/database-lifecycle/appDatabaseImport.d.ts +12 -4
  2. package/dist/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
  3. package/dist/database-lifecycle/appDatabaseImport.js +21 -3
  4. package/dist/database-lifecycle/appDatabaseImport.js.map +1 -1
  5. package/dist/database-lifecycle/appDatabaseImport.test.js +17 -0
  6. package/dist/database-lifecycle/appDatabaseImport.test.js.map +1 -1
  7. package/dist/database-lifecycle/index.d.ts +4 -2
  8. package/dist/database-lifecycle/index.d.ts.map +1 -1
  9. package/dist/database-lifecycle/index.js +6 -2
  10. package/dist/database-lifecycle/index.js.map +1 -1
  11. package/dist/database-lifecycle/index.test.js +1 -0
  12. package/dist/database-lifecycle/index.test.js.map +1 -1
  13. package/dist/plugins/templates/shared/auth.d.ts +17 -1
  14. package/dist/plugins/templates/shared/auth.d.ts.map +1 -1
  15. package/dist/plugins/templates/shared/auth.js +139 -2
  16. package/dist/plugins/templates/shared/auth.js.map +1 -1
  17. package/dist/plugins/templates/shared/auth.test.js +86 -0
  18. package/dist/plugins/templates/shared/auth.test.js.map +1 -1
  19. package/dist/plugins/templates/shared/openapi.d.ts +6 -1
  20. package/dist/plugins/templates/shared/openapi.d.ts.map +1 -1
  21. package/dist/plugins/templates/shared/openapi.js +7 -3
  22. package/dist/plugins/templates/shared/openapi.js.map +1 -1
  23. package/dist/plugins/templates/shared/restapiintegration.d.ts +1 -0
  24. package/dist/plugins/templates/shared/restapiintegration.d.ts.map +1 -1
  25. package/dist/plugins/templates/shared/restapiintegration.js +3 -1
  26. package/dist/plugins/templates/shared/restapiintegration.js.map +1 -1
  27. package/dist/plugins/templates/zendesk.d.ts +1 -0
  28. package/dist/plugins/templates/zendesk.d.ts.map +1 -1
  29. package/dist/plugins/templates/zendesk.js +41 -3
  30. package/dist/plugins/templates/zendesk.js.map +1 -1
  31. package/dist/plugins/templates/zendesk.test.d.ts +2 -0
  32. package/dist/plugins/templates/zendesk.test.d.ts.map +1 -0
  33. package/dist/plugins/templates/zendesk.test.js +79 -0
  34. package/dist/plugins/templates/zendesk.test.js.map +1 -0
  35. package/dist/socket/protocol.d.ts +23 -0
  36. package/dist/socket/protocol.d.ts.map +1 -1
  37. package/dist/socket/socketWithClientAuth.d.ts +3 -0
  38. package/dist/socket/socketWithClientAuth.d.ts.map +1 -1
  39. package/dist/socket/socketWithClientAuth.js +12 -1
  40. package/dist/socket/socketWithClientAuth.js.map +1 -1
  41. package/dist/socket/socketWithClientAuth.test.d.ts +2 -0
  42. package/dist/socket/socketWithClientAuth.test.d.ts.map +1 -0
  43. package/dist/socket/socketWithClientAuth.test.js +106 -0
  44. package/dist/socket/socketWithClientAuth.test.js.map +1 -0
  45. package/dist/types/datasource/auth.d.ts +2 -0
  46. package/dist/types/datasource/auth.d.ts.map +1 -1
  47. package/dist/types/datasource/auth.js.map +1 -1
  48. package/dist/types/datasource/index.d.ts +15 -0
  49. package/dist/types/datasource/index.d.ts.map +1 -1
  50. package/dist/types/datasource/index.js +31 -0
  51. package/dist/types/datasource/index.js.map +1 -1
  52. package/dist/types/datasource/index.test.js +20 -0
  53. package/dist/types/datasource/index.test.js.map +1 -1
  54. package/dist/types/deployment/index.d.ts +2 -0
  55. package/dist/types/deployment/index.d.ts.map +1 -1
  56. package/dist/types/event/index.d.ts +3 -0
  57. package/dist/types/event/index.d.ts.map +1 -1
  58. package/dist/types/event/index.js +3 -0
  59. package/dist/types/event/index.js.map +1 -1
  60. package/dist/types/observability/index.d.ts +1 -1
  61. package/dist/types/observability/index.d.ts.map +1 -1
  62. package/dist/types/observability/index.js.map +1 -1
  63. package/dist/types/plugin/form.d.ts +31 -0
  64. package/dist/types/plugin/form.d.ts.map +1 -1
  65. package/dist/types/plugin/form.js +39 -1
  66. package/dist/types/plugin/form.js.map +1 -1
  67. package/dist/types/plugin/form.test.js +24 -0
  68. package/dist/types/plugin/form.test.js.map +1 -1
  69. package/dist/types/plugin/integration.d.ts +32 -0
  70. package/dist/types/plugin/integration.d.ts.map +1 -1
  71. package/dist/types/plugin/integration.js +58 -1
  72. package/dist/types/plugin/integration.js.map +1 -1
  73. package/dist/types/plugin/integration.test.d.ts +2 -0
  74. package/dist/types/plugin/integration.test.d.ts.map +1 -0
  75. package/dist/types/plugin/integration.test.js +39 -0
  76. package/dist/types/plugin/integration.test.js.map +1 -0
  77. package/dist-esm/database-lifecycle/appDatabaseImport.d.ts +12 -4
  78. package/dist-esm/database-lifecycle/appDatabaseImport.d.ts.map +1 -1
  79. package/dist-esm/database-lifecycle/appDatabaseImport.js +21 -3
  80. package/dist-esm/database-lifecycle/appDatabaseImport.js.map +1 -1
  81. package/dist-esm/database-lifecycle/appDatabaseImport.test.js +17 -0
  82. package/dist-esm/database-lifecycle/appDatabaseImport.test.js.map +1 -1
  83. package/dist-esm/database-lifecycle/index.d.ts +4 -2
  84. package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
  85. package/dist-esm/database-lifecycle/index.js +5 -1
  86. package/dist-esm/database-lifecycle/index.js.map +1 -1
  87. package/dist-esm/database-lifecycle/index.test.js +1 -0
  88. package/dist-esm/database-lifecycle/index.test.js.map +1 -1
  89. package/dist-esm/plugins/templates/shared/auth.d.ts +17 -1
  90. package/dist-esm/plugins/templates/shared/auth.d.ts.map +1 -1
  91. package/dist-esm/plugins/templates/shared/auth.js +140 -3
  92. package/dist-esm/plugins/templates/shared/auth.js.map +1 -1
  93. package/dist-esm/plugins/templates/shared/auth.test.js +87 -1
  94. package/dist-esm/plugins/templates/shared/auth.test.js.map +1 -1
  95. package/dist-esm/plugins/templates/shared/openapi.d.ts +6 -1
  96. package/dist-esm/plugins/templates/shared/openapi.d.ts.map +1 -1
  97. package/dist-esm/plugins/templates/shared/openapi.js +7 -3
  98. package/dist-esm/plugins/templates/shared/openapi.js.map +1 -1
  99. package/dist-esm/plugins/templates/shared/restapiintegration.d.ts +1 -0
  100. package/dist-esm/plugins/templates/shared/restapiintegration.d.ts.map +1 -1
  101. package/dist-esm/plugins/templates/shared/restapiintegration.js +3 -1
  102. package/dist-esm/plugins/templates/shared/restapiintegration.js.map +1 -1
  103. package/dist-esm/plugins/templates/zendesk.d.ts +1 -0
  104. package/dist-esm/plugins/templates/zendesk.d.ts.map +1 -1
  105. package/dist-esm/plugins/templates/zendesk.js +42 -4
  106. package/dist-esm/plugins/templates/zendesk.js.map +1 -1
  107. package/dist-esm/plugins/templates/zendesk.test.d.ts +2 -0
  108. package/dist-esm/plugins/templates/zendesk.test.d.ts.map +1 -0
  109. package/dist-esm/plugins/templates/zendesk.test.js +77 -0
  110. package/dist-esm/plugins/templates/zendesk.test.js.map +1 -0
  111. package/dist-esm/socket/protocol.d.ts +23 -0
  112. package/dist-esm/socket/protocol.d.ts.map +1 -1
  113. package/dist-esm/socket/socketWithClientAuth.d.ts +3 -0
  114. package/dist-esm/socket/socketWithClientAuth.d.ts.map +1 -1
  115. package/dist-esm/socket/socketWithClientAuth.js +12 -1
  116. package/dist-esm/socket/socketWithClientAuth.js.map +1 -1
  117. package/dist-esm/socket/socketWithClientAuth.test.d.ts +2 -0
  118. package/dist-esm/socket/socketWithClientAuth.test.d.ts.map +1 -0
  119. package/dist-esm/socket/socketWithClientAuth.test.js +101 -0
  120. package/dist-esm/socket/socketWithClientAuth.test.js.map +1 -0
  121. package/dist-esm/types/datasource/auth.d.ts +2 -0
  122. package/dist-esm/types/datasource/auth.d.ts.map +1 -1
  123. package/dist-esm/types/datasource/auth.js.map +1 -1
  124. package/dist-esm/types/datasource/index.d.ts +15 -0
  125. package/dist-esm/types/datasource/index.d.ts.map +1 -1
  126. package/dist-esm/types/datasource/index.js +29 -0
  127. package/dist-esm/types/datasource/index.js.map +1 -1
  128. package/dist-esm/types/datasource/index.test.js +21 -1
  129. package/dist-esm/types/datasource/index.test.js.map +1 -1
  130. package/dist-esm/types/deployment/index.d.ts +2 -0
  131. package/dist-esm/types/deployment/index.d.ts.map +1 -1
  132. package/dist-esm/types/event/index.d.ts +3 -0
  133. package/dist-esm/types/event/index.d.ts.map +1 -1
  134. package/dist-esm/types/event/index.js +3 -0
  135. package/dist-esm/types/event/index.js.map +1 -1
  136. package/dist-esm/types/observability/index.d.ts +1 -1
  137. package/dist-esm/types/observability/index.d.ts.map +1 -1
  138. package/dist-esm/types/observability/index.js.map +1 -1
  139. package/dist-esm/types/plugin/form.d.ts +31 -0
  140. package/dist-esm/types/plugin/form.d.ts.map +1 -1
  141. package/dist-esm/types/plugin/form.js +36 -0
  142. package/dist-esm/types/plugin/form.js.map +1 -1
  143. package/dist-esm/types/plugin/form.test.js +25 -1
  144. package/dist-esm/types/plugin/form.test.js.map +1 -1
  145. package/dist-esm/types/plugin/integration.d.ts +32 -0
  146. package/dist-esm/types/plugin/integration.d.ts.map +1 -1
  147. package/dist-esm/types/plugin/integration.js +54 -0
  148. package/dist-esm/types/plugin/integration.js.map +1 -1
  149. package/dist-esm/types/plugin/integration.test.d.ts +2 -0
  150. package/dist-esm/types/plugin/integration.test.d.ts.map +1 -0
  151. package/dist-esm/types/plugin/integration.test.js +37 -0
  152. package/dist-esm/types/plugin/integration.test.js.map +1 -0
  153. package/package.json +1 -1
  154. package/src/database-lifecycle/appDatabaseImport.test.ts +21 -0
  155. package/src/database-lifecycle/appDatabaseImport.ts +37 -4
  156. package/src/database-lifecycle/index.test.ts +1 -0
  157. package/src/database-lifecycle/index.ts +7 -1
  158. package/src/plugins/templates/shared/auth.test.ts +119 -1
  159. package/src/plugins/templates/shared/auth.ts +165 -2
  160. package/src/plugins/templates/shared/openapi.ts +15 -3
  161. package/src/plugins/templates/shared/restapiintegration.ts +3 -1
  162. package/src/plugins/templates/zendesk.test.ts +98 -0
  163. package/src/plugins/templates/zendesk.ts +44 -4
  164. package/src/socket/protocol.ts +19 -0
  165. package/src/socket/socketWithClientAuth.test.ts +117 -0
  166. package/src/socket/socketWithClientAuth.ts +15 -1
  167. package/src/types/datasource/auth.ts +4 -0
  168. package/src/types/datasource/index.test.ts +24 -1
  169. package/src/types/datasource/index.ts +31 -0
  170. package/src/types/deployment/index.ts +2 -0
  171. package/src/types/event/index.ts +3 -0
  172. package/src/types/observability/index.ts +2 -1
  173. package/src/types/plugin/form.test.ts +32 -1
  174. package/src/types/plugin/form.ts +54 -0
  175. package/src/types/plugin/integration.test.ts +45 -0
  176. 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
- export const NATIVE_DB_CONNECTOR_ROLE_NAME_PREFIX = 'superblocks-native-db-connector';
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
- rules: [{ required: true, message: '' }]
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
- allowNone: false
141
+ defaultMethod: defaultAuthMethod,
142
+ allowNone: false,
143
+ authOptionsLdFlag
132
144
  })
133
145
  ]
134
146
  },
@@ -10,5 +10,7 @@ export const RestApiIntegrationPluginVersions = {
10
10
  V13: '0.0.13',
11
11
  V14: '0.0.14',
12
12
  V16: '0.0.16',
13
- V17: '0.0.17'
13
+ V17: '0.0.17',
14
+ // First agent that puts a client-credentials token on the Authorization header itself.
15
+ V18: '0.0.18'
14
16
  };
@@ -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 { getNativeOpenApiPlugin } from './shared/index.js';
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
- basic: true
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
- serverURL: `https://${SUPERBLOCKS_OPENAPI_TENANT_KEYWORD}.zendesk.com`,
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',
@@ -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<