You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b6c6ff2
Browse filesBrowse the repository at this point in the historyBrowse files
feat(service-automation): `oauth2` connector instances are served per dispatch, and an administrator connects an `authorization_code` one for their organization (ADR-0143 S1, part of #26702)
8
+
9
+
Clause-②: yes
10
+
Metadata-protocol: added
11
+
12
+
ADR-0143 S0 (#28137) let a declarative connector instance declare `auth: { type: 'oauth2', ... }`, and this server held every such instance degraded. This slice serves it.
13
+
14
+
-**`@objectstack/service-automation`**: an `oauth2` instance is materialized with a `credentialHandle` and no static `auth` (D4). The handle obtains the access token from `tokenUrl` (a confidential client authenticates with HTTP Basic, a public client sends its `client_id`), keeps it in memory only, refreshes it within the 30-second skew margin and once on a `401`, and shares one token request among concurrent dispatches. With a cluster service a refresh runs under the cluster lock. An unreachable or `5xx` token endpoint is `CONNECTOR_UPSTREAM_UNAVAILABLE`.
15
+
-`client_credentials` needs nothing more than the client secret its `credentialRef` names.
16
+
-`authorization_code` keeps its refresh token in a new object, `sys_connector_connection` (D3): one row per organization and instance, the token a `Field.secret`, never an access token, never a sign-in account (D6). An instance nobody connected is degraded "not connected", never a failed boot. A refused grant (`invalid_grant`) marks the row `reconnect_required` and degrades the instance until it is connected again. A dispatch uses its run's organization's connection; a run with no organization uses the only one, or is refused `422` when there are several.
17
+
- A provider opts in with `registerConnectorProvider(key, factory, { credentialHandle: true })`, saying it asks the handle on every request. An `oauth2` instance on any other provider stays degraded, so it is never called without its token. The built-in `rest`, `openapi` and `mcp` providers do not opt in yet (S2), and `app-credential` stays degraded until S2.
18
+
- A new plugin option, `connectorOAuthRedirectUri`, fixes the redirect URI; otherwise it is `OS_BASE_URL` (or `OS_AUTH_URL`) plus `/api/v1/automation/connectors/oauth/callback`.
19
+
-**`@objectstack/spec`**: `IAutomationService` gains four optional members, `getConnectorConnection`, `beginConnectorConnection`, `completeConnectorConnection` and `revokeConnectorConnection`, with their request and result types (`ConnectorConnectionState`, `ConnectorConnectionBegin`, …). `sys_connector_connection` joins the `service-automation` platform object names.
20
+
-**`@objectstack/runtime`**: the connection door (D5), every route needing `manage_metadata`:
21
+
-`GET` / `POST` / `DELETE /api/v1/automation/connectors/:name/connection` answer the organization's connection status, begin a connect (a fresh `state` and an `S256` PKCE challenge bound to the caller's user and organization, answered as the vendor's authorization URL), or revoke it. They take no query parameter.
22
+
-`GET /api/v1/automation/connectors/oauth/callback` is the vendor's redirect. Its query set is closed (`code`, `state`, `error`, `error_description`, `error_uri`, `iss`), each parameter once. The `state` is used once, and a callback from another user or organization is refused before its code is exchanged.
23
+
- No answer, refusal or log line carries a token, a code, a verifier or a client secret.
feat(console): a bulk result names the records it wrote, and a required-field refusal says what to fill in
10
+
11
+
Clause-②: yes (widening: `ActionResult` gains the optional `fieldErrors` member, `ActionFieldError` is a new export of `@object-ui/types`, and `ActionResponseOutcome` gains `fieldErrors`; no accept set moves)
12
+
Metadata-protocol: none
13
+
14
+
-**Bulk action result.** After a bulk action such as Mark Done, the result read only "Succeeded 2 / 2". It now lists the records that succeeded, named the way the confirm step names them (the object's name field). The list appears when every failure is reported against a record of the run; when the server answered a bare count, the result keeps to the count rather than name a record that may have failed.
15
+
-**Required-field refusal from an action.** Running an action whose write the server refuses for a missing required field, such as Submit for Sign-off on a draft invoice with no Issued On, used to say only "Issued On is required". It now continues "Fill in Issued On, then run “Submit for Sign-off” again." Several fields are joined in the session's language. Any other refusal is shown as the server wrote it.
16
+
- For code that dispatches actions through `createServerActionHandler`: a failed `ActionResult` now carries the refusal's per-field entries as `fieldErrors` (field, code, message, label), read from the answer's `error.details.fields`. `error` is unchanged.
feat(plugin-report): a summary report with two or more `rows` levels draws a subtotal per outer group, and no longer repeats the outer value on every row
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
**Visible change on existing reports.** Every dataset-bound `summary` report whose `rows` names two or more dimensions now renders grouped on its next render, with nothing for the author to change:
12
+
13
+
- Each outer group draws a header row naming its value once, then its inner rows with the outer column left blank, then a `Subtotal` row. With three or more levels the groups nest, one header and one subtotal per level above the innermost.
14
+
- The subtotals are the server's. The report asks the dataset for every proper prefix of `rows` through `totals.groupings` (`[[owner], []]` for `rows: [owner, stage]`), the way a `matrix` already asks for its row and column totals. They are never summed in the browser, so an `avg` or a distinct count subtotals correctly.
15
+
- Groups keep the order their first row arrives in: the declared option order of a select dimension, or the report's own `order`. Rows of one group that an `order` by a measure brings back apart are drawn together under it.
16
+
- Clicking a subtotal row drills into the whole group, scoped by the outer dimensions only. Clicking an inner row drills by every dimension, as before.
17
+
- The export (CSV and XLSX) writes the grouped table as drawn.
18
+
19
+
Unchanged: a one-level `summary` (grand-total footer only), a `tabular` report, and a `matrix`, including a matrix without `columns`. A server that does not answer the prefix groupings gets the plain table, with the outer value repeated and no subtotal rows.
20
+
21
+
`@object-ui/i18n` adds `report.subtotal` to every pack.
feat(plugin-auth): opt-in Cloudflare Turnstile on email sign-up — off unless both `OS_TURNSTILE_SITE_KEY` and `OS_TURNSTILE_SECRET_KEY` are set
12
+
13
+
Clause-②: yes (widening: the public auth config gains `features.turnstileSiteKey`, served only while the challenge is on; `PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS` and `AuthFeaturesConfigSchema` gain `turnstileSiteKey`; with the challenge on, `/sign-up/email` answers better-auth's `captcha` refusals `400 MISSING_RESPONSE`, `403 VERIFICATION_FAILED` and `500 UNKNOWN_ERROR`; two new env vars; `@object-ui/auth` gains `TurnstileWidget`, `TURNSTILE_SCRIPT_URL`, `RegisterForm`'s `turnstileSiteKey` prop and `captchaFailed`/`captchaRetry` labels, `SignUpData.captchaToken` and a fifth `signUp` argument; `@object-ui/app-shell` gains `signUpCaptchaFailedMessage`)
14
+
Metadata-protocol: added
15
+
16
+
A deployment can now ask for a human check before an email/password account is created. Set both variables and:
17
+
18
+
-**The server** mounts better-auth's `captcha` plugin with the `cloudflare-turnstile` provider on `/sign-up/email` only. A sign-up without a token in the `x-captcha-response` header is refused before anything is read or written, a token is verified with Cloudflare using the secret, and a verification that cannot complete fails closed. Sign-in and password reset are not challenged (better-auth's default list would have included them; it is overridden). The per-address rate limit still runs first. Server-side registrations through `auth.api.signUpEmail` (the development admin seed, demo staff) are unaffected.
19
+
-**The public auth config** (`GET /api/v1/auth/config`) serves `features.turnstileSiteKey`, the public site key, and never the secret.
20
+
-**The Console** renders the challenge on `/register` and on the first-run `/setup` wizard, holds submit until it is solved, and sends the token. A failed, expired or refused challenge shows one retry message, never Cloudflare's code or the plugin's text. The Console document's content-security-policy admits `https://challenges.cloudflare.com` in `script-src` and `frame-src` only while both variables are set.
21
+
22
+
With either variable unset or blank nothing changes: no plugin, no config key, no widget, the same policy. A deployment that sets only one gets one boot `warn` naming the missing variable.
`GET …/:name/connection` answers the organization's status
495
+
(`not_connected`, `connected`, `reconnect_required`), never a token. The
496
+
`state` and the PKCE verifier are bound to the user and organization that
497
+
began the connect and are used once; a callback from anyone else is refused
498
+
before its code is exchanged. The redirect URI is the deployment's
499
+
(`OS_BASE_URL` or `OS_AUTH_URL` plus `/api/v1/automation/connectors/oauth/callback`,
500
+
or the automation plugin's `connectorOAuthRedirectUri`), never the
501
+
request's; register that URI with the vendor. The refresh token is kept
502
+
encrypted in `sys_connector_connection`, one row per organization; it is
503
+
not a sign-in account, and signing in never touches it. A run dispatches
504
+
with its own organization's connection; a run with no organization uses the
505
+
instance's only connection, and is refused when there are several. When the
506
+
vendor refuses the grant (`invalid_grant`), the connection is marked
507
+
**reconnect required** and the instance degraded until an administrator
508
+
connects it again. A revoke deletes the stored connection only: the server
509
+
does not yet revoke the refresh token at the vendor (RFC 7009), so it stays
510
+
valid there until it expires or is revoked in the vendor's console.
511
+
512
+
What is not served yet is held **degraded** with a reason that names it, and
513
+
never called without its credential: `app-credential` (its vendor exchanges
514
+
are not run yet), an `oauth2` instance on a provider that does not yet ask the
515
+
credential handle (the built-in `rest`, `openapi` and `mcp` providers do not
516
+
yet), an `mcp` instance that left its endpoints to discovery, and an `oauth2`
517
+
instance whose `tokenUrl` or `authorizationUrl` is not `https://` (plain
518
+
`http://` is accepted only to a loopback host, so the client secret and the
519
+
refresh token never cross a network in the clear). A datasource
520
+
that borrows from a grant-bearing instance (`external.connector`) is refused
521
+
with `422`. Until then, declare a static kind (`bearer`, `api-key` or `basic`)
522
+
to dispatch now; don't emulate a grant by putting a short-lived access token
523
+
behind a `bearer` ref.
480
524
481
525
### Request signing: `signed`
482
526
@@ -599,7 +643,9 @@ retry):
599
643
|:---|:---|
600
644
|`provider` names no installed factory |**Hard boot error** naming the entry, the provider, and the plugin that supplies it — add the executor to `plugins:`|
601
645
| Invalid `providerConfig` (missing `baseUrl` / `spec` / `transport`, wrong shape) |**Hard boot error** from the provider factory |
602
-
|`auth` is a grant-bearing kind (`oauth2`, `app-credential`) on a server that does not yet run its per-dispatch resolution |**Not fatal**: the instance registers **degraded**, naming the kind, and the upstream is never called without its credential; a datasource borrowing it is refused with `422`|
646
+
|`auth` is a grant-bearing kind (`oauth2`, `app-credential`) this server does not serve yet (`app-credential`; `oauth2` on a provider that does not ask the credential handle) |**Not fatal**: the instance registers **degraded**, naming the kind, and the upstream is never called without its credential; a datasource borrowing it is refused with `422`|
647
+
| An `authorization_code` instance nobody connected, or whose grant the vendor refused |**Not fatal**: the instance registers **degraded**, "not connected" or "reconnect required" — connect it (`POST …/connectors/:name/connection`), and it materializes without a restart |
648
+
| The token endpoint is unreachable or answers `5xx` / `429`|**Not fatal**: the dispatch fails `CONNECTOR_UPSTREAM_UNAVAILABLE` and is retried on the existing backoff; the connection stays connected |
603
649
|`auth` is `signed` on a provider that does not say it applies the request signer |**Hard boot error** naming the instance, the scheme and the provider: its calls would go out unsigned — declare it through `rest` or `openapi`|
604
650
|`credentialRef` resolves to nothing |**Not fatal**: the instance registers **degraded**, naming the reference — set it (`PUT …/credential-refs/:ref`), and it materializes without a restart |
605
651
| The upstream refuses the credential (`401` / `403`) |**Not fatal**: the instance registers **degraded**. A stored credential is marked `rejected` and waits until it is set again (or a restart); an environment or vault credential is retried on the backoff |
Copy file name to clipboardExpand all lines: content/docs/deployment/environment-variables.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -110,6 +110,7 @@ Read by `@objectstack/service-print` when `PrintServicePlugin` is composed; see
110
110
|`OS_TRUSTED_ORIGINS`| csv | — | Comma-separated list of origins permitted for auth callbacks / CSRF. |
111
111
|`OS_TERMS_URL`| url | — | URL shown next to the "Terms of Service" link on the sign-up form. Empty string hides the link. |
112
112
|`OS_PRIVACY_URL`| url | — | URL shown next to the "Privacy Policy" link on the sign-up form. Empty string hides the link. |
113
+
|`OS_TURNSTILE_SITE_KEY` / `OS_TURNSTILE_SECRET_KEY`| string | — | Opt-in Cloudflare Turnstile on email sign-up. Set **both** and `/sign-up/email` (the register page and the first-run setup) requires a verified Turnstile token, the Console renders the challenge, and its content-security-policy admits `https://challenges.cloudflare.com`; sign-in and password reset are not challenged. Only the site key is served to the browser (`features.turnstileSiteKey`); the secret never leaves the server. With either unset nothing changes (one boot `warn` names the missing one). |
0 commit comments