Skip to content

Commit 0e67bba

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-jqgfyc
2 parents bf270bc + 2678600 commit 0e67bba

32 files changed

Lines changed: 1473 additions & 49 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/core': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
fix(console): Create User left without an email marks the Email input as required, in the user's language
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
Create User with neither an email nor a phone number answers `400 VALIDATION_FAILED` with one `required` entry per input that would satisfy it on `error.fields`. The Console showed only the server's English sentence above the dialog's buttons and marked no input.
12+
13+
- **`@object-ui/core`**: `refusalFieldErrors`, the reader behind `ActionResult.fieldErrors`, now also reads a refusal's `error.fields` (the spec's `EnhancedApiErrorSchema.fields`), beside `error.details.fields` and the legacy inner `fields`. It is exported.
14+
- **`@object-ui/app-shell`**: a refused `type: 'api'` action carries the refusal's field entries as `fieldErrors`, on the list and record pages alike. The action parameter dialog marks each `required` entry on the param it names, with its own translated "required" sentence and the param's label. When every entry is marked, the refusal box is not drawn. A refusal that names no field, or names one the dialog does not draw, keeps the box with the server's text.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
'@objectstack/plugin-hono-server': patch
4+
---
5+
6+
feat(plugin-hono-server,plugin-sharing): `GET /auth/me/permissions` answers `hasAnyQueue`, whether the caller's organization has at least one ownership queue
7+
8+
Clause-②: yes
9+
Metadata-protocol: none
10+
11+
The console's list toolbar offers an "in a queue" toggle, and it needs to know, once per session, whether that toggle can ever return a row. A plain member cannot find out: `member_default` names no `sys_queue` entry, so `GET /data/sys_queue` answers 403. The server now answers the one question for them.
12+
13+
- **The field.** Every authenticated `/auth/me/permissions` body carries `hasAnyQueue: boolean`. It is `true` when the caller's organization has at least one ownership queue: a declared queue the activation ledger has not switched off, or a `sys_queue` row of the organization. A row under a declared queue's name does not count, since that name resolves nowhere. Only the boolean is disclosed: no names and no counts. An anonymous caller still gets `{ "authenticated": false }` and nothing else.
14+
- **Where it is resolved.** `OwnershipQueueService.hasAnyQueue(organizationId)` in `@objectstack/plugin-sharing`, under system context, with the same registry-first read and activation-ledger filter `queuesOf` uses.
15+
- **When there is no answer.** No ownership-queue service registered, a service without the method, or a service that throws answers `false`. A throw is logged at `warn` and does not cost the caller the rest of the permissions body. The closed body the handler answers on a fault carries `false` too.
16+
- **Unchanged.** A member's read of `sys_queue` and `sys_queue_member` stays refused. No route is added.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): an object extension can carry a per-field display patch (`fieldPatches`), its keys derived from `BASE_FIELD_DISPLAY_WHITELIST`; not applied yet
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
ADR-0146 D10's second rule: a customer changes a vendor field's display properties without taking the field over, so the field's type, values and rules stay the vendor's and the vendor's next version still applies. `ObjectExtensionSchema` gains one optional key for it:
11+
12+
```ts
13+
objectExtensions: [{
14+
extend: 'crm_account',
15+
fieldPatches: {
16+
industry: {
17+
label: 'Sector',
18+
inlineHelpText: 'The market the account sells into',
19+
options: { tech: { label: 'Technology' } },
20+
},
21+
},
22+
}]
23+
```
24+
25+
- **Keys.** A patch takes the field keys of `BASE_FIELD_DISPLAY_WHITELIST.field` (`label`, `description`, `inlineHelpText`, `placeholder`) plus `options`, a map from an option's stored `value` to that option's patch, which takes the keys of `BASE_FIELD_DISPLAY_WHITELIST.option` (`label`). Both sets are read from the constant: widening the whitelist widens the slot.
26+
- **Refused at parse, each with a prescription.** Every other field key (`type`, `required`, `unique`, `reference`, `defaultValue`, `expression`, …) and every other option key (`color`, `default`, …) is refused by name, naming the whitelist and stating that it stays the vendor's (ADR-0146 D6). An option's `value` is refused: the option is addressed by it, and it cannot be renamed. A field's option list under `options` is refused as membership. A key spelled as an extension field (`region__c`) is refused with "declare it in `fields`; it is your own field".
27+
- **Pure shape.** Whether the target object has the field or the option, and who may patch it, are the server's to judge.
28+
- **Not enforced yet.** The slot's description reads `[EXPERIMENTAL — not enforced]`. The engine's extension registration does not carry the key, so a patch is parsed and then dropped until the registry merge applies it (ADR-0146 rollout card N-23d). Nothing reads it before then.
29+
30+
An extension without `fieldPatches` parses exactly as before.

