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
24 changes: 24 additions & 0 deletions .changeset/26710-signed-connector-auth-tencentcloud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@objectstack/spec': minor
'@objectstack/service-automation': minor
---

feat(spec): `tencentcloud-tc3-hmac-sha256` joins the `signed` connector-instance auth schemes (ADR-0143 D2.3, #26710)

Clause-②: yes (widening)
Metadata-protocol: added

<!-- adr-0087: not-required (no-migration-prescription) One member is added to the closed signing-scheme list and one shape to the `signed` union; the signer seam's `scheme` member is typed as that list instead of any string; that interface is in no release yet (it ships with the pending `26710-signed-connector-auth-aliyun` changeset), so no published surface narrows. Nothing an author wrote before is refused or re-shaped, so there is nothing for a migration to rewrite. -->

A declarative connector instance can now declare Tencent Cloud's API 3.0 signature (v3) as its signing scheme. It joins Alibaba Cloud's V3 signature, which shipped with the `signed` kind.

- **`@objectstack/spec`**
- `ConnectorSigningSchemeSchema` gains `tencentcloud-tc3-hmac-sha256`.
- The `signed` union gains its closed shape, `{ type: 'signed', scheme: 'tencentcloud-tc3-hmac-sha256', keyId, credentialRef }`. `keyId` is the SecretId. The SecretKey is only ever a `credentialRef`.
- The same publish refusals apply as for the first scheme: an inline key spelling by name, an authored `signature`, and `signed` on `provider: 'mcp'`. `secretId` is answered with `keyId`. An unknown scheme's refusal now lists both shipped schemes.
- `ConnectorRequestSigner.scheme` is typed as `ConnectorSigningScheme` rather than `string`. The interface ships for the first time in this release, together with the first scheme.
- **`@objectstack/service-automation`**
- The signer registry binds a `tencentcloud-tc3-hmac-sha256` instance's key to its TC3-HMAC-SHA256 signer.
- Every attempt is signed with a fresh `X-TC-Timestamp`, over `content-type` and `host`. The scope's service is the first label of the endpoint host.
- A request without a content type is signed and sent as `application/x-www-form-urlencoded`.
- The key never leaves the host.
22 changes: 20 additions & 2 deletions content/docs/automation/connectors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -483,12 +483,21 @@ auth: {
keyId: 'YourAccessKeyId', // the AccessKey ID: public, stays in metadata
credentialRef: 'ALIYUN_ACCESS_KEY_SECRET',
}

// Tencent Cloud API 3.0, signature v3 (TC3-HMAC-SHA256):
auth: {
type: 'signed',
scheme: 'tencentcloud-tc3-hmac-sha256',
keyId: 'YourSecretId', // the SecretId: public, stays in metadata
credentialRef: 'TENCENT_SECRET_KEY',
}
```

What the platform does with it:

- **Every request is signed, each attempt fresh.** A retry is signed again with
a new nonce and date, so a scheme that refuses a replayed nonce accepts it.
a new nonce and date, so a scheme that refuses a replayed nonce or a stale
timestamp accepts it.
- **The key never reaches the provider.** The automation service binds the
resolved key to the scheme's signer at materialization and hands the
provider the signer only. A datasource cannot borrow a `signed` instance
Expand All @@ -501,6 +510,14 @@ What the platform does with it:
`x-acs-content-sha256` and `Authorization`. The API's own action and version
are yours to send, as `x-acs-action` / `x-acs-version` in `defaultHeaders`
(rest) or in each call's headers.
- **`tencentcloud-tc3-hmac-sha256`** signs `content-type` and `host`, and sets
`X-TC-Timestamp` and `Authorization`. The service in the signature's scope is
the first label of the endpoint host (`cvm.tencentcloudapi.com`, or a
regional `cvm.ap-guangzhou.tencentcloudapi.com`, is `cvm`), so point
`baseUrl` at the product's own endpoint. `X-TC-Action`, `X-TC-Version` and
`X-TC-Region` are yours to send and are not signed. A request without a body
is sent with `Content-Type: application/x-www-form-urlencoded`, the type it
is signed with.

What the publish door refuses:

Expand All @@ -509,7 +526,8 @@ What the publish door refuses:
signer and its test vectors, so a scheme the server cannot run is never
accepted.
- **An inline key**: `secret`, `privateKey`, `secretKey` or `accessKeySecret`
is refused by name, pointing at `credentialRef`. An authored `signature` is
is refused by name, pointing at `credentialRef`. The vendors' words for the
key id (`accessKeyId`, `secretId`) are answered with `keyId`. An authored `signature` is
refused too: it is computed for every request.
- **`provider: 'mcp'`**, at `auth.type`: an MCP server is not signed per
request. Declare `provider: 'rest'` or `provider: 'openapi'`.
Expand Down
10 changes: 5 additions & 5 deletions content/docs/references/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Protocol reference — every schema by module
navTitle: Protocol Reference
description: Every schema published by @objectstack/spec — 1569 schemas across 14 protocol modules
description: Every schema published by @objectstack/spec — 1570 schemas across 14 protocol modules
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
Expand All @@ -25,7 +25,7 @@ counts are sums of the rows they head. Regenerate with
| [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
| [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
| [Integration Protocol](/docs/references/integration) | 1 | 21 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
| [Integration Protocol](/docs/references/integration) | 1 | 22 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
| [Kernel Protocol](/docs/references/kernel) | 29 | 151 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
| [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. |
| [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. |
Expand All @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 172 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **203** | **1569** | 14 protocol modules |
| **Total** | **203** | **1570** | 14 protocol modules |

---

Expand Down Expand Up @@ -191,13 +191,13 @@ Users and accounts, organizations, positions, SCIM provisioning.

## Integration Protocol

**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 21 schemas**
**Source:** `packages/spec/src/integration/` · **Import:** `@objectstack/spec/integration` · **1 page, 22 schemas**

The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances.

| File | Schemas |
| :--- | :--- |
| [`connector.zod.ts`](/docs/references/integration/connector) | `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorAppCredentialVendor`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAppCredentialAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorInstanceOAuth2Auth`, `ConnectorInstanceOAuth2AuthorizationCodeAuth`, `ConnectorInstanceOAuth2ClientCredentialsAuth`, `ConnectorInstanceSignedAliyunAcs3Auth`, `ConnectorInstanceSignedAuth`, `ConnectorOAuth2Grant`, `ConnectorRetryStrategy`, `ConnectorSigningScheme`, `ConnectorType`, `DeclarativeConnectorEntry`, `RetryConfig` |
| [`connector.zod.ts`](/docs/references/integration/connector) | `Connector`, `ConnectorAction`, `ConnectorActionEffect`, `ConnectorAppCredentialVendor`, `ConnectorInstanceAPIKeyAuth`, `ConnectorInstanceAppCredentialAuth`, `ConnectorInstanceAuth`, `ConnectorInstanceBasicAuth`, `ConnectorInstanceBearerAuth`, `ConnectorInstanceNoAuth`, `ConnectorInstanceOAuth2Auth`, `ConnectorInstanceOAuth2AuthorizationCodeAuth`, `ConnectorInstanceOAuth2ClientCredentialsAuth`, `ConnectorInstanceSignedAliyunAcs3Auth`, `ConnectorInstanceSignedAuth`, `ConnectorInstanceSignedTencentCloudTc3Auth`, `ConnectorOAuth2Grant`, `ConnectorRetryStrategy`, `ConnectorSigningScheme`, `ConnectorType`, `DeclarativeConnectorEntry`, `RetryConfig` |

---

Expand Down
60 changes: 54 additions & 6 deletions content/docs/references/integration/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ an exception for itself.)
## TypeScript Usage

