Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,48 @@ await descopeClient.management.outboundApplication.deleteUserTokens(undefined, '
// Delete a specific token by its ID
// Token deletion cannot be undone. Use carefully.
await descopeClient.management.outboundApplication.deleteTokenById('token-id');

// List the IDs of the outbound apps a user currently holds a valid token for.
// Use this for connection-status UIs instead of calling fetchToken once per app.
const connectedApps = await descopeClient.management.outboundApplication.listAppsWithUserToken(
'user-id',
'tenant-id' // optional
);
// connectedApps.data => ['app-1', 'app-2']

// Store a static API key for a user / tenant on an apikey-type outbound app
await descopeClient.management.outboundApplication.uploadUserApiKey(
'my-app-id',
'user-id',
'the-users-api-key',
'tenant-id' // optional
);
await descopeClient.management.outboundApplication.uploadTenantApiKey(
'my-app-id',
'tenant-id',
'the-tenants-api-key'
);

// Upload (migrate) an existing OAuth token for a user / tenant on an oauth-type outbound app,
// without requiring the user to re-run the OAuth flow.
await descopeClient.management.outboundApplication.uploadUserToken({
appId: 'my-app-id',
userId: 'user-id',
refreshToken: 'the-refresh-token',
scopes: ['read', 'write'],
});
await descopeClient.management.outboundApplication.uploadTenantToken({
appId: 'my-app-id',
tenantId: 'tenant-id',
accessToken: 'the-access-token',
});

// Batch upload OAuth tokens (all-or-nothing): inspect `failures` to see rejected items
const batchRes = await descopeClient.management.outboundApplication.batchUploadUserTokens([
{ appId: 'my-app-id', userId: 'user-1', accessToken: 'token-1' },
{ appId: 'my-app-id', userId: 'user-2', accessToken: 'token-2' },
]);
// batchRes.data.failures => [{ appId, userId, errorCode, reason }, ...]
```

### Manage Inbound Applications
Expand Down
199 changes: 199 additions & 0 deletions lib/management/outboundapplication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,4 +725,203 @@ describe('Management OutboundApplication', () => {
});
});
});

describe('listAppsWithUserToken', () => {
it('should send the correct request and return the app ids', async () => {
const listResponse = { appIds: ['app1', 'app2'] };
const httpResponse = {
ok: true,
json: () => listResponse,
clone: () => ({ json: () => Promise.resolve(listResponse) }),
status: 200,
};
mockHttpClient.get.mockResolvedValue(httpResponse);

const resp: SdkResponse<string[]> =
await management.outboundApplication.listAppsWithUserToken('user456', 'tenant789');

expect(mockHttpClient.get).toHaveBeenCalledWith(
apiPaths.outboundApplication.listAppsWithUserToken,
{ queryParams: { userId: 'user456', tenantId: 'tenant789' } },
);

expect(resp).toEqual({
code: 200,
data: ['app1', 'app2'],
ok: true,
response: httpResponse,
});
});

it('should omit tenantId when not provided', async () => {
const listResponse = { appIds: ['app1'] };
const httpResponse = {
ok: true,
json: () => listResponse,
clone: () => ({ json: () => Promise.resolve(listResponse) }),
status: 200,
};
mockHttpClient.get.mockResolvedValue(httpResponse);

await management.outboundApplication.listAppsWithUserToken('user456');

expect(mockHttpClient.get).toHaveBeenCalledWith(
apiPaths.outboundApplication.listAppsWithUserToken,
{ queryParams: { userId: 'user456' } },
);
});
});

describe('uploadUserApiKey', () => {
it('should send the correct request', async () => {
const httpResponse = {
ok: true,
json: () => ({}),
clone: () => ({ json: () => Promise.resolve({}) }),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

await management.outboundApplication.uploadUserApiKey(
'app123',
'user456',
'secret-key',
'tenant789',
);

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.outboundApplication.uploadUserApiKey,
{ appId: 'app123', userId: 'user456', apiKey: 'secret-key', tenantId: 'tenant789' },
);
});
});

describe('uploadTenantApiKey', () => {
it('should send the correct request', async () => {
const httpResponse = {
ok: true,
json: () => ({}),
clone: () => ({ json: () => Promise.resolve({}) }),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

await management.outboundApplication.uploadTenantApiKey('app123', 'tenant789', 'secret-key');

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.outboundApplication.uploadTenantApiKey,
{ appId: 'app123', tenantId: 'tenant789', apiKey: 'secret-key' },
);
});
});

describe('uploadUserToken', () => {
it('should send the correct request', async () => {
const httpResponse = {
ok: true,
json: () => ({}),
clone: () => ({ json: () => Promise.resolve({}) }),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

await management.outboundApplication.uploadUserToken({
appId: 'app123',
userId: 'user456',
refreshToken: 'refresh',
scopes: ['read'],
verifyRefresh: true,
});

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.outboundApplication.uploadUserToken,
{
appId: 'app123',
userId: 'user456',
refreshToken: 'refresh',
scopes: ['read'],
verifyRefresh: true,
},
);
});
});

describe('uploadTenantToken', () => {
it('should send the correct request', async () => {
const httpResponse = {
ok: true,
json: () => ({}),
clone: () => ({ json: () => Promise.resolve({}) }),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

await management.outboundApplication.uploadTenantToken({
appId: 'app123',
tenantId: 'tenant789',
accessToken: 'access',
});

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.outboundApplication.uploadTenantToken,
{ appId: 'app123', tenantId: 'tenant789', accessToken: 'access' },
);
});
});

describe('batchUploadUserTokens', () => {
it('should send the tokens and return failures', async () => {
const batchResponse = {
failures: [{ appId: 'app123', userId: 'user2', errorCode: 'E152110', reason: 'bad token' }],
};
const httpResponse = {
ok: true,
json: () => batchResponse,
clone: () => ({ json: () => Promise.resolve(batchResponse) }),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const tokens = [
{ appId: 'app123', userId: 'user1', accessToken: 'a1' },
{ appId: 'app123', userId: 'user2', accessToken: 'a2' },
];
const resp = await management.outboundApplication.batchUploadUserTokens(tokens);

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.outboundApplication.batchUploadUserTokens,
{ tokens },
);

expect(resp).toEqual({
code: 200,
data: batchResponse,
ok: true,
response: httpResponse,
});
});
});

describe('batchUploadTenantTokens', () => {
it('should send the tokens', async () => {
const batchResponse = { failures: [] };
const httpResponse = {
ok: true,
json: () => batchResponse,
clone: () => ({ json: () => Promise.resolve(batchResponse) }),
status: 200,
};
mockHttpClient.post.mockResolvedValue(httpResponse);

const tokens = [{ appId: 'app123', tenantId: 'tenant1', accessToken: 'a1' }];
const resp = await management.outboundApplication.batchUploadTenantTokens(tokens);

expect(mockHttpClient.post).toHaveBeenCalledWith(
apiPaths.outboundApplication.batchUploadTenantTokens,
{ tokens },
);

expect(resp.data).toEqual(batchResponse);
});
});
});
77 changes: 77 additions & 0 deletions lib/management/outboundapplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import {
FetchOutboundAppTokenOptions,
OutboundAppTokenResponse,
CreateOutboundAppByTemplateOptions,
OutboundAppUserTokenToUpload,
OutboundAppTenantTokenToUpload,
UploadOutboundAppUserTokenRequest,
UploadOutboundAppTenantTokenRequest,
BatchUploadOutboundAppTokensResponse,
} from './types';

type OutboundApplicationResponse = {
Expand Down Expand Up @@ -130,6 +135,78 @@ const withOutboundApplication = (httpClient: HttpClient) => ({
queryParams: { id },
}),
),
/**
* List the IDs of the outbound applications the given user currently holds a valid token for.
* Replaces calling fetchToken once per app to derive connection status.
* @param userId the user to look up
* @param tenantId optional tenant to scope the lookup to
*/
listAppsWithUserToken: (userId: string, tenantId?: string): Promise<SdkResponse<string[]>> =>
transformResponse<{ appIds: string[] }, string[]>(
httpClient.get(apiPaths.outboundApplication.listAppsWithUserToken, {
queryParams: { userId, ...(tenantId ? { tenantId } : {}) },
}),
(data) => data.appIds,
),
/** Upload/set a static API key for a user on an apikey-type outbound application. */
uploadUserApiKey: (
appId: string,
userId: string,
apiKey: string,
tenantId?: string,
): Promise<SdkResponse<never>> =>
transformResponse(
httpClient.post(apiPaths.outboundApplication.uploadUserApiKey, {
appId,
userId,
apiKey,
tenantId,
}),
),
/** Upload/set a static API key for a tenant on an apikey-type outbound application. */
uploadTenantApiKey: (
appId: string,
tenantId: string,
apiKey: string,
): Promise<SdkResponse<never>> =>
transformResponse(
httpClient.post(apiPaths.outboundApplication.uploadTenantApiKey, {
appId,
tenantId,
apiKey,
}),
),
/**
* Upload (migrate) an existing OAuth token for a user on an oauth-type outbound application,
* without requiring the user to re-run the OAuth flow.
*/
uploadUserToken: (token: UploadOutboundAppUserTokenRequest): Promise<SdkResponse<never>> =>
transformResponse(httpClient.post(apiPaths.outboundApplication.uploadUserToken, { ...token })),
/** Upload (migrate) an existing OAuth token for a tenant on an oauth-type outbound application. */
uploadTenantToken: (token: UploadOutboundAppTenantTokenRequest): Promise<SdkResponse<never>> =>
transformResponse(
httpClient.post(apiPaths.outboundApplication.uploadTenantToken, { ...token }),
),
/**
* Batch upload (migrate) existing OAuth tokens for users. All-or-nothing: if any item fails
* per-item validation, the returned `failures` are populated and no tokens are committed.
*/
batchUploadUserTokens: (
tokens: OutboundAppUserTokenToUpload[],
): Promise<SdkResponse<BatchUploadOutboundAppTokensResponse>> =>
transformResponse(
httpClient.post(apiPaths.outboundApplication.batchUploadUserTokens, { tokens }),
),
/**
* Batch upload (migrate) existing OAuth tokens for tenants. All-or-nothing: if any item fails
* per-item validation, the returned `failures` are populated and no tokens are committed.
*/
batchUploadTenantTokens: (
tokens: OutboundAppTenantTokenToUpload[],
): Promise<SdkResponse<BatchUploadOutboundAppTokensResponse>> =>
transformResponse(
httpClient.post(apiPaths.outboundApplication.batchUploadTenantTokens, { tokens }),
),
});

export default withOutboundApplication;
7 changes: 7 additions & 0 deletions lib/management/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ export default {
fetchTenantTokenByScopes: '/v1/mgmt/outbound/app/tenant/token',
deleteUserTokens: '/v1/mgmt/outbound/user/tokens',
deleteTokenById: '/v1/mgmt/outbound/token',
listAppsWithUserToken: '/v1/mgmt/outbound/apps-with-user-token',
uploadUserApiKey: '/v1/mgmt/outbound/app/user/apikey/upload',
uploadTenantApiKey: '/v1/mgmt/outbound/app/tenant/apikey/upload',
uploadUserToken: '/v1/mgmt/outbound/app/user/oauthtoken/upload',
uploadTenantToken: '/v1/mgmt/outbound/app/tenant/oauthtoken/upload',
batchUploadUserTokens: '/v1/mgmt/outbound/app/user/oauthtoken/batch/upload',
batchUploadTenantTokens: '/v1/mgmt/outbound/app/tenant/oauthtoken/batch/upload',
},
sso: {
settings: '/v1/mgmt/sso/settings',
Expand Down
Loading
Loading