‎content/docs/references/api/package-api-assembled.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Installed package row whose manifest is the assembled package body
112112
| **functions** | `Record<string, string \| { handler?: string; effect?: Enum<'pure' \| 'writes'> }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
113113
| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
114114
| **translations** | `Record<string, { objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }>[]` | optional | I18n Translation Bundles |
115-
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
115+
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; fieldPatches?: Record<string, object>; label?: string; … }[]` | optional | Extensions to objects owned by other packages |
116116
| **picklists** | `{ name: string; label: string; description?: string; options: object[]; … }[]` | optional | Shared option lists that select fields reference by name |
117117
| **picklistExtensions** | `{ extend: string; options: object[] }[]` | optional | Options added to picklists owned by other packages (additive only) |
118118
| **apps** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; icon?: string; … }[]` | optional | Applications |
@@ -362,7 +362,7 @@ This schema accepts one of the following structures:
362362
| **functions** | `Record<string, string \| { handler?: string; effect?: Enum<'pure' \| 'writes'> }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
363363
| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
364364
| **translations** | `Record<string, { objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }>[]` | optional | I18n Translation Bundles |
365-
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
365+
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; fieldPatches?: Record<string, object>; label?: string; … }[]` | optional | Extensions to objects owned by other packages |
366366
| **picklists** | `{ name: string; label: string; description?: string; options: object[]; … }[]` | optional | Shared option lists that select fields reference by name |
367367
| **picklistExtensions** | `{ extend: string; options: object[] }[]` | optional | Options added to picklists owned by other packages (additive only) |
368368
| **apps** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; icon?: string; … }[]` | optional | Applications |