```typescript
import { ConnectorSchema, ConnectorActionSchema, ConnectorActionEffectSchema, ConnectorAppCredentialVendorSchema, ConnectorInstanceAPIKeyAuthSchema, ConnectorInstanceAppCredentialAuthSchema, ConnectorInstanceAuthSchema, ConnectorInstanceBasicAuthSchema, ConnectorInstanceBearerAuthSchema, ConnectorInstanceNoAuthSchema, ConnectorInstanceOAuth2AuthSchema, ConnectorInstanceOAuth2AuthorizationCodeAuthSchema, ConnectorInstanceOAuth2ClientCredentialsAuthSchema, ConnectorInstanceSignedAliyunAcs3AuthSchema, ConnectorInstanceSignedAuthSchema, ConnectorOAuth2GrantSchema, ConnectorRetryStrategySchema, ConnectorSigningSchemeSchema, ConnectorTypeSchema, DeclarativeConnectorEntrySchema, RetryConfigSchema } from '@objectstack/spec/integration';
import type { Connector, ConnectorAction, ConnectorActionEffect, ConnectorAppCredentialVendor, ConnectorInstanceAPIKeyAuth, ConnectorInstanceAppCredentialAuth, ConnectorInstanceAuth, ConnectorInstanceBasicAuth, ConnectorInstanceBearerAuth, ConnectorInstanceNoAuth, ConnectorInstanceOAuth2Auth, ConnectorInstanceOAuth2AuthorizationCodeAuth, ConnectorInstanceOAuth2ClientCredentialsAuth, ConnectorInstanceSignedAliyunAcs3Auth, ConnectorInstanceSignedAuth, ConnectorOAuth2Grant, ConnectorRetryStrategy, ConnectorSigningScheme, ConnectorType, DeclarativeConnectorEntry, RetryConfig } from '@objectstack/spec/integration';
import { ConnectorSchema, ConnectorActionSchema, ConnectorActionEffectSchema, ConnectorAppCredentialVendorSchema, ConnectorInstanceAPIKeyAuthSchema, ConnectorInstanceAppCredentialAuthSchema, ConnectorInstanceAuthSchema, ConnectorInstanceBasicAuthSchema, ConnectorInstanceBearerAuthSchema, ConnectorInstanceNoAuthSchema, ConnectorInstanceOAuth2AuthSchema, ConnectorInstanceOAuth2AuthorizationCodeAuthSchema, ConnectorInstanceOAuth2ClientCredentialsAuthSchema, ConnectorInstanceSignedAliyunAcs3AuthSchema, ConnectorInstanceSignedAuthSchema, ConnectorInstanceSignedTencentCloudTc3AuthSchema, ConnectorOAuth2GrantSchema, ConnectorRetryStrategySchema, ConnectorSigningSchemeSchema, ConnectorTypeSchema, DeclarativeConnectorEntrySchema, RetryConfigSchema } from '@objectstack/spec/integration';
import type { Connector, ConnectorAction, ConnectorActionEffect, ConnectorAppCredentialVendor, ConnectorInstanceAPIKeyAuth, ConnectorInstanceAppCredentialAuth, ConnectorInstanceAuth, ConnectorInstanceBasicAuth, ConnectorInstanceBearerAuth, ConnectorInstanceNoAuth, ConnectorInstanceOAuth2Auth, ConnectorInstanceOAuth2AuthorizationCodeAuth, ConnectorInstanceOAuth2ClientCredentialsAuth, ConnectorInstanceSignedAliyunAcs3Auth, ConnectorInstanceSignedAuth, ConnectorInstanceSignedTencentCloudTc3Auth, ConnectorOAuth2Grant, ConnectorRetryStrategy, ConnectorSigningScheme, ConnectorType, DeclarativeConnectorEntry, RetryConfig } from '@objectstack/spec/integration';

// Validate data
const result = ConnectorSchema.parse(data);
Expand Down Expand Up @@ -293,7 +293,7 @@ const result = ConnectorSchema.parse(data);
| **appId** | `string` | ✅ | The vendor's app identifier (not a secret): Feishu `app_id`, DingTalk `appKey`, WeCom `corpid`. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the app secret (WeCom: the application `corpsecret`). Never an inline secret. |

### Nested Shape: `Connector.auth[option 7]`
### Nested Shape: `Connector.auth[option 7][scheme='aliyun-acs3-hmac-sha256']`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
Expand All @@ -302,6 +302,15 @@ const result = ConnectorSchema.parse(data);
| **keyId** | `string` | ✅ | The AccessKey ID (not a secret); sent in the `Credential` of every signature. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the AccessKey secret, the HMAC key every request is signed with. Never an inline secret. |

### Nested Shape: `Connector.auth[option 7][scheme='tencentcloud-tc3-hmac-sha256']`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `'signed'` | ✅ | |
| **scheme** | `'tencentcloud-tc3-hmac-sha256'` | ✅ | Tencent Cloud API 3.0 signature v3 (TC3-HMAC-SHA256). |
| **keyId** | `string` | ✅ | The SecretId (not a secret); sent in the `Credential` of every signature. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the SecretKey every request's signing key is derived from. Never an inline secret. |

### Nested Shape: `Connector.actions[number]`

| Property | Type | Required | Description |
Expand Down Expand Up @@ -481,7 +490,7 @@ Type: `{ type: 'oauth2'; tokenUrl?: string; scopes?: string[]; resource?: string

#### Option 7

Type: `{ type: 'signed'; scheme: 'aliyun-acs3-hmac-sha256'; keyId: string; credentialRef: string }`
Type: `{ type: 'signed'; scheme: 'aliyun-acs3-hmac-sha256'; keyId: string; credentialRef: string } | { type: 'signed'; scheme: 'tencentcloud-tc3-hmac-sha256'; keyId: string; credentialRef: string }`

---

Expand Down Expand Up @@ -640,6 +649,35 @@ This schema accepts one of the following structures:

---

#### Option 2

**Type:** `signed`

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `'signed'` | ✅ | |
| **scheme** | `'tencentcloud-tc3-hmac-sha256'` | ✅ | Tencent Cloud API 3.0 signature v3 (TC3-HMAC-SHA256). |
| **keyId** | `string` | ✅ | The SecretId (not a secret); sent in the `Credential` of every signature. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the SecretKey every request's signing key is derived from. Never an inline secret. |

---


---

## ConnectorInstanceSignedTencentCloudTc3Auth

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `'signed'` | ✅ | |
| **scheme** | `'tencentcloud-tc3-hmac-sha256'` | ✅ | Tencent Cloud API 3.0 signature v3 (TC3-HMAC-SHA256). |
| **keyId** | `string` | ✅ | The SecretId (not a secret); sent in the `Credential` of every signature. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the SecretKey every request's signing key is derived from. Never an inline secret. |


---

Expand Down Expand Up @@ -671,11 +709,12 @@ Retry strategy

## ConnectorSigningScheme

Request-signing scheme: `aliyun-acs3-hmac-sha256` (Alibaba Cloud OpenAPI V3 signature).
Request-signing scheme: `aliyun-acs3-hmac-sha256` (Alibaba Cloud OpenAPI V3 signature) or `tencentcloud-tc3-hmac-sha256` (Tencent Cloud API 3.0 signature v3).

### Allowed Values

* `aliyun-acs3-hmac-sha256`
* `tencentcloud-tc3-hmac-sha256`


---
Expand Down Expand Up @@ -815,7 +854,7 @@ Connector type
| **appId** | `string` | ✅ | The vendor's app identifier (not a secret): Feishu `app_id`, DingTalk `appKey`, WeCom `corpid`. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the app secret (WeCom: the application `corpsecret`). Never an inline secret. |

### Nested Shape: `DeclarativeConnectorEntry.auth[option 7]`
### Nested Shape: `DeclarativeConnectorEntry.auth[option 7][scheme='aliyun-acs3-hmac-sha256']`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
Expand All @@ -824,6 +863,15 @@ Connector type
| **keyId** | `string` | ✅ | The AccessKey ID (not a secret); sent in the `Credential` of every signature. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the AccessKey secret, the HMAC key every request is signed with. Never an inline secret. |

### Nested Shape: `DeclarativeConnectorEntry.auth[option 7][scheme='tencentcloud-tc3-hmac-sha256']`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `'signed'` | ✅ | |
| **scheme** | `'tencentcloud-tc3-hmac-sha256'` | ✅ | Tencent Cloud API 3.0 signature v3 (TC3-HMAC-SHA256). |
| **keyId** | `string` | ✅ | The SecretId (not a secret); sent in the `Credential` of every signature. |
| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the SecretKey every request's signing key is derived from. Never an inline secret. |

### Nested Shape: `DeclarativeConnectorEntry.actions[number]`

| Property | Type | Required | Description |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.

| Dir | Sites |
|---|---|
| `shared/` | 24 |
| `shared/` | 25 |
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,55 @@ describe('ConnectorRestPlugin — a declarative `signed` instance (ADR-0143 D2.3

await kernel.shutdown();
});

