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
18 changes: 18 additions & 0 deletions .changeset/27238-connector-credentials-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@object-ui/app-shell': patch
'@object-ui/i18n': patch
'@objectstack/service-automation': patch
'@objectstack/platform-objects': patch
---

feat(console): Setup → Integrations → Connector Credentials, and a credential step after a marketplace install

Clause-②: yes (widening: `@objectstack/service-automation` exports `CONNECTOR_CREDENTIALS_UI_BUNDLE`, `CONNECTOR_CREDENTIALS_COMPONENT_REF` and `CONNECTOR_CREDENTIALS_NAV_CAPABILITY`, and registers a new Setup navigation entry `nav_connector_credentials` resolving component ref `automation:connector-credentials`; no existing key, route or shape changes)
Metadata-protocol: none

An administrator can now set a connector credential without the API or the server environment.

- **Setup → Integrations → Connector Credentials** lists every reference the credential routes list (`GET /api/v1/automation/connectors/credential-refs`): its status (`set`, `missing`, `rejected` with the upstream's reason), the connectors naming it, and when and by whom it last changed. Each writable reference has a masked input that starts empty, whatever is stored; an empty input means "keep the current value", so nothing is sent for it. Saving sends `PUT` and the row takes the entry the server answers after its reconcile. A reference with a value stored here has **Clear**, behind a confirmation, which sends `DELETE`. A reference an environment variable supplies is read-only.
- Where the routes refuse (`403` for a missing capability or a multi-organization tenancy posture, `501`, `503`), the page shows the server's own sentence as a configuration notice, and a server without the routes (`404` / `405`) gets "this server does not manage connector credentials". Neither is drawn as an error.
- **After a marketplace install** into this runtime — install-local, an org package installed here, or a cloud install once its app is served — the install result lists the credentials the package's connectors still need (the same listing, filtered by `connectors[].packageId`), each with the same masked input and **Skip for now**. A skipped reference stays listed with a link to the Setup page. The step comes after the install because the routes refuse a reference no installed package declares (`404`); the connectors materialize as soon as a credential is set, with no restart. Where the listing cannot be read, the install answer's `credentialNote` is shown instead.
- The navigation entry is contributed by `@objectstack/service-automation` at `kernel:ready` through the manifest service, so it appears only where the automation service runs, and only for a viewer holding `manage_metadata`. The label is translated in the four platform locales; the page and the step are translated in all ten Console packs.
19 changes: 18 additions & 1 deletion content/docs/automation/connectors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,24 @@ that names the reference, and a dispatch to it fails with the same reason.

#### Setting a credential

Three routes, each requiring the `manage_metadata` capability:
**In the Console**, an administrator sets them in **Setup → Integrations →
Connector Credentials**. The page lists every reference with its status, the
connectors that name it, and when it last changed. Each writable reference has
a masked input that starts empty, whatever is stored: typing a value and saving
stores it, and leaving the input empty keeps the current one. A reference with
a value stored here also has **Clear**. One that an environment variable
supplies is read-only, since the variable wins, and a `rejected` one shows the
upstream's reason. Where the routes below refuse (a multi-organization
environment, or a server without them), the page shows the server's sentence
instead of a list.

Installing an app from the marketplace ends on the same inputs: once the
install lands, the credentials its connectors still need are listed, each with
**Skip for now**. A skipped one leaves its connectors degraded until it is set
on the Setup page. The step comes after the install, not before it, because the
routes below refuse a reference that no installed app declares.

Over the API, three routes, each requiring the `manage_metadata` capability:

