Skip to content

Commit c0c8e43

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-u9x8uu
2 parents 8fe6166 + 5374967 commit c0c8e43

68 files changed

Lines changed: 2450 additions & 114 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
---
5+
6+
feat(spec): `tencentcloud-tc3-hmac-sha256` joins the `signed` connector-instance auth schemes (ADR-0143 D2.3, #26710)
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: added
10+
11+
<!-- 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. -->
12+
13+
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.
14+
15+
- **`@objectstack/spec`**
16+
- `ConnectorSigningSchemeSchema` gains `tencentcloud-tc3-hmac-sha256`.
17+
- 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`.
18+
- 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.
19+
- `ConnectorRequestSigner.scheme` is typed as `ConnectorSigningScheme` rather than `string`. The interface ships for the first time in this release, together with the first scheme.
20+
- **`@objectstack/service-automation`**
21+
- The signer registry binds a `tencentcloud-tc3-hmac-sha256` instance's key to its TC3-HMAC-SHA256 signer.
22+
- 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.
23+
- A request without a content type is signed and sent as `application/x-www-form-urlencoded`.
24+
- The key never leaves the host.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/skills': minor
4+
---
5+
6+
feat(spec): `currencyConfig.homeAmount: { field, rateDate, rateField }` — a record-mode amount declares its stored home-currency amount by pointer (ADR-0144 D5)
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: added
10+
11+
⛔ Not released alone: this ships in the same release as the write-time home-currency projection that fills these fields (ADR-0144 D9, release R2: "the `homeAmount` key never ships ahead of the engine").
12+
13+
An amount in `currencyMode: 'record'` can now declare where its value is stored after conversion into the organization's home currency (the `localization.currency` setting), which date picks the exchange rate, and where the rate used is stored. Consolidated totals then aggregate the home field like any other stored currency field.
14+
15+
```ts
16+
fields: {
17+
currency_code: Field.text({ label: 'Currency', valueDomain: 'iso_4217_currency' }),
18+
invoice_date: Field.date({ label: 'Invoice date' }),
19+
amount: Field.currency({
20+
label: 'Amount',
21+
currencyConfig: {
22+
currencyMode: 'record',
23+
currencyField: 'currency_code',
24+
homeAmount: { field: 'amount_home', rateDate: 'invoice_date', rateField: 'amount_rate' },
25+
},
26+
}),
27+
amount_home: Field.currency({ label: 'Amount (home)', readonly: true }),
28+
amount_rate: Field.number({ label: 'Rate used', readonly: true }),
29+
}
30+
```
31+
32+
- **Three required pointers.** When `homeAmount` is declared, `field`, `rateDate` and `rateField` must all be given, and none may be empty. There is no default, no injected column and no fixed field name. The document date is never taken from `created_at`. `homeAmount` is refused under `dynamic` and `fixed`.
33+
- **What the pointers name, refused at parse with a remedy and the object's candidates.** Each refusal is located at `fields.AMOUNT.currencyConfig.homeAmount.POINTER`.
34+
- `field` must name another `currency` field of the same object in `dynamic` mode (it displays in the home currency, so metadata never names that currency). No other amount may already store into it.
35+
- `rateDate` must name a `date` or `datetime` field.
36+
- `rateField` must name a `number` field. Two amounts may share it only when they have the same `currencyField` and `rateDate`.
37+
- The amount itself must be a stored `currency` field. A `formula` amount is refused.
38+
- **The home and rate fields are written by the platform only.** They must declare `readonly: true`, the existing mark that drops a client's write of the field, and no `expression`.
39+
- **Skills:** `objectstack-data/rules/field-types.md` teaches `homeAmount`. Its field names (`amount_home`, `amount_rate`) are examples, not a contract.
40+
- **Nothing to migrate.** `homeAmount` is new, and no existing config changes meaning.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
'@objectstack/cli': patch
4+
'@objectstack/spec': patch
5+
---
6+
7+
feat(service-datasource): a datasource defined in code reads its credential at connect from `OS_DATASOURCE_<NAME>_PASSWORD`
8+
9+
Clause-②: yes (widening) — `@objectstack/service-datasource` exports `datasourceEnvCredentialVar`, `resolveDatasourceEnvCredentialsSwitch`, `DATASOURCE_ENV_CREDENTIALS_SWITCH` and the `DatasourceEnvCredentialSource` type, and `DatasourceConnectionServiceConfig` gains the optional `envCredentials`; `@objectstack/spec/data` exports `CODE_DEFINED_CREDENTIAL_ROUTE`. Nothing an author writes is added or refused.
10+
Metadata-protocol: none
11+
12+
A datasource declared in code (`defineDatasource`, or a package's `*.datasource.ts`) had no way to get its password: the driver contracts refuse `config.password` and a `user:password@` URL, the Setup → Datasources form cannot edit a code datasource, and nothing mints a `sys_secret:` handle for one. Now the credential stays out of the declaration and is read at connect from a variable derived from the datasource name: `warehouse` reads `OS_DATASOURCE_WAREHOUSE_PASSWORD` (upper-cased; every character outside `A-Z0-9` becomes `_`). The value is the connection's secret: the password for postgres, mysql and mongodb, the `authToken` for turso.
13+
14+
- **A host switch.** `OS_DATASOURCE_ENV_CREDENTIALS` takes `on` or `off`; any other value refuses the boot with a sentence. `os serve` (and so `os start` and `os dev`) turns it on unless the operator set it. Every other composition leaves it off, so a host that loads several tenants' artifacts into one process never reads the variable.
15+
- **Code datasources only, by provenance.** The variable is read on the boot's declared connect, for a name in the host's code-datasource set: the booted artifact's datasources and those of a package installed locally. A datasource created through Setup or the API under the same name never reads it, and neither does the `default` datasource, which keeps `OS_DATABASE_URL`. A record's own `origin` is never consulted.
16+
- **A declared credential wins.** With `external.credentialsRef` or `external.connector` declared the variable is not read, and a reference that fails to resolve stays fail-closed.
17+
- **Unset.** The connect goes ahead without a secret as before; when the database then refuses, the reason names the variable to set.
18+
- **Two names deriving one variable** are refused at their connect.
19+
- **Who can read it.** Any code datasource the host registers under a name reads that name's variable, including one in an installed package. Install only packages you trust with the credentials in the process environment.
20+
21+
The credential refusals (`config.password`, a URL userinfo password, a credential query parameter, the mongo `options` passthrough) and the two `password` guidance entries on `DatasourceSchema` / `ExternalDatasourceSettingsSchema` end with one shared sentence naming this route, after the Setup and `external.credentialsRef` routes they already name. `planCredentialMigration`'s remedy for a code-defined row names the row's variable.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a calendar view can declare the date it opens on, `calendar.initialDate`
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
A calendar view used to open on the current month, always, so a calendar whose records all fall months ahead (a renewals calendar) opened on an empty grid and the author had no key to say otherwise. `CalendarConfigSchema` (the list view's `calendar` block, and the `object-calendar` node's `calendar` prop, which is the same block by reference) gains one optional key:
11+
12+
- **`initialDate`, one of two forms.** A record anchor: `'first_record'` opens on the period containing the earliest start date (the `startDateField` value) among the records the view selects, and `'next_record'` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, the same formula vocabulary filter values and a dashboard date filter's `defaultValue` already accept (`DATE_MACRO_TOKENS` and the parameterised `{N_unit_ago}` / `{N_unit_from_now}` grammar): `'{today}'`, `'{next_month_start}'`, `'{current_quarter_start}'`, `'{3_months_from_now}'`, resolved each time the calendar opens. The period is the month, week or day the active view mode shows.
13+
- **Absent means today.** The key has no spec-side default, so a calendar stored before it existed parses to exactly the block it stored and opens where it always did. There is no bare `'today'`: `'{today}'` is its one spelling.
14+
- **What is refused.** A fixed calendar date (`'2026-11-01'`) or timestamp, an unwrapped token (`'today'`), an unknown token (`'{next_moon}'`) and any other spelling are refused at `calendar.initialDate`. The message names both forms and points to `DATE_MACRO_TOKENS`, and a bare known token is told the wrapped spelling to write. A fixed date goes stale as time passes; a macro does not.
15+
16+
The Console renderer reads the key in a follow-up change; until then a calendar that declares it still opens on today.

‎content/docs/automation/connectors.mdx‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,12 +483,21 @@ auth: {
483483
keyId: 'YourAccessKeyId', // the AccessKey ID: public, stays in metadata
484484
credentialRef: 'ALIYUN_ACCESS_KEY_SECRET',
485485
}
486+
487+
// Tencent Cloud API 3.0, signature v3 (TC3-HMAC-SHA256):
488+
auth: {
489+
type: 'signed',
490+
scheme: 'tencentcloud-tc3-hmac-sha256',
491+
keyId: 'YourSecretId', // the SecretId: public, stays in metadata
492+
credentialRef: 'TENCENT_SECRET_KEY',
493+
}
486494
```
487495

488496
What the platform does with it:
489497

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

505522
What the publish door refuses:
506523

@@ -509,7 +526,8 @@ What the publish door refuses:
509526
signer and its test vectors, so a scheme the server cannot run is never
510527
accepted.
511528
- **An inline key**: `secret`, `privateKey`, `secretKey` or `accessKeySecret`
512-
is refused by name, pointing at `credentialRef`. An authored `signature` is
529+
is refused by name, pointing at `credentialRef`. The vendors' words for the
530+
key id (`accessKeyId`, `secretId`) are answered with `keyId`. An authored `signature` is
513531
refused too: it is computed for every request.
514532
- **`provider: 'mcp'`**, at `auth.type`: an MCP server is not signed per
515533
request. Declare `provider: 'rest'` or `provider: 'openapi'`.

‎content/docs/data-modeling/drivers.mdx‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -944,11 +944,13 @@ export default defineStack({
944944
```
945945

946946
Each `url` must be credential-free when the stack is published: a
947-
`user:password@` URL is refused, like an inline `config.password`. Bind the
948-
password through `external.credentialsRef` instead, keeping the user in the URL
949-
(`mongodb://svc@host/db`): the bound secret is injected beside it at connect. The
950-
[read-only example](#read-only-grant-it-at-the-database-not-in-metadata) below
951-
shows the binding.
947+
`user:password@` URL is refused, like an inline `config.password`. Keep the user
948+
in the URL (`mongodb://svc@host/db`) and supply the password at runtime: a
949+
datasource declared in code reads it at connect from `OS_DATASOURCE_<NAME>_PASSWORD`
950+
(`OS_DATASOURCE_ANALYTICS_PASSWORD` here), which `os serve` / `os start` honour by
951+
default, and the value is injected beside the user. A datasource created through
952+
Setup binds it through `external.credentialsRef` instead. See
953+
[Credentials](/docs/data-modeling/external-datasources#4-credentials).
952954

953955
Then in your object definition:
954956

@@ -990,19 +992,21 @@ export const Analytics = defineDatasource({
990992
driver: 'postgres',
991993
// The connection itself cannot write. Nothing in the app can talk past it.
992994
config: { host: 'db.internal', database: 'analytics', username: 'analytics_ro' },
993-
// The account's password, bound through the secret store (see below).
994-
external: { credentialsRef: 'sys_secret:9f2c…' },
995+
// The account's password is read at connect from OS_DATASOURCE_ANALYTICS_PASSWORD.
995996
active: true,
996997
});
997998
```
998999

9991000
The read-only account's password never goes in `config`: the driver contract
1000-
refuses `config.password`, and a `user:password@` URL, at publish. Bind it
1001-
instead. The Setup → Datasources connection form's secret field encrypts it into
1002-
`sys_secret` and stores the opaque handle at `external.credentialsRef`, or you
1003-
write a handle the secret store minted there yourself. `credentialsRef` is the
1004-
one `external` key a `managed` datasource may carry; every federation key is
1005-
still refused there. See
1001+
refuses `config.password`, and a `user:password@` URL, at publish. Because this
1002+
datasource is defined in code, the runtime reads the password at connect from
1003+
`OS_DATASOURCE_ANALYTICS_PASSWORD`, derived from its name. The CLI host
1004+
(`os serve` / `os start`) reads it by default; elsewhere the host turns it on
1005+
with `OS_DATASOURCE_ENV_CREDENTIALS=on`. The same datasource created through
1006+
Setup → Datasources binds the password instead: the connection form's secret
1007+
field encrypts it into `sys_secret` and stores the opaque handle at
1008+
`external.credentialsRef`. `credentialsRef` is the one `external` key a `managed`
1009+
datasource may carry; every federation key is still refused there. See
10061010
[Credentials](/docs/data-modeling/external-datasources#4-credentials).
10071011

10081012
Note that a read-only account also refuses **DDL**, so such a datasource cannot

‎content/docs/data-modeling/external-datasources.mdx‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@ const policy: DatasourceConnectPolicy = {
258258
Never inline a password. Put a reference in `external.credentialsRef` and store the
259259
secret in the secret store (the same `SecretBinder` / `ICryptoProvider` the
260260
runtime-admin "Add Datasource" wizard uses). The credential is resolved to
261-
cleartext **at connect, before the driver is built**.
261+
cleartext **at connect, before the driver is built**. A datasource defined in code
262+
reads an environment variable instead; see
263+
[A datasource defined in code](#a-datasource-defined-in-code-the-environment-variable).
262264

263265
The shipped binder encrypts the cleartext into a `sys_secret` row and mints an
264266
**opaque** handle — `sys_secret:<id>` — as the `credentialsRef`; that is the only
@@ -274,6 +276,77 @@ for the datasources listed under [When auto-connect fails](#when-auto-connect-fa
274276
and degrades with a warning otherwise. (Credential-less drivers such as SQLite
275277
simply have no `credentialsRef`.)
276278

279+
### A datasource defined in code: the environment variable
280+
281+
The handle above is minted by the Setup → Datasources form, which cannot edit a
282+
datasource defined in code (`defineDatasource`, or a `*.datasource.ts` file in a
283+
package). A code author leaves the credential out of the declaration entirely,
284+
and the runtime reads it **at connect** from an environment variable whose name
285+
the platform derives from the datasource name:
286+
287+
| Datasource name | Variable read at connect |
288+
| :--- | :--- |
289+
| `warehouse` | `OS_DATASOURCE_WAREHOUSE_PASSWORD` |
290+
| `crm_replica` | `OS_DATASOURCE_CRM_REPLICA_PASSWORD` |
291+
292+
The name is upper-cased and every character outside `A-Z0-9` becomes `_`. The
293+
value is handed to the driver as the connection's secret: the password for
294+
`postgres`, `mysql` and `mongodb`, the `authToken` for `turso`. It is never
295+
logged, stored or answered.
296+
297+
```typescript
298+
import { defineDatasource } from '@objectstack/spec/data';
299+
300+
export const Warehouse = defineDatasource({
301+
name: 'warehouse',
302+
driver: 'postgres',
303+
// No password here: it is read from OS_DATASOURCE_WAREHOUSE_PASSWORD at connect.
304+
config: { host: 'db.internal', database: 'warehouse', username: 'svc_app' },
305+
});
306+
```
307+
308+
```bash
309+
OS_DATASOURCE_WAREHOUSE_PASSWORD='…' os start
310+
```
311+
312+
What the slot does and does not do:
313+
314+
- **It is a host switch, `OS_DATASOURCE_ENV_CREDENTIALS`, which takes `on` or
315+
`off`.** `os serve`, `os start` and `os dev` turn it on unless you set it
316+
yourself. Every other composition (a host that builds its own kernel, a cloud
317+
runtime) leaves it off, so the variable is never read there. Any other value
318+
is refused at boot.
319+
- **It only serves datasources the host registers from code.** That means the
320+
ones in the booted artifact and in packages installed locally. A datasource
321+
created through Setup or the API under the same name never reads the
322+
variable. Nor does the `default` datasource, which keeps `OS_DATABASE_URL`.
323+
- **A declared credential wins.** With `external.credentialsRef` or
324+
`external.connector` declared, the variable is not consulted. If that
325+
reference fails to resolve, the datasource stays unconnected (fail-closed, as
326+
above) and does not fall back to the environment.
327+
- **An unset variable leaves the connect as it was.** The driver connects
328+
without a secret; if the database refuses, the datasource is unconnected and
329+
its reason names the variable to set.
330+
- **Two datasources that derive one variable are refused.** Valid datasource
331+
names are lower-case, so this cannot happen between two declarations that
332+
pass the schema; the guard covers a host that hands `connect()` other names.
333+
- **A plugin driver that resolves its credential per request** (the OData
334+
driver resolves `credentialsRef` that way) does not read this slot: it feeds
335+
only the secret a driver receives when its connection is built.
336+
337+
<Callout type="warn">
338+
**Who can read the variable.** The derived name is the only key, so any code
339+
datasource the host registers under that name reads it. Under `os serve` that
340+
includes every package installed into the environment: **a package can declare
341+
a datasource of any free name and so read the matching
342+
`OS_DATASOURCE_<NAME>_PASSWORD`**. Install only packages you trust with the
343+
credentials in that process's environment. On a host where several tenants'
344+
artifacts share one process, a process-wide variable would be readable by every
345+
tenant artifact that declares the name. That is why the switch is off outside the
346+
CLI host; turn it on there only when one operator owns every artifact the process
347+
loads.
348+
</Callout>
349+
277350
### Borrowing a connector instance's credential
278351

279352
An **API-backed** datasource, one served by a plugin driver such as the

0 commit comments

Comments
 (0)