Skip to content

Commit d62f8eb

Browse files
huangyiireneclaude
andauthored
feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) (#8078)
* feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990) Driver family (postgres/mysql/mongo/turso): config.password / config.authToken are declared-unwritable (z.never with the refusal prescription) so tsc, the parse and the connection form's format:'password' secret input all stay wired to the datasource secret binder (sys_secret + external.credentialsRef). Former alias spellings (passwd/pwd/token/jwt/auth_token/authtoken) carry the refusal directly via guidance. Connector: DeclarativeConnectorEntrySchema now refuses non-none authentication on EVERY authored entry — catalog descriptors included (was: provider-bound instances only, ADR-0097 §3). Runtime registerConnector keeps the resolved inline shape. Ledger: 4 retired-keys entries + 2 D3 semantic entries (no D2 conversion — a cleartext credential cannot be mechanically rewritten into an encrypted sys_secret row); regenerated registry, spec-changes, upgrade guide, authorable surface ([RETIRED] flips), api-surface, export-origins, reference docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * test(spec): family refusal pins + fixture triage; changeset; showcase descriptor migration (#7990) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * chore(spec): regenerate artifact projections on the merged tree (os-regen step 4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * merge origin/main (os-regen artifacts taken from main; regeneration follows) * chore(spec): regenerate artifact projections on the merged tree (os-regen step 4, second lap) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c2e197e commit d62f8eb

32 files changed