```http
GET /api/v1/automation/connectors/credential-refs
Expand Down
21 changes: 12 additions & 9 deletions content/docs/deployment/marketplace-handbook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,12 @@ administrator, not for a contributor:
install looks like.
- **Every credential it needs** — for each one, its name, where the
administrator obtains it, and which permissions or scopes it must carry. You
write the names; the administrator supplies the values. The platform does not
prompt for credentials at install yet: the install-time prompt and the
credential store arrive with
[#27237](https://github.com/objectstack-ai/objectstack/issues/27237) and the
Console credentials page with
[#27238](https://github.com/objectstack-ai/objectstack/issues/27238). Until
then the README is the whole hand-off. An app that ships declarative
write the names; the administrator supplies the values. Once the app is
installed, the Console lists the credentials its connectors still need in the
install result, and **Setup → Integrations → Connector Credentials** holds
every one of them ([Setting a credential](/docs/automation/connectors#setting-a-credential)).
The README is still where the administrator learns what each value is and
where to get it. An app that ships declarative
connectors gains an offline spec-normalization tool with
[#26709](https://github.com/objectstack-ai/objectstack/issues/26709).

Expand Down Expand Up @@ -203,8 +202,12 @@ the detail.
4. **The dependency check.** An app that declares a dependency the environment
lacks is refused before anything is written, one line per unmet entry
([App dependencies](/docs/deployment/app-dependencies)).
5. **Credentials.** No screen yet — the administrator follows your README (see
above, and #27237 / #27238).
5. **Credentials.** Once the install lands, its result lists the credentials
the app's connectors still need, each with a masked input and **Skip for
now**. A skipped one leaves its connectors disconnected until it is set in
**Setup → Integrations → Connector Credentials**
([Setting a credential](/docs/automation/connectors#setting-a-credential)).
Your README says where each value comes from (see above).
6. **Sample data.** The **⋯** (More install options) menu on the app's page
offers **Add sample data**, then **Re-seed sample data**, and **Purge sample
data**, which deletes only the records your seed created. Behind them are
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/scripts/check-app-nav-i18n.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,18 @@ const CONTRIBUTORS = [
return { manifests: [CONNECT_AGENT_UI_BUNDLE] };
},
},
{
// `nav_connector_credentials` (Setup → Integrations). Registered by
// `AutomationServicePlugin` at `kernel:ready`, which the shared context
// below never fires, so the bundle the plugin registers is read directly —
// the `@objectstack/mcp` row's shape.
source: '@objectstack/service-automation (CONNECTOR_CREDENTIALS_UI_BUNDLE)',
apps: ['setup'],
async load() {
const { CONNECTOR_CREDENTIALS_UI_BUNDLE } = await import('@objectstack/service-automation');
return { manifests: [CONNECTOR_CREDENTIALS_UI_BUNDLE] };
},
},
{
source: '@objectstack/cloud-connection (cloud + marketplace UI bundles)',
apps: ['setup'],
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 @@ -155,6 +155,7 @@ export const en: TranslationData = {
nav_webhooks: { label: 'Webhooks' },
nav_http_deliveries: { label: 'HTTP Deliveries' },
nav_datasources: { label: 'Datasources' },
nav_connector_credentials: { label: 'Connector Credentials' },

// 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 @@ -106,6 +106,7 @@ export const esES: TranslationData = {
nav_webhooks: { label: 'Webhooks' },
nav_http_deliveries: { label: 'Entregas HTTP' },
nav_datasources: { label: 'Fuentes de datos' },
nav_connector_credentials: { label: 'Credenciales 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 @@ -106,6 +106,7 @@ export const jaJP: TranslationData = {
nav_webhooks: { label: 'Webhooks' },
nav_http_deliveries: { label: 'HTTP 配信' },
nav_datasources: { label: 'データソース' },
nav_connector_credentials: { 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 @@ -116,6 +116,7 @@ export const zhCN: TranslationData = {
nav_webhooks: { label: 'Webhooks' },
nav_http_deliveries: { label: 'HTTP 投递' },
nav_datasources: { label: '数据源' },
nav_connector_credentials: { label: '连接器凭据' },

nav_oauth_apps: { label: 'OAuth 应用' },
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* [#27238] The Setup entry for the connector credential door (ruling A on
* #27230) ships with the door: `CONNECTOR_CREDENTIALS_UI_BUNDLE`, registered
* through the `manifest` service at `kernel:ready` (cloud ADR-0009 shape).
*
* • the bundle is a valid manifest whose one contribution puts a component
* 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;
* • 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,
* and that boot does not fail.
*/
import { describe, expect, it } from 'vitest';
import { LiteKernel } from '@objectstack/core';
import { ManifestSchema } from '@objectstack/spec/kernel';

import { AutomationServicePlugin } from './plugin.js';
import {
CONNECTOR_CREDENTIALS_COMPONENT_REF,
CONNECTOR_CREDENTIALS_NAV_CAPABILITY,
CONNECTOR_CREDENTIALS_UI_BUNDLE,
} 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', () => {
expect(CONNECTOR_CREDENTIALS_COMPONENT_REF).toBe('automation:connector-credentials');
expect(CONNECTOR_CREDENTIALS_NAV_CAPABILITY).toBe('manage_metadata');
expect(CONNECTOR_CREDENTIALS_UI_BUNDLE.navigationContributions).toEqual([
{
app: 'setup',
group: 'group_integrations',
priority: 120,
items: [
{
id: 'nav_connector_credentials',
type: 'component',
label: 'Connector Credentials',
componentRef: 'automation:connector-credentials',
icon: 'key-round',
requiredPermissions: ['manage_metadata'],
},
],
},
]);
});
});

/** Boot a kernel whose `manifest` service (when given) records every registration. */
async function boot(opts: { withManifest: boolean; armRuntime?: boolean }) {
const registered: Array<{ id?: string }> = [];
const kernel = new LiteKernel({ logger: { level: 'silent' } } as never);
// Registered FIRST, so the manifest service exists before the automation
// plugin's init() — the order a real composition gives it.
kernel.use({
name: 'test.manifest',
type: 'standard',
version: '1.0.0',
dependencies: [] as string[],
async init(ctx: any) {
if (opts.withManifest) ctx.registerService('manifest', { register: (m: { id?: string }) => { registered.push(m); } });
},
async start() {},
} as never);
kernel.use(new AutomationServicePlugin(opts.armRuntime === false ? { armRuntime: false } : {}));
await kernel.bootstrap();
return { kernel, ids: () => registered.map((m) => m.id) };
}

describe('[#27238] the automation plugin registers the bundle at kernel:ready', () => {
it('a booted kernel registers it exactly once', async () => {
const { kernel, ids } = await boot({ withManifest: true });
try {
expect(ids().filter((id) => id === CONNECTOR_CREDENTIALS_UI_BUNDLE.id)).toHaveLength(1);
} finally {
await kernel.shutdown();
}
});

it('an inert boot registers no entry', async () => {
const { kernel, ids } = await boot({ withManifest: true, armRuntime: false });
try {
expect(ids()).not.toContain(CONNECTOR_CREDENTIALS_UI_BUNDLE.id);
} finally {
await kernel.shutdown();
}
});

it('a kernel with no manifest service boots, with no entry to carry', async () => {
const { kernel, ids } = await boot({ withManifest: false });
try {
expect(ids()).toEqual([]);
} finally {
await kernel.shutdown();
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Setup → Integrations → "Connector Credentials" — the navigation entry of the
* connector credential door, carried by the automation service that owns the
* door (ruling A on #27230; cloud ADR-0009 shape, as `packages/cloud-connection`
* restates it: the nav entry lives and dies with the capability — no automation
* service, no entry).
*
* The page body is the console's `automation:connector-credentials` component
* (`@object-ui/app-shell`), the way Setup's packaged-automation entry names
* `automation:packaged`. It reads `GET /api/v1/automation/connectors/credential-refs`
* and writes through `PUT` / `DELETE …/credential-refs/:ref`; ⛔ no value ever
* reaches it, since none of the three routes answers one.
*
* Registered by {@link AutomationServicePlugin} at `kernel:ready`. The entry
* 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.
*/

/** The console component the entry opens. */
export const CONNECTOR_CREDENTIALS_COMPONENT_REF = 'automation:connector-credentials';

/** The capability the credential door asks on all three routes. */
export const CONNECTOR_CREDENTIALS_NAV_CAPABILITY = 'manage_metadata';

/**
* `priority` orders contributions within a group, lowest first. The group's
* other contributors, read where they are declared: Webhooks / HTTP Deliveries
* (`@objectstack/plugin-webhooks`) and Datasources
* (`@objectstack/service-datasource`) at 100, Connect an Agent
* (`@objectstack/mcp`) at 110, Cloud Connection (`@objectstack/cloud-connection`)
* at 200 — so 120 sorts this entry after the connection surfaces it serves.
*/
export const CONNECTOR_CREDENTIALS_UI_BUNDLE = {
id: 'com.objectstack.service-automation.connector-credentials-ui',
namespace: 'sys',
version: '0.1.0',
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.',
navigationContributions: [
{
app: 'setup',
group: 'group_integrations',
priority: 120,
items: [
{
id: 'nav_connector_credentials',
type: 'component',
label: 'Connector Credentials',
componentRef: CONNECTOR_CREDENTIALS_COMPONENT_REF,
icon: 'key-round',
requiredPermissions: [CONNECTOR_CREDENTIALS_NAV_CAPABILITY],
},
],
},
],
};
9 changes: 9 additions & 0 deletions packages/services/service-automation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ export { InMemoryFlowDispatchStore, ObjectStoreFlowDispatchStore } from './flow-
export type { FlowDispatchStoreEngine } from './flow-dispatch-store.js';
export { SysFlowDispatch } from './sys-flow-dispatch.object.js';

// [#27238] The Setup → Integrations entry of the connector credential door,
// registered by AutomationServicePlugin at `kernel:ready` and exported for the
// composition-root census of platform-app navigation (`check:app-nav-i18n`).
export {
CONNECTOR_CREDENTIALS_UI_BUNDLE,
CONNECTOR_CREDENTIALS_COMPONENT_REF,
CONNECTOR_CREDENTIALS_NAV_CAPABILITY,
} from './connector-credentials-ui.js';

// [#20790] The write-only flow credential channel: where a flow's inbound hook
// secret and http signing secrets live once they are out of its definition —
// the object, the channel the plugin registers on the metadata save door, and
Expand Down
22 changes: 22 additions & 0 deletions packages/services/service-automation/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
} from './connector-credentials.js';
import { CONNECTOR_CREDENTIAL_SOURCE_SERVICE } from '@objectstack/types';
import { createConnectorCredentialSource } from './connector-credential-source.js';
import { CONNECTOR_CREDENTIALS_UI_BUNDLE } from './connector-credentials-ui.js';
import {
FlowCredentialChannel,
storedFlowStates,
Expand Down Expand Up @@ -951,6 +952,20 @@ export class AutomationServicePlugin implements Plugin {
}
}

/**
* [#27238] Register {@link CONNECTOR_CREDENTIALS_UI_BUNDLE} — the Setup
* entry for the connector credential door this plugin attaches in
* `init()`. A composition with no `manifest` service (a bare kernel, a
* test) serves no console, so it gets no entry and nothing is lost.
*/
private registerConnectorCredentialsUi(ctx: PluginContext): void {
try {
ctx.getService<{ register(m: unknown): void }>('manifest').register(CONNECTOR_CREDENTIALS_UI_BUNDLE);
} catch {
/* no manifest service: no console to carry the entry */
}
}

/**
* [#20790] The data engine the credential channel, its index and the
* one-time move read and write through — ObjectQL, resolved at call time
Expand Down Expand Up @@ -1764,6 +1779,13 @@ export class AutomationServicePlugin implements Plugin {
}
});

// [#27238] Setup → Integrations → Connector Credentials — the door's
// nav entry ships with the door (cloud ADR-0009 shape). After the
// inert-mode return above: a one-shot tool serves no console.
ctx.hook('kernel:ready', async () => {
this.registerConnectorCredentialsUi(ctx);
});

// ── Runtime re-bind: a flow saved, published or deleted at runtime ────
// [#21725] The protocol's mutation signal, which every metadata write
// raises — the same one ObjectQL re-binds authored hooks and actions
Expand Down
Loading
Loading