Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .changeset/26702-console-connector-connections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@object-ui/app-shell': patch
'@object-ui/i18n': patch
'@objectstack/service-automation': patch
'@objectstack/platform-objects': patch
---

feat(console): Connector connections, a Setup page to connect, reconnect or disconnect each OAuth connector instance and see its status

Clause-②: yes (widening: new Setup entry nav_connector_connections resolving automation:connector-connections; no existing key, route or shape changes)
Metadata-protocol: none

A connector instance declared with `auth: { type: 'oauth2', grant: 'authorization_code' }` needs an administrator to sign in to the vendor once for their organization. The Console now has a page for that (`automation:connector-connections`). `@objectstack/service-automation` contributes its Setup → Integrations entry, **Connector Connections**, gated on `manage_connector_connections`, the organization-scoped capability the connection door asks, so an organization administrator sees it; `@objectstack/platform-objects` carries the entry's label in its four Setup translation packs. The page lists every such instance with the organization's connection status as the server answers it (Not connected, Connected, or Reconnect required with the reason the server recorded), the granted scopes, who connected it and when, and when the access token renews. A `degraded` instance shows the server's reason in the same row as the connection that caused it.

**Connect** and **Reconnect** open the vendor's sign-in in its own window, detached from the page, and send it to the authorization URL the server answered. The page re-reads the status until the connect lands, then closes the window and shows the new status. **Disconnect** removes the connection after a confirmation. No token, code or authorization URL is ever rendered.
9 changes: 9 additions & 0 deletions content/docs/automation/connectors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,15 @@ a rotating refresh token is never spent twice.
DELETE /api/v1/automation/connectors/:name/connection → revoke
```

**In the Console**, **Setup → Integrations → Connector Connections** drives
these calls; the entry is shown to holders of `manage_connector_connections`.
It lists each `authorization_code` instance with the organization's status,
the reason the server recorded and, for a `degraded` instance, the server's
reason beside the connection that caused it. **Connect** and **Reconnect**
open the vendor's sign-in in its own window, detached from the page, and the
page shows the new status once the callback has stored it; **Disconnect**
revokes after a confirmation.

`GET …/:name/connection` answers the organization's status
(`not_connected`, `connected`, `reconnect_required`), never a token. The
`state` and the PKCE verifier are bound to the user and organization that
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/setup-integrations-order.pin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// Datasources `@objectstack/service-datasource`
// Connect an Agent `@objectstack/mcp`
// Connector Credentials `@objectstack/service-automation`
// Connector Connections `@objectstack/service-automation` (same contribution)
// Cloud Connection `@objectstack/cloud-connection`
//
// Each contributor states the group's other priorities in its own header so
Expand Down Expand Up @@ -66,7 +67,8 @@ const GROUP = 'group_integrations';
const EXPECTED_TIERS: readonly (readonly string[])[] = [
['nav_webhooks', 'nav_http_deliveries', 'nav_datasources'],
['nav_connect_agent'],
['nav_connector_credentials'],
// One contribution carries both, in this order (ADR-0143 D5 added the second).
['nav_connector_credentials', 'nav_connector_connections'],
['nav_cloud_connection'],
];

Expand Down
1 change: 1 addition & 0 deletions packages/platform-objects/src/apps/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const en: TranslationData = {
nav_http_deliveries: { label: 'HTTP Deliveries' },
nav_datasources: { label: 'Datasources' },
nav_connector_credentials: { label: 'Connector Credentials' },
nav_connector_connections: { label: 'Connector Connections' },

// Advanced
//
Expand Down
1 change: 1 addition & 0 deletions packages/platform-objects/src/apps/translations/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const esES: TranslationData = {
nav_http_deliveries: { label: 'Entregas HTTP' },
nav_datasources: { label: 'Fuentes de datos' },
nav_connector_credentials: { label: 'Credenciales de conectores' },
nav_connector_connections: { label: 'Conexiones de conectores' },

nav_oauth_apps: { label: 'Aplicaciones OAuth' },
},
Expand Down
1 change: 1 addition & 0 deletions packages/platform-objects/src/apps/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const jaJP: TranslationData = {
nav_http_deliveries: { label: 'HTTP 配信' },
nav_datasources: { label: 'データソース' },
nav_connector_credentials: { label: 'コネクタ認証情報' },
nav_connector_connections: { label: 'コネクタ接続' },

nav_oauth_apps: { label: 'OAuth アプリケーション' },
},
Expand Down
1 change: 1 addition & 0 deletions packages/platform-objects/src/apps/translations/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const zhCN: TranslationData = {
nav_http_deliveries: { label: 'HTTP 投递' },
nav_datasources: { label: '数据源' },
nav_connector_credentials: { label: '连接器凭据' },
nav_connector_connections: { label: '连接器连接' },

nav_oauth_apps: { label: 'OAuth 应用' },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
* item in Setup's Integrations group, naming the console component
* `automation:connector-credentials` and asking `manage_metadata` — the
* capability every verb of the door asks;
* • ADR-0143 D5: a second item names `automation:connector-connections`, the
* Console page of the connector connection door, and asks that door's own
* organization-scoped `manage_connector_connections`, so an organization
* administrator (who holds it, not `manage_metadata`) sees it;
* • a booted kernel registers it once, after `kernel:ready`;
* • an inert boot (`armRuntime: false`, a one-shot tool) serves no console
* and registers no entry; neither does a kernel with no manifest service,
Expand All @@ -17,20 +21,24 @@
import { describe, expect, it } from 'vitest';
import { LiteKernel } from '@objectstack/core';
import { ManifestSchema } from '@objectstack/spec/kernel';
import { PLATFORM_CAPABILITIES } from '@objectstack/spec/security';

import { AutomationServicePlugin } from './plugin.js';
import {
CONNECTOR_CREDENTIALS_COMPONENT_REF,
CONNECTOR_CREDENTIALS_NAV_CAPABILITY,
CONNECTOR_CREDENTIALS_UI_BUNDLE,
CONNECTOR_CONNECTIONS_COMPONENT_REF,
CONNECTOR_CONNECTIONS_NAV_CAPABILITY,
} from './connector-credentials-ui.js';


describe('[#27238] the connector credentials UI bundle', () => {
it('is a valid manifest', () => {
expect(() => ManifestSchema.parse(CONNECTOR_CREDENTIALS_UI_BUNDLE)).not.toThrow();
});

it('contributes one Setup → Integrations component item, gated on manage_metadata', () => {
it('contributes two Setup → Integrations component items, each gated on its own door\'s capability', () => {
expect(CONNECTOR_CREDENTIALS_COMPONENT_REF).toBe('automation:connector-credentials');
expect(CONNECTOR_CREDENTIALS_NAV_CAPABILITY).toBe('manage_metadata');
expect(CONNECTOR_CREDENTIALS_UI_BUNDLE.navigationContributions).toEqual([
Expand All @@ -47,10 +55,25 @@ describe('[#27238] the connector credentials UI bundle', () => {
icon: 'key-round',
requiredPermissions: ['manage_metadata'],
},
{
id: 'nav_connector_connections',
type: 'component',
label: 'Connector Connections',
componentRef: 'automation:connector-connections',
icon: 'plug',
requiredPermissions: ['manage_connector_connections'],
},
],
},
]);
});

it('the connections item asks the declared organization-scoped capability the connection door enforces', () => {
expect(CONNECTOR_CONNECTIONS_COMPONENT_REF).toBe('automation:connector-connections');
expect(CONNECTOR_CONNECTIONS_NAV_CAPABILITY).toBe('manage_connector_connections');
const declared = PLATFORM_CAPABILITIES.find((c) => c.name === CONNECTOR_CONNECTIONS_NAV_CAPABILITY);
expect(declared?.scope).toBe('org');
});
});

/** Boot a kernel whose `manifest` service (when given) records every registration. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
* declares the door's own capability, `manage_metadata`, which every one of its
* three routes asks before anything else: the rule the page's routes enforce
* is stated by the plugin that enforces it, not borrowed from the group.
*
* ADR-0143 D5 — the same contribution carries Setup → Integrations →
* "Connector Connections": the console's `automation:connector-connections`
* page, which reads and drives the connector connection door
* (`/api/v1/automation/connectors/:name/connection`). That door asks the
* organization-scoped `manage_connector_connections` and nothing else, so the
* entry asks it too: an organization administrator holds it and not
* `manage_metadata`, and must see the page that connects their organization's
* vendors.
*/

/** The console component the entry opens. */
Expand All @@ -25,6 +34,12 @@ export const CONNECTOR_CREDENTIALS_COMPONENT_REF = 'automation:connector-credent
/** The capability the credential door asks on all three routes. */
export const CONNECTOR_CREDENTIALS_NAV_CAPABILITY = 'manage_metadata';

/** The console component the connections entry opens (ADR-0143 D5). */
export const CONNECTOR_CONNECTIONS_COMPONENT_REF = 'automation:connector-connections';

/** The organization-scoped capability the connector connection door asks on every route. */
export const CONNECTOR_CONNECTIONS_NAV_CAPABILITY = 'manage_connector_connections';

/**
* `priority` orders contributions within a group, lowest first. The group's
* other contributors, read where they are declared: Webhooks / HTTP Deliveries
Expand All @@ -40,7 +55,7 @@ export const CONNECTOR_CREDENTIALS_UI_BUNDLE = {
type: 'plugin',
scope: 'system',
name: 'Connector Credentials UI',
description: 'Setup navigation for the connector credentials the installed apps declare: set, replace or clear each one.',
description: 'Setup navigation for the connector credentials the installed apps declare (set, replace or clear each one) and for the OAuth connections an organization administrator connects.',
navigationContributions: [
{
app: 'setup',
Expand All @@ -55,6 +70,14 @@ export const CONNECTOR_CREDENTIALS_UI_BUNDLE = {
icon: 'key-round',
requiredPermissions: [CONNECTOR_CREDENTIALS_NAV_CAPABILITY],
},
{
id: 'nav_connector_connections',
type: 'component',
label: 'Connector Connections',
componentRef: CONNECTOR_CONNECTIONS_COMPONENT_REF,
icon: 'plug',
requiredPermissions: [CONNECTOR_CONNECTIONS_NAV_CAPABILITY],
},
],
},
],
Expand Down
16 changes: 16 additions & 0 deletions ui/packages/app-shell/src/services/builtinComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { PositionGrantedSetsCell, PositionHolderCountCell } from '../views/metad
import { PackagedAutomationPage } from '../views/setup/PackagedAutomationPage.js';
import { ConnectorCredentialsPage } from '../views/setup/ConnectorCredentialsPage.js';
import { CONNECTOR_CREDENTIALS_REF } from '../views/setup/connectorCredentials.js';
import { ConnectorConnectionsPage } from '../views/setup/ConnectorConnectionsPage.js';
import { CONNECTOR_CONNECTIONS_REF } from '../views/setup/connectorConnections.js';
import { PositionHoldersPage } from '../views/setup/PositionHoldersPage.js';
import { PermissionSetAssignmentPage } from '../views/setup/PermissionSetAssignmentPage.js';
import { PERMISSION_SET_ASSIGNMENT_REF, POSITION_HOLDERS_REF } from '../views/setup/peopleAssignments.js';
Expand Down Expand Up @@ -167,6 +169,20 @@ registerAppComponent({
component: ConnectorCredentialsPage,
});

/**
* Setup → Integrations → Connector Connections (ADR-0143 D5): an organization
* administrator connects, reconnects or disconnects each `oauth2`
* `authorization_code` connector instance for their organization. The entry
* naming this ref is the automation service's to contribute, gated on
* `manage_connector_connections`; the shell declares no entry for it.
*/
registerAppComponent({
ref: CONNECTOR_CONNECTIONS_REF,
label: 'Connector Connections',
source: '@object-ui/app-shell',
component: ConnectorConnectionsPage,
});

/**
* ADR-0146 D7 — Setup → People & organization: who holds a position, and who
* holds a permission set. Each page picks a catalog item and renders the
Expand Down
Loading
Loading