Lines changed: 699 additions & 79 deletions
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/example-showcase": patch
4+
---
5+
6+
feat(spec)!: refuse inline credentials at publish — driver `config.password` / `config.authToken` and connector `authentication` on authored entries (#7990)
7+
8+
`sys_metadata.metadata` is served back by the ordinary data API, and a datasource or
9+
connector artefact is persisted whole — so any schema that *accepted* an inline
10+
credential stored that credential in cleartext at rest. The maintainer-ruled fix
11+
(#7990, Option A: per-artefact contract closure) makes the two measured surfaces
12+
refuse the inline form at publish and divert to the mechanisms that already exist.
13+
14+
**Driver config (postgres / mysql / mongo / turso).** `config.password` (SQL/mongo)
15+
and `config.authToken` (turso) are now declared-unwritable: writing one fails `tsc`
16+
(the input type is `never`) and fails the parse with a prescription naming the
17+
replacement. The former alias spellings (`passwd`, `pwd`, `token`, `jwt`,
18+
`auth_token`, `authtoken`) carry the same refusal. The connection form's masked
19+
secret input is unaffected — it never wrote `config`; it feeds the datasource secret
20+
binder, which encrypts into `sys_secret` and stores only an opaque handle.
21+
22+
**Connector authoring door.** `DeclarativeConnectorEntrySchema` (behind
23+
`defineStack({ connectors })` and `PUT /meta/connector/:name`) now refuses a
24+
non-`none` `authentication` on **every** authored entry — catalog descriptors
25+
included. Until now only provider-bound instances were covered (ADR-0097 §3), so a
26+
descriptor could publish an inline `token`/`key`/`password`/`clientSecret`. The
27+
runtime shape is unchanged: a plugin handing resolved secrets to
28+
`registerConnector` keeps working.
29+
30+
## FROM → TO
31+
32+
```ts
33+
// before — accepted, stored in cleartext in sys_metadata
34+
defineDatasource({
35+
name: 'warehouse', driver: 'postgres',
36+
config: { database: 'analytics', username: 'ro', password: 'hunter2' },
37+
})
38+
39+
// after — the secret lives in the secret store; config carries no credential
40+
defineDatasource({
41+
name: 'warehouse', driver: 'postgres', schemaMode: 'external',
42+
config: { database: 'analytics', username: 'ro' },
43+
external: { allowWrites: false, credentialsRef: 'sys_secret:<handle>' },
44+
})
45+
// (Setup → Datasources binds the secret for you: its password field encrypts into
46+
// sys_secret and writes external.credentialsRef — it never wrote config.)
47+
```
48+
49+
```ts
50+
// before — descriptor published an inline credential
51+
defineConnector({
52+
name: 'erp', label: 'ERP', type: 'saas',
53+
authentication: { type: 'api-key', key: '', headerName: 'X-API-Key' },
54+
})
55+
56+
// after — descriptor: no live credentials (document the scheme in prose);
57+
defineConnector({ name: 'erp', label: 'ERP', type: 'saas',
58+
description: 'Authenticates with an API key in the X-API-Key header.' })
59+
// instance: reference the credential (ADR-0097 §3)
60+
defineConnector({ name: 'erp', label: 'ERP', type: 'saas', provider: 'openapi',
61+
providerConfig: { spec: './erp-openapi.json' },
62+
auth: { type: 'api-key', credentialRef: 'ERP_API_KEY' } })
63+
```
64+
65+
There is deliberately **no automatic rewrite**: moving a cleartext credential into
66+
`sys_secret` requires encrypting it through a running secret binder, which a
67+
source-file transform cannot do — auto-deleting the key would silently drop a live
68+
credential instead. `os migrate meta` surfaces both changes as structured TODOs
69+
(semantic entries `datasource-config-inline-credential-refused`,
70+
`connector-inline-authentication-publish-refused`). The migration story for
71+
**already-stored** cleartext rows is programme scope, tracked as a follow-up card
72+
under #7990 — this release closes the doors that keep writing new ones.
73+
74+
<!-- adr-0087: registered datasource-config-inline-credential-refused, connector-inline-authentication-publish-refused -->

content/docs/references/data/driver-mongo.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ MongoDB Connection Configuration
4646
| **host** | `string` || Host address |
4747
| **port** | `integer` || Port number |
4848
| **username** | `string` | optional | Authentication user |
49-
| **password** | `string` | optional | Authentication password (prefer external.credentialsRef) |
49+
| **password** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config` (#7990) |
5050
| **authSource** | `string` | optional | Authentication database |
5151
| **options** | `Record<string, any>` | optional | Extra MongoClient options (replicaSet, tls, timeouts, …) |
5252

content/docs/references/data/driver-mysql.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ MySQL / MariaDB connection configuration
4747
| **port** | `integer` || Port number |
4848
| **database** | `string` | optional | Database name |
4949
| **username** | `string` | optional | Authentication user |
50-
| **password** | `string` | optional | Authentication password (prefer external.credentialsRef) |
50+
| **password** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config` (#7990) |
5151
| **ssl** | `boolean` | optional | Enable TLS. Certificates go in the datasource-level `ssl` block. |
5252
| **autoMigrate** | `Enum<'off' \| 'safe'>` | optional | Dev-only non-destructive schema self-heal (#2186) |
5353

content/docs/references/data/driver-postgres.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ PostgreSQL connection configuration
4545
| **port** | `integer` || Port number |
4646
| **database** | `string` | optional | Database name |
4747
| **username** | `string` | optional | Authentication user |
48-
| **password** | `string` | optional | Authentication password (prefer external.credentialsRef) |
48+
| **password** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config` (#7990) |
4949
| **ssl** | `boolean` | optional | Enable TLS. Certificates go in the datasource-level `ssl` block. |
5050
| **schema** | `string` || Default schema (knex searchPath) |
5151
| **applicationName** | `string` | optional | Postgres application_name |

content/docs/references/data/driver-turso.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Turso / libSQL Connection Configuration
6666
| Property | Type | Required | Description |
6767
| :--- | :--- | :--- | :--- |
6868
| **url** | `string` || libSQL endpoint or local file: a remote libsql/https Turso URL, a file path, or :memory: |
69-
| **authToken** | `string` | optional | JWT auth token for a remote libSQL database (prefer external.credentialsRef) |
69+
| **authToken** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config` (#7990) |
7070
| **encryptionKey** | `string` | optional | AES-256 encryption key for the local database file (local/replica modes) |
7171
| **concurrency** | `integer` | optional | Maximum concurrent requests to the remote database |
7272
| **syncUrl** | `string` | optional | Remote sync URL for embedded-replica mode: a libsql or https Turso endpoint |

content/docs/references/integration/connector.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Circuit breaker configuration
166166
| **type** | `Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>` || Connector type |
167167
| **description** | `string` | optional | Connector description |
168168
| **icon** | `string` | optional | Icon identifier |
169-
| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
169+
| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets — plugin-supplied at registerConnector). Authored entries must not inline secrets (#7990): use `auth.credentialRef` on a provider-bound instance. |
170170
| **provider** | `string` | optional | Generic-executor key that materializes this declarative entry at boot (e.g. openapi/mcp/rest). Omit for a catalog-only descriptor. Unknown provider ⇒ hard boot error (ADR-0097). |
171171
| **providerConfig** | `Record<string, any>` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
172172
| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
@@ -488,7 +488,7 @@ Connector type
488488
| **type** | `Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>` || Connector type |
489489
| **description** | `string` | optional | Connector description |
490490
| **icon** | `string` | optional | Icon identifier |
491-
| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets). Provider-bound declarative instances use `auth.credentialRef` instead. |
491+
| **authentication** | `{ type: 'oauth2'; authorizationUrl: string; tokenUrl: string; clientId: string; … } \| { type: 'api-key'; key: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; password: string } \| { type: 'bearer'; token: string } \| { type: 'none' }` | optional | Authentication configuration (runtime shape with inline secrets — plugin-supplied at registerConnector). Authored entries must not inline secrets (#7990): use `auth.credentialRef` on a provider-bound instance. |
492492
| **provider** | `string` | optional | Generic-executor key that materializes this declarative entry at boot (e.g. openapi/mcp/rest). Omit for a catalog-only descriptor. Unknown provider ⇒ hard boot error (ADR-0097). |
493493
| **providerConfig** | `Record<string, any>` | optional | Provider-specific config validated by the provider factory at boot (e.g. `{ spec, baseUrl }` for openapi, where spec is an inline document, a package-relative file path like './billing-openapi.json', or an http(s) URL). Requires `provider`. |
494494
| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |

0 commit comments

Comments
 (0)