‎content/docs/references/data/object.mdx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ const result = ApiMethod.parse(data);
576576
| :--- | :--- | :--- | :--- |
577577
| **extend** | `string` | ✅ | Target object name (FQN) to extend |
578578
| **fields** | `Record<string, { name?: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; description?: string; … }>` | optional | Fields to add/override |
579+
| **fieldPatches** | `Record<string, { label?: string; description?: string; inlineHelpText?: string; placeholder?: string; … }>` | optional | [EXPERIMENTAL — not enforced] Display patches of the target object's own fields, keyed by field name: change a vendor field's display properties (BASE_FIELD_DISPLAY_WHITELIST: label, description, help text, placeholder, option labels) without taking the field over, so its type, values and rules stay the vendor's and its upgrades still apply (ADR-0146 D10). Nothing applies a patch yet: the registry merge that does is rollout card N-23d, and until it lands the engine drops this slot at registration. Example: `fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into', options: { tech: { label: 'Technology' } } } }`. |
579580
| **label** | `string` | optional | Override label for the extended object |
580581
| **pluralLabel** | `string` | optional | Override plural label for the extended object |
581582
| **description** | `string` | optional | Override description for the extended object |
@@ -669,6 +670,16 @@ const result = ApiMethod.parse(data);
669670
| **_packageVersion** | `string` | optional | Owning package version. |
670671
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
671672

673+
### Nested Shape: `ObjectExtension.fieldPatches[string]`
674+
675+
| Property | Type | Required | Description |
676+
| :--- | :--- | :--- | :--- |
677+
| **label** | `string` | optional | Human readable label |
678+
| **description** | `string` | optional | Tooltip/Help text |
679+
| **inlineHelpText** | `string` | optional | Help text displayed below the field in forms |
680+
| **placeholder** | `string` | optional | Placeholder text rendered inside the empty input (the HTML placeholder attribute); disappears once a value is entered. Distinct from `inlineHelpText` (always-visible help rendered beside/under the input) and `description` (tooltip/developer documentation). |
681+
| **options** | `Record<string, { label?: string }>` | optional | Display patches of the base field's options, keyed by each option's stored `value` (which stays the vendor's); each entry takes the option display keys of BASE_FIELD_DISPLAY_WHITELIST. |
682+
672683
### Nested Shape: `ObjectExtension.indexes[number]`
673684

674685
| Property | Type | Required | Description |

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th
2121

2222
| Dir | Sites | strict | passthrough | catchall | strip |
2323
|---|---|---|---|---|---|
24-
| `data/` | 156 | 90 | 2 | 0 | 64 |
24+
| `data/` | 158 | 92 | 2 | 0 | 64 |
2525

2626
## `data/` — sites
2727

@@ -50,22 +50,22 @@ classify and is not listed (it becomes reportable the day it grows its first sit
5050
| `hook-body.zod.ts` | 2 |
5151
| `hook.zod.ts` | 8 |
5252
| `mapping.zod.ts` | 5 |
53-
| `object.zod.ts` | 23 |
53+
| `object.zod.ts` | 25 |
5454
| `picklist.zod.ts` | 3 |
5555
| `query.zod.ts` | 5 |
5656
| `seed-file.zod.ts` | 2 |
5757
| `seed-loader.zod.ts` | 12 |
5858
| `seed.zod.ts` | 2 |
5959
| `validation.zod.ts` | 6 |
60-
| **total** | **156** |
60+
| **total** | **158** |
6161

6262
## `data/` — open
6363

6464
Per file, how many of its sites still silently discard unknown keys. The `Class`
6565
column that decides the bucket split is hand-written in the ledger; the arithmetic
6666
over it is here.
6767

68-
**64 strip of 156**, in 10 file(s).
68+
**64 strip of 158**, in 10 file(s).
6969

7070
| File | Strip | Sites |
7171
|---|---|---|
@@ -79,7 +79,7 @@ over it is here.
7979
| `hook.zod.ts` | 5 | 8 |
8080
| `query.zod.ts` | 4 | 5 |
8181
| `seed-loader.zod.ts` | 12 | 12 |
82-
| **total** | **64** | **156** |
82+
| **total** | **64** | **158** |
8383

8484
| Bucket | Sites |
8585
|---|---|
Lines changed: 7 additions & 0 deletions
Loading

‎packages/apps/connector-github/objectstack.manifest.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"category": "devtools",
99
"tags": ["github", "mcp", "connector", "issues"],
1010
"license": "Apache-2.0",
11+
"iconUrl": "https://raw.githubusercontent.com/objectstack-ai/objectstack/main/packages/apps/connector-github/assets/icon.svg",
1112
"homepageUrl": "https://github.com/objectstack-ai/objectstack/tree/main/packages/apps/connector-github",
1213
"readmePath": "README.md"
1314
}

‎packages/apps/connector-github/test/listing.test.ts‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,43 @@
33
/**
44
* The listing this package publishes as: `objectstack.manifest.json` parses
55
* against the spec's own schema for that file, names the same package id as
6-
* the stack, points at a README that exists, and that README's credentials
6+
* the stack, points at a README that exists and at the icon the package owns
7+
* (a public http(s) `iconUrl`), and that README's credentials
78
* section names every credential the app's connectors reference — the
89
* marketplace handbook's "every credential it needs" rule, checked against
910
* the metadata rather than trusted.
1011
*/
1112

1213
import { describe, it, expect } from 'vitest';
13-
import { readFileSync, existsSync } from 'node:fs';
14+
import { readFileSync, existsSync, statSync } from 'node:fs';
1415
import { fileURLToPath } from 'node:url';
1516
import { TemplateManifestSchema } from '@objectstack/spec/marketplace';
1617
import stack from '../objectstack.config.js';
1718

1819
const manifestPath = fileURLToPath(new URL('../objectstack.manifest.json', import.meta.url));
1920
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;
2021

22+
/** Where the listing icon is served from once this package's tree is on `main`. */
23+
const ICON_URL = 'https://raw.githubusercontent.com/objectstack-ai/objectstack/main/packages/apps/connector-github/assets/icon.svg';
24+
25+
/**
26+
* Why a manifest's `iconUrl` cannot be the listing icon, or `null` when it can:
27+
* `os package publish` sends the key as the package's `icon_url`, and the
28+
* marketplace handbook asks for a public http(s) URL. `TemplateManifestSchema`
29+
* accepts any URL scheme, so the scheme is checked here.
30+
*/
31+
function iconUrlProblem(m: Record<string, unknown>): string | null {
32+
const value = m.iconUrl;
33+
if (typeof value !== 'string') return 'iconUrl is absent';
34+
let url: URL;
35+
try {
36+
url = new URL(value);
37+
} catch {
38+
return `iconUrl is not a URL: ${value}`;
39+
}
40+
return url.protocol === 'https:' || url.protocol === 'http:' ? null : `iconUrl is not http(s): ${value}`;
41+
}
42+
2143
/** The README's `## Credentials` section, up to the next second-level heading. */
2244
function credentialsSection(readme: string): string {
2345
const start = readme.indexOf('\n## Credentials\n');
@@ -55,6 +77,27 @@ describe('connector-github listing', () => {
5577
expect(section).toContain('https://github.com/settings/personal-access-tokens/new');
5678
});
5779

80+
it('carries a public http(s) iconUrl', () => {
81+
expect(iconUrlProblem(manifest)).toBeNull();
82+
});
83+
84+
it('refuses an iconUrl that is absent or not http(s)', () => {
85+
const withoutIcon = { ...manifest };
86+
delete withoutIcon.iconUrl;
87+
expect(iconUrlProblem(withoutIcon)).toBe('iconUrl is absent');
88+
expect(iconUrlProblem({ ...manifest, iconUrl: 'ftp://example.com/icon.svg' })).toMatch(/not http\(s\)/);
89+
expect(iconUrlProblem({ ...manifest, iconUrl: 'assets/icon.svg' })).toMatch(/not a URL/);
90+
});
91+
92+
it('points iconUrl at the icon this package owns', () => {
93+
expect(manifest.iconUrl).toBe(ICON_URL);
94+
const iconPath = fileURLToPath(new URL('../assets/icon.svg', import.meta.url));
95+
expect(existsSync(iconPath)).toBe(true);
96+
// `--icon-file` takes up to 256 KB; hold the owned asset to the same bound.
97+
expect(statSync(iconPath).size).toBeLessThan(256 * 1024);
98+
expect(readFileSync(iconPath, 'utf8')).toMatch(/^<svg [^>]*viewBox="0 0 256 256"/);
99+
});
100+
58101
it('carries no secret in metadata', () => {
59102
for (const c of stack.connectors ?? []) {
60103
expect(c).not.toHaveProperty('authentication.token');
Lines changed: 7 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)