Skip to content

Commit 82440a8

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22510-activity-actor-name
2 parents 0cff02f + ee8751d commit 82440a8

78 files changed

Lines changed: 3440 additions & 278 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Source comments that cited a tracker number now say in words which decision they mean, and the pin tests that read those comments match the new words
6+
7+
Clause-②: no
8+
9+
The package ships its `src/**/*.zod.ts` sources, and `dist` carries the member docblocks. Four of those texts named a decision only by its tracker number. Each now names the decision in words. The pin test that reads each text matches the new words, at the places the old number stood:
10+
11+
- `ai/build-progress.zod.ts`: the cloud card that ruled the build-progress phase vocabulary in, and that owns where the emitter sits, is now "the cloud build-progress card" in all seven places. The consumer's strict-parse carrier is "block 2 of the objectui build-panel card". The `ai/build-progress` reference page is regenerated from the module docblock, so its description now ends with "(the cloud build-progress card, ruling A)".
12+
- `contracts/approval-service.ts`: the `IApprovalService.continueRestoredRun` docblock says the verb is declared "exactly as the automation-service contract declared" `IAutomationService.cancelRun` / `restoreConsumedSuspension`.
13+
- `ui/notification.zod.ts`: the tombstone note for the removed `./ui` `Notification` / `NotificationConfig` wrappers is labelled `[dual-source removal]`. The `NotificationAction` removal is called "the no-door retirement".
14+
- `ui/sharing.zod.ts`: `EmbedConfigSchema` is recorded as removed "at the no-door retirement (ADR-0049 enforce-or-remove)". The `ui/sharing` reference page is regenerated from the module docblock.
15+
16+
Comments and docblocks only. No key, type, default, accepted value, `.describe()` string or export changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
A `date` or `datetime` field can declare that it is a deadline, and when that deadline is settled: `dueLike` and `settledWhen`
7+
8+
Clause-②: yes
9+
10+
- **`dueLike: boolean`** (optional, no default) declares the date a deadline. A renderer may then show relative overdue wording and an overdue colour once the date has passed. Absent means not a deadline: nothing is inferred from the field's name.
11+
- **`settledWhen`** (optional) is a per-record CEL predicate in the `visibleWhen` / `readonlyWhen` / `requiredWhen` family. While it holds, the deadline is settled and no overdue wording or colour applies to that record, for example `record.status == 'done'`.
12+
- **The doors that judge them.** `FieldSchema` refuses either key on any type other than `date` / `datetime`, and refuses `settledWhen` unless the field also declares `dueLike: true`. The CEL is judged by `@objectstack/lint`'s field-rule pass, the one `visibleWhen` goes through, at `objectstack build`, `objectstack validate` and the object save: it must parse, read fields as `record.<field>`, name declared fields, and read no root but `record` (plus `previous`, and `parent` on a master-detail line item).
13+
- **Display only.** Nothing on the write path reads either key. Every metadata that parsed before parses unchanged; a field without the keys behaves exactly as before.
14+
- **Who reads them.** objectui's date and datetime cells read `dueLike` on the record detail surfaces today. Reading `settledWhen`, and retiring the field-name guess, follows in objectui once this spec version is published.
15+
- **Where to author them.** The field designer offers both on date and datetime fields, `settledWhen` only once the field is a deadline. The example apps declare them on the showcase and todo `task` and the CRM `activity`.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-storage': minor
4+
---
5+
6+
feat(storage)!: the storage scope `public` is retired — no scope ever made a file publicly readable, and `acl: 'public_read'` stays the one opt-in for anonymous download (#22443)
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: registered storage-scope-public-retired -->
11+
12+
**BREAKING** — an accept-set narrowing on two published surfaces, shipped as `minor` under the launch-window convention for accept-set narrowings.
13+
14+
`StorageScopeSchema` described `public` as "publicly accessible static assets", and the upload doors stored a caller's `scope: 'public'` on the new file. Neither ever made a file public. The download doors judge a file by its `acl`, the `attachments` scope and field ownership alone, so a file uploaded with scope `public` and the default acl was stored private, needed a signed-in caller, and nobody was told (ADR-0049 enforce-or-remove). ADR-0104 makes `acl: 'public_read'` the one opt-in for anonymous download, so the scope is retired, not enforced: enforcing it would have let any uploader make a file anonymous at upload.
15+
16+
### What now refuses `public`
17+
18+
- **The presigned upload door and the chunked upload door** (`@objectstack/service-storage`) answer an upload naming `scope: 'public'` with `400 INVALID_REQUEST`, before any file row, session row, upload URL or backend upload exists. The message names the remedy. Every other scope, and an omitted one, is taken exactly as before.
19+
- **`StorageScopeSchema`** (`@objectstack/spec`): `public` left the enum. Writing it fails `tsc`, and parsing it, on its own or as `ObjectStorageConfig.scope`, fails with the prescription instead of zod's generic enum message.
20+
21+
### FROM → TO
22+
23+
| before | what to write instead |
24+
| --- | --- |
25+
| an upload with `scope: 'public'` | another scope, or no `scope` for the default `user`; then set `acl: 'public_read'` on the stored file record of each file that must be readable before sign-in |
26+
| `ObjectStorageConfig` with `scope: 'public'` | another scope, or no `scope` for the default `global` |
27+
28+
**The one-line fix: stop naming scope `public`, and mark each file that must render before sign-in `acl: 'public_read'` on its stored file record.** The upload request carries no `acl`: every upload is stored `acl: 'private'`, so adding an `acl` to the upload request changes nothing.
29+
30+
**Files already stored with scope `public`** are not touched. They download exactly as they did: a signed-in caller gets them, an anonymous one gets `401`, unless the file is `acl: 'public_read'`.
31+
32+
### The retirement kit
33+
34+
- **Schema.** `StorageScopeSchema` retires the member with `enumWithRetiredValues`. No D2 conversion: no metadata type carries this schema or the upload request, so `os migrate meta` has no authored source to rewrite.
35+
- **D3 entry `storage-scope-public-retired`** carries the judgement no rewrite can make: whether a file that was uploaded as `public` must really be readable before sign-in.
36+
- **Upload request contract.** The `scope` description on the presigned and chunked request schemas no longer offers `public` as an example, and says what the scope is and is not.
37+
- **Liveness.** No ledger row: the liveness ledger walks metadata types, and no metadata type carries `StorageScope`.
38+
39+
**Measured producers: none.** At origin/main da159f74e6, nothing in `packages/`, `examples/`, `apps/`, `skills/` or `content/docs/` uploads with scope `public` or declares an `ObjectStorageConfig` with it. The one hit was a `@objectstack/spec` request-schema test fixture, changed to `tenant` here. The same search finds 29 `scope: 'attachments'` writes, which is its control. At the `.objectui-sha` pin f0268ad784 and at objectui main 2063f7a, the upload adapter forwards a caller's scope and no caller names `public`: the console passes none, and the record attachments panel passes `attachments` (the control). Deployed callers and stored rows NOT MEASURED.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/service-storage': minor
3+
'@objectstack/plugin-audit': minor
4+
---
5+
6+
fix(service-storage,plugin-audit)!: the `sys_attachment` and `sys_comment` parent gates judge a `controlled_by_parent` parent through its master, so a member can no longer attach files to, or delete other people's files or comments from, a child record they may not edit
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling or stored shape is removed, renamed or re-shaped, and no stored row is read, rewritten, converted or dropped, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is a runtime accept set: the attachment parent gate (attach, the update and delete row rule, the re-point) and the comment moderation gate now refuse a write on a controlled_by_parent parent whose master the caller may not edit. The remedy is an access decision the app owner makes on the master record, not a rewrite. The two exported port types (AttachmentSharingLike, CommentSharingLike) now pick the sharing service's tri-state member instead of its two-state projection; the registered sharing service serves both, and a hand-written double meets the compiler at its own call site. The other categories are closed on facts: both bumped packages publish (not unpublished); no ADR-0087 id covers these paths and this diff adds none (not registered / already-registered); and the port types were concretely typed before, not erased (not type-surface-only). -->
11+
12+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes. It follows ADR-0055.
13+
14+
**What stops being accepted.**
15+
16+
- A `controlled_by_parent` record takes its access from its MASTER. Record sharing does not enforce on it, so the sharing service's edit verdict abstains on every such record, and both gates used to take that abstention as permission. A member holding the `sys_attachment` create or delete bit, or the `sys_comment` delete bit, could therefore attach files to, and delete other people's files and comments from, every master-detail child record of the organization. That included records whose own `PATCH` refuses the member, and records the member cannot read.
17+
- Both gates now ask the sharing service's tri-state `checkEdit`. `allow` admits and `deny` refuses, as before. On an abstention they ask the security service's master-detail write check (`ISecurityService.checkControlledByParentWrite`), which is the answer a by-id update of the parent gets. Its `allow` and `not_applicable` admit. Its `deny` refuses, and so does an `unresolvable` outcome: a broken master-detail declaration, a record that does not exist, or an empty master reference. A datasource fault propagates with its `503`.
18+
- The refusals keep today's envelopes. An attach or a re-point answers `403 ATTACHMENT_PARENT_ACCESS`. A delete of another user's file answers `403 ATTACHMENT_DELETE_DENIED`, and an update of it answers `403 RECORD_NOT_ACCESSIBLE`. A delete or update of another user's comment answers `403 RECORD_NOT_ACCESSIBLE`. A caller who cannot read the parent still gets the not-visible `403 PERMISSION_DENIED`.
19+
20+
**What is unchanged.** An uploader still deletes their own file, and an author still deletes their own comment. Posting a comment still needs only read access to the record. A parent that is not `controlled_by_parent` is judged exactly as before, and a `public_read_write` parent still admits every member. A kernel whose security service does not serve the master-detail write check composes no master check, and there the gates admit on the abstention as before, which is also what the parent's own update gets.
21+
22+
**What changes for you.** A member who was attaching or moderating on a child record through this gap now gets the refusal above. To give them that access, grant them edit access on the MASTER record: ownership of it, an `edit`-level record share, the `modifyAllRecords` bypass on the master object, or an app-authored `update` row-level security policy on the master that admits the row. Then their `PATCH` of the child and their attachment and moderation rights on it agree. The port types `AttachmentSharingLike` and `CommentSharingLike` now pick `checkEdit` rather than `canEdit`. The registered sharing service serves both, so a caller passing it is unaffected. `installAttachmentAccessHooks` and `installCommentAccessHooks` take an optional trailing resolver for the security service (new port types `AttachmentSecurityLike` and `CommentSecurityLike`). The two plugins pass it themselves.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
---
4+
5+
The registered `security` service serves `checkControlledByParentWrite(object, recordId, context)`, the ADR-0055 master-detail write check that `ISecurityService` declares as optional.
6+
7+
- **What it answers.** It answers what a by-id update of the record gets from the master-detail write check. It runs the write path's own composition and never re-derives it: the engine middleware's context prologue (a principal-less context, permission sets that cannot be resolved and a missing on-behalf-of delegator are refused with the same `403 PERMISSION_DENIED`), and then the check for the principal's permission sets and, on a delegated context, for the delegator's (ADR-0090 D10). The first refusal is the answer. A refusal resolves `deny` with the leg it came from, the three non-verdicts resolve `unresolvable` with their reason, and an object that is not `controlled_by_parent` resolves `not_applicable`. A system context resolves `allow`. A store fault rejects with the engine's own error, so a datasource outage keeps its `503`.
8+
- **Where it differs from the write path's guard.** A principal whose permission sets resolve to none is judged over that empty list, so it answers `deny` on `object_permission` as the contract declares. A principal with positions or sets and no user id is also judged, where the write path skips the check. It answers `deny` when it holds no `update` grant on the master, and its own update is refused by the object-level gate anyway.
9+
- **Who asks it.** The `sys_attachment` parent gate in `@objectstack/service-storage` and the `sys_comment` moderation gate in `@objectstack/plugin-audit` ask it, so they judge a `controlled_by_parent` parent through its master.
10+
- The boot line that announces the `security` service now lists the members the registered service actually serves, read from the service object. The hand-kept list it replaced had fallen behind by five members.
11+
12+
Nothing an author writes changes, and no by-id write is judged differently. The middleware's context prologue moved into one method that the middleware and the member both call. Its refusals, their order and their wording are unchanged.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
A `validate()` preview binds the master-detail header the write binds, so an import dry run of a detail row whose field rule reads `parent` admits the rows the import admits (#22474)
6+
7+
Clause-②: yes
8+
9+
The preview's accept set widens: rows it refused, and the write admits, are now admitted. No key, export or error code is added, removed or renamed.
10+
11+
- **Before.** A preview (`engine.validate(object, rows, { mode })`, `validateData`, and the import dry run) bound no master-detail header. A field `requiredWhen` that reads the header, such as `parent.status == 'sent'` on an invoice line, faulted there and the preview refused the row with `rule_violation` / `unevaluable`, in both modes, although the insert and the update resolve the header and admit it under a draft header.
12+
- **Now.** The preview binds `parent` as the write binds it: from the row's master-detail reference on an insert, and on an update from the reference the patch carries, else from the stored row the row's `id` names. An update that moves the row to another header also binds the header the stored row hangs off, which the write reads to decide whether the stored row already violated the rule. So a row under a draft header is admitted and a row that leaves the field empty under a sent header is refused with `required`, as the write refuses it. Only an object that declares a field `requiredWhen` reading `parent` reads a header, as on the write.
13+
- **Read under the caller's access.** The write reads the header with system access, kept to the caller's organization. The preview first reads it through the engine's read door, under the caller's own context: a header the caller cannot read gets the verdict a missing header gets. A header column is judged only where the read door served this caller that very value. A column the read door hid, or served transformed (a partially masked field, a masked secret), is judged as empty, so the verdict never depends on a header value the caller could not read in full. For such a caller the preview can therefore answer differently from the write, which judges the stored header.
14+
- **Unchanged.** An object with no field `requiredWhen` reading `parent` reads no header and gets the verdict it got before. The preview still runs no `readonlyWhen` strip, so an update whose `readonlyWhen` would keep the row on its stored header is judged against the header the patch names.

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,44 @@ start_date: Field.date({ label: 'Start Date', required: true }),
9999
created_at: Field.datetime({ label: 'Created At', readonly: true }),
100100
```
101101

102+
#### Deadlines: `dueLike` and `settledWhen`
103+
104+
A date is a **deadline** only when its field says so. `dueLike: true` on a
105+
`date` or `datetime` field declares one: once the date has passed, a renderer
106+
may show it as overdue, with relative wording ("Overdue 3d") and an overdue
107+
colour. Without the key the field is not a deadline, and nothing is inferred
108+
from its name — a `due_date` that is not declared is just a date.
109+
110+
A deadline usually stops mattering when the record is finished, and only the
111+
object knows what finished means. `settledWhen` says it: a CEL predicate over
112+
the record, in the same family as `visibleWhen`, and while it is `TRUE` no
113+
overdue wording or colour applies to that record.
114+
115+
```typescript
116+
import { P } from '@objectstack/spec';
117+
import { Field } from '@objectstack/spec/data';
118+
119+
due_date: Field.date({
120+
label: 'Due Date',
121+
dueLike: true,
122+
settledWhen: P`record.status == 'done'`,
123+
}),
124+
```
125+
126+
Both keys are judged when the metadata is authored, not when a view renders it:
127+
128+
- Either key on any other field type is refused, and so is `settledWhen`
129+
without `dueLike: true` — a settle condition on a date that is no deadline
130+
would change nothing.
131+
- `settledWhen` is checked exactly like `visibleWhen`: it must parse, read
132+
fields as `record.<field>`, name fields the object declares, and read no root
133+
but `record` (plus `previous`, and `parent` on a master-detail line item).
134+
`objectstack validate`, `objectstack build` and saving the object all refuse
135+
anything else, naming the field.
136+
137+
Both are display semantics: nothing on the write path reads them, and a past
138+
deadline never blocks a save.
139+
102140
### Boolean Type
103141

104142
```typescript
@@ -352,6 +390,7 @@ For values that must be encrypted at rest (API keys, tokens, DB passwords), use
352390
| `visibleWhen` | `string \| Expression` | CEL predicate; field is shown only when `TRUE` |
353391
| `readonlyWhen` | `string \| Expression` | CEL predicate; field is read-only when `TRUE` |
354392
| `requiredWhen` | `string \| Expression` | CEL predicate; field is required when `TRUE` |
393+
| `settledWhen` | `string \| Expression` | CEL predicate on a `dueLike` date; the deadline is settled when `TRUE` — see [Deadlines](#deadlines-duelike-and-settledwhen) |
355394
| `multiple` | `boolean` | Allow multiple values (for select, lookup, file) |
356395

357396
```typescript

0 commit comments

Comments
 (0)