it('signs a tencentcloud-tc3-hmac-sha256 instance the same way, with the service named by its host', async () => {
const KEY = 'dummy-tencent-secret-key-26710';
const calls: Array<{ url: string; headers: Record<string, string> }> = [];
const impl = (async (url: string, init: RequestInit) => {
calls.push({ url, headers: init.headers as Record<string, string> });
return new Response('{"Response":{"RequestId":"r-1"}}', { status: 200, headers: { 'content-type': 'application/json' } });
}) as unknown as typeof fetch;
const declared = [{
name: 'tencent_cvm',
label: 'Tencent CVM',
type: 'api',
provider: 'rest',
providerConfig: { baseUrl: 'https://cvm.tencentcloudapi.com', defaultHeaders: { 'X-TC-Version': '2017-03-12', 'X-TC-Region': 'ap-guangzhou' } },
auth: { type: 'signed', scheme: 'tencentcloud-tc3-hmac-sha256', keyId: 'test-secret-id', credentialRef: 'TENCENT_SECRET_26710' },
}];
const kernel = new LiteKernel();
kernel.use(new AutomationServicePlugin({ credentialResolver: (r) => (r === 'TENCENT_SECRET_26710' ? KEY : undefined) }));
kernel.use(new ConnectorRestPlugin({ fetchImpl: impl }));
kernel.use({
name: 'test.metadata',
type: 'standard',
version: '1.0.0',
dependencies: [],
async init(ctx: any) {
ctx.registerService('objectql', { registry: { listItems: (t: string) => (t === 'connector' ? declared : []) } });
},
async start() {},
} as never);
await kernel.bootstrap();
const engine = kernel.getService<AutomationEngine>('automation');
expect(engine.getConnectorDegradedReason('tencent_cvm')).toBeUndefined();

const request = engine.resolveConnectorAction('tencent_cvm', 'request')!;
const answered = await request({ method: 'POST', path: '/', body: { Limit: 1 }, headers: { 'X-TC-Action': 'DescribeInstances' } }, {} as never);
expect(answered).toEqual({ status: 200, ok: true, body: { Response: { RequestId: 'r-1' } } });

expect(calls).toHaveLength(1);
const headers = calls[0]!.headers;
expect(headers.authorization).toMatch(
/^TC3-HMAC-SHA256 Credential=test-secret-id\/\d{4}-\d{2}-\d{2}\/cvm\/tc3_request, SignedHeaders=content-type;host, Signature=[0-9a-f]{64}$/,
);
expect(headers['x-tc-timestamp']).toMatch(/^\d{10}$/);
expect(headers['content-type']).toBe('application/json');
expect(headers['x-tc-action']).toBe('DescribeInstances');

expect(JSON.stringify(calls)).not.toContain(KEY);
expect(JSON.stringify(engine.getConnectorDescriptors())).not.toContain(KEY);

await kernel.shutdown();
});
});
Loading
Loading