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
14 changes: 14 additions & 0 deletions .changeset/28226-credential-literal-explain-grant-kinds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/lint': patch
---

docs(lint): `os explain flow-credential-literal` names the `oauth2`, `app-credential` and `signed` connector auth kinds, and says why its fix line never routes to them

Clause-②: no
Metadata-protocol: none

The connector auth union now declares `oauth2` (grant `authorization_code` or `client_credentials`), `app-credential` (vendor `feishu`, `dingtalk` or `wecom`) and `signed` beside the static kinds. The explanation said only that the fix line routes to a variant the schema "really has", which read as if `bearer`, `api-key` and `basic` were the whole list.

- The explanation now names the other kinds, says their secret is also only a `credentialRef`, and says none is a home a literal moves into by shape: `oauth2` and `app-credential` carry a grant whose token the server obtains per dispatch, and until it runs that grant such an instance is held degraded and calls no upstream; `signed` signs each request with its key.
- The `fix:` lines are unchanged and still route to `bearer`, `api-key` or `basic`. The rule's predicate and severity are unchanged.
- The rule's test holds every kind and grant the explanation names to `ConnectorInstanceAuthSchema`.
16 changes: 16 additions & 0 deletions packages/lint/src/lint-flow-credential-literals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,9 +518,25 @@ describe('[#22161] one-line fixes — flow-credential-literal', () => {
{ type: 'api-key', headerName: 'X-Api-Key', credentialRef: 'PROBE_REF' },
{ type: 'api-key', paramName: 'api_key', credentialRef: 'PROBE_REF' },
{ type: 'basic', username: 'probe', credentialRef: 'PROBE_REF' },
// The grant-bearing and signing kinds the explanation names, with the
// grants and vendors it lists.
{ type: 'oauth2', grant: 'authorization_code', clientId: 'probe', credentialRef: 'PROBE_REF' },
{ type: 'oauth2', grant: 'client_credentials', clientId: 'probe', credentialRef: 'PROBE_REF' },
...['feishu', 'dingtalk', 'wecom'].map((vendor) => (
{ type: 'app-credential', vendor, appId: 'probe', credentialRef: 'PROBE_REF' })),
{ type: 'signed', scheme: 'aliyun-acs3-hmac-sha256', keyId: 'probe', credentialRef: 'PROBE_REF' },
]) {
expect(ConnectorInstanceAuthSchema.safeParse(auth).success, JSON.stringify(auth)).toBe(true);
}
const explanation = explainRule(FLOW_CREDENTIAL_LITERAL)!.paragraphs.join('\n');
for (const fact of [
"`oauth2` (grant `authorization_code` or `client_credentials`)",
"`app-credential` (vendor `feishu`, `dingtalk` or `wecom`)",
'held degraded and calls no upstream',
'`signed` signs each request',
]) {
expect(explanation, `explanation names ${fact}`).toContain(fact);
}
// The connector-input line offers every secret-bearing variant by name.
const input = lintFlowCredentialLiteralsUnrecorded(controlStack()).find((f) =>
f.message.startsWith('connector input '),
Expand Down
10 changes: 6 additions & 4 deletions packages/lint/src/lint-flow-credential-literals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ const SERVED = 'readable by every member who can read flows';
* prints it only under `--fix`, so a route stated only in the hint misses the
* author who runs `os lint`.
*
* The hint carries the route BY SHAPE, to a variant `ConnectorInstanceAuthSchema`
* really has (`none` / `bearer` / `api-key` / `basic`): a header credential goes
* to `bearer` or a header `api-key`, a query-string key to `api-key` with
* `paramName`. ⛔ No variant carries a secret in a url PATH, so no text here
* The hint carries the route BY SHAPE, to a static variant
* `ConnectorInstanceAuthSchema` really has (`bearer` / `api-key` / `basic`): a
* header credential goes to `bearer` or a header `api-key`, a query-string key
* to `api-key` with `paramName`. Its other kinds (`oauth2`, `app-credential`,
* `signed`) carry a grant or a signing key, not a value a literal moves into by
* shape, so no hint routes to them; the explanation names them. ⛔ No variant carries a secret in a url PATH, so no text here
* may promise that `credentialRef` can hold one — an author cannot follow it.
* (This rule does not judge url paths; it judges query parameters only.)
*/
Expand Down
7 changes: 6 additions & 1 deletion packages/lint/src/rule-explanations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4706,7 +4706,12 @@ const FLOW_CREDENTIAL_LITERAL_EXPLANATION: RuleExplanation = {
'variable in the open tier) and never stored in metadata (ADR-0097 §3), called from a ' +
'`connector_action` node. The fix line routes by shape, to a variant the connector auth schema ' +
'really has: a header credential to `bearer` or a header `api-key`, a query-string key to ' +
'`api-key` with `paramName`. A credential in `connectorConfig.input` is dropped instead, ' +
'`api-key` with `paramName`. The schema\'s other kinds also take their secret only as a ' +
'`credentialRef`, but none is a home a literal moves into by shape: `oauth2` (grant ' +
'`authorization_code` or `client_credentials`) and `app-credential` (vendor `feishu`, ' +
'`dingtalk` or `wecom`) carry a grant whose token the server obtains per dispatch, and until it ' +
'runs that grant such an instance is held degraded and calls no upstream; `signed` signs each ' +
'request with its key. A credential in `connectorConfig.input` is dropped instead, ' +
'because the connector authenticates through its own `auth`. No variant carries a secret in a ' +
'url path, so a url path is not judged and no fix line promises one.',
// [#22161] What the fix line stopped saying. The variants are held to the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"id": 1,
"prompt": "Our contract-lifecycle app (objects `contract`, `crm_account`) needs DocuSign: send a contract out for signature, read an envelope's status, and reflect status changes on the record. DocuSign publishes a 4.7 MB Swagger 2.0 document with several hundred operations, authenticates with OAuth 2.0, and its Connect webhooks sign posts with their own HMAC header. Propose what to build and where.",
"expected_output": "A declarative instance inside the app, not a platform plugin and not a `connector-esign` family: `defineConnector` with `provider: 'openapi'`, whose `providerConfig.spec` is a packaged OpenAPI 3.x document converted offline from the Swagger 2.0 document and trimmed to the three operations (create envelope, get envelope, list status changes) (or `providerConfig.include` names those three operation keys, the `mcp` provider's allowlist meaning, where a non-array or unknown entry is fatal). Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Two gaps are named plainly, each blocked on its platform card: the declarative `auth` union has no `oauth2`, so the integration is marked **blocked** on outbound OAuth (no short-lived token parked behind a `bearer` `credentialRef`); DocuSign Connect signs its own header, which the `api` door does not verify, so status is polled until vendor schemes are verified. `connectorConfig.input` is a value slot: the per-contract payload reads the record through a CEL envelope (`envelopeId: { dialect: 'cel', source: 'record.envelope_id' }`), judged against the action's `inputSchema` before dispatch. The answer proposes no `registerConnector` code and no plugin.",
"expected_output": "A declarative instance inside the app, not a platform plugin and not a `connector-esign` family: `defineConnector` with `provider: 'openapi'`, whose `providerConfig.spec` is a packaged OpenAPI 3.x document converted offline from the Swagger 2.0 document and trimmed to the three operations (create envelope, get envelope, list status changes) (or `providerConfig.include` names those three operation keys, the `mcp` provider's allowlist meaning, where a non-array or unknown entry is fatal). Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Two gaps are named plainly, each blocked on its platform card: the declarative `auth` union declares `oauth2` (`grant: 'authorization_code'`, the client secret only a `credentialRef`), but no server runs the grant yet, so such an instance is held degraded and calls no upstream: the integration is marked **blocked** on outbound OAuth (no short-lived token parked behind a `bearer` `credentialRef`); DocuSign Connect signs its own header, which the `api` door does not verify, so status is polled until vendor schemes are verified. `connectorConfig.input` is a value slot: the per-contract payload reads the record through a CEL envelope (`envelopeId: { dialect: 'cel', source: 'record.envelope_id' }`), judged against the action's `inputSchema` before dispatch. The answer proposes no `registerConnector` code and no plugin.",
"files": [],
"assertions": {
"must_contain": ["defineConnector", "provider: 'openapi'", "credentialRef", "connector_action", "type: 'api'", "secretRef", "Swagger 2.0", "blocked"],
Expand Down
24 changes: 13 additions & 11 deletions skills/objectstack-automation/references/integration-recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ deployment and to the sender. Scaffolded apps list the three executors in
`mcp`: `{ ok, content, structuredContent? }`), read from a later node's value slot.
- `auth`, each variant with a `credentialRef`: `bearer`; `api-key` (`headerName`,
default `X-API-Key`, or `paramName` for a query-string key); `basic` (`username`
stays in metadata). A secret in a url path has no variant — use a
stays in metadata); `signed` on `openapi` / `rest` (`scheme`
`aliyun-acs3-hmac-sha256` or `tencentcloud-tc3-hmac-sha256`, `keyId`) signs each
request. A secret in a url path has no variant — use a
token-authenticated connector. `authentication: { token }` is refused at `os validate`.
- `defineConnector`: `@objectstack/spec/integration`; `defineFlow`: `@objectstack/spec`; both register in `defineStack`.

## 1 · OpenAPI document, static key — Stripe balance, nightly

```ts
// Trim the vendor's OpenAPI 3.x document to the operations the app calls; its path
// is package-relative.
// Trim the vendor's OpenAPI 3.x document to the operations the app calls.
export const StripeConnector = defineConnector({
name: 'stripe', label: 'Stripe', type: 'api', provider: 'openapi',
providerConfig: { spec: './src/connectors/stripe.trimmed.json' },
Expand Down Expand Up @@ -78,7 +79,7 @@ export const GitHubMcp = defineConnector({
providerConfig: {
transport: { kind: 'http', url: 'https://api.githubcopilot.com/mcp/', // `stdio` spawns a local process:
headers: { 'X-MCP-Readonly': 'true' } }, // denied unless the host allowlists the command
include: ['search_issues'], // tool-name allowlist → the actions
include: ['search_issues'],
},
auth: { type: 'bearer', credentialRef: 'GITHUB_MCP_TOKEN' }, // folded into the request headers
});
Expand Down Expand Up @@ -125,7 +126,7 @@ export const CarrierCallback = defineFlow({
`x-objectstack-signature: t=<unix>,v1=<hex>` — HMAC-SHA256 of `<t>.<raw body>`
under `CARRIER_HOOK_SECRET`. The door has no unsigned form and verifies no other
scheme, so a vendor whose webhook signs its own header cannot post here today:
name the gap and poll its status (recipe 1's shape) until vendor schemes verify.
poll its status (recipe 1's shape) until vendor schemes verify.

## 5 · Vendor with no machine-readable spec — write the document by hand

Expand All @@ -148,9 +149,10 @@ Each `operationId` is a named action in the flow palette and to agents;

## Not yet — declare the rest, mark the integration blocked, write no plugin

- **OAuth-protected vendor** (DocuSign, Salesforce, Google): the declarative `auth`
union has no `oauth2`; the authorization-code / refresh
lifecycle is not delivered in the open tier. Do not park a short-lived access
token behind a `bearer` ref. Blocked on the platform's outbound-OAuth card.
- **Signature-authenticated vendor** (Alipay, WeChat Pay, Aliyun — RSA2 / HMAC
canonical-request signing): no auth kind signs requests today; blocked on the request-signing card.
- **OAuth-protected vendor** (DocuSign, Salesforce, Google): `auth` declares
`oauth2` (`authorization_code` / `client_credentials`) and `app-credential`
(`feishu` / `dingtalk` / `wecom`), but no server runs the grant yet: the
instance is held degraded, calling no upstream. Do not park a short-lived
token behind a `bearer` ref. Blocked on the outbound-OAuth card.
- **RSA-signing vendor** (Alipay, WeChat Pay): no `signed` scheme signs RSA yet;
blocked on the request-signing card.
Loading