Skip to content

Commit 898990e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-i7mwht
2 parents 0263a19 + 4cc17db commit 898990e

11 files changed

Lines changed: 386 additions & 73 deletions

‎docs/adr/0126-packaged-metadata-customization-model.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,10 @@ read at each runtime's own consult point; an empty ledger changes nothing anywhe
317317
> install of a package shipping a catalog name an environment-authored item holds writes its
318318
> install entry, and the next cold boot refuses; it is to be refused before the first write, not
319319
> confirmed (#25366).
320-
> - **The other classes follow after v18** (ruling item 4), as does the retirement of
321-
> `package-upgrade.zod.ts`. Until then this wall holds in practice for the access class only.
320+
> - **The other classes follow after v18** (ruling item 4). Until then this wall holds in
321+
> practice for the access class only. Item 3 retired `kernel/package-upgrade.zod.ts` whole
322+
> (#25608): its upgrade plan, diff, snapshot and rollback model had no implementation, so no
323+
> upgrade protocol stands beside this wall.
322324
4. **The paper protocol is not the model.** The three-layer
323325
`metadata-customization.zod.ts` surface (#12057 — exported, documented, zero consumers) is
324326
**superseded by this ADR as a matter of record**: nothing may build against it, and its

‎docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,9 @@ enforcer in the same change. The measured demand at `7b926f7600` is none: the re
475475
apps open public forms on a single organization, and the measurement round found the same in the
476476
hotcrm tree. The trigger is a named deployment that needs it.
477477

478+
If this is built later, the binding is opened by the organization administrator inside their own
479+
organization, never declared by the package (ruling A `6386464761`: 「若将来建,绑定由组织管理员开,不由包声明」).
480+
478481
### D6 — Disclosure and explain: unchanged
479482

480483
*Ruled: Q6 A.* ADR-0106 D7 stands: a caller resolving no set takes the configured fallback set on

‎packages/formula/src/mail-html-hole-placement.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ describe('the refused placements, by class', () => {
5959
['<style>p { color: {{x}} }</style>', 'raw-text-element'],
6060
['<noscript>{{x}}</noscript>', 'raw-text-element'],
6161
['<plaintext>{{x}}', 'raw-text-element'],
62+
// The script data double escaped state: an end tag there does not end the element.
63+
['<script><!--<script></script>{{x}}</script>', 'raw-text-element'],
6264
['<!-- {{x}} -->', 'comment'],
6365
['<!--[if mso]><p>{{x}}</p><![endif]-->', 'comment'],
6466
['<!DOCTYPE {{x}}>', 'comment'],
@@ -68,6 +70,8 @@ describe('the refused placements, by class', () => {
6870
['<p {{x}}>a</p>', 'tag'],
6971
['<{{x}}>', 'tag'],
7072
['</{{x}}>', 'tag'],
73+
// The tag open state: a character that opens no tag is reconsumed in the data state.
74+
['<<p {{x}}>a</p>', 'tag'],
7175
['<p title="a"{{x}}>a</p>', 'tag'],
7276
['<p data-{{x}}="1">a</p>', 'tag'],
7377
['<a href="{{x}}">a</a>', 'url-attribute'],
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 9] The credential carry-forward of `ObjectStackProtocolImplementation`
5+
* — the registered credential channel lookup (`credentialChannelFor`), the
6+
* save's write-path inverse of the read exits' credential redaction
7+
* (`carryForwardRedactedCredentials`), the stored body it compares against
8+
* (`storedBodyForCarryForward`) and the runtime authoring gate's half of the
9+
* same context (`restoredCredentialPathsFor`) — moved out of `protocol.ts`
10+
* whole. One member of the group stays in the class:
11+
* `readCodeLayerForCarryForward`, the code-layer read the stored body falls
12+
* back to, a `lookupArtifactItem(` site that
13+
* `protocol.lookup-artifact-item-call-sites.test.ts` reads in `protocol.ts`.
14+
*
15+
* A MOVE, not a redesign. Each function below is the body of the
16+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
17+
* re-indented one level and otherwise unedited: the same reads, in the same
18+
* order, with no `try`/`catch` added where the carry-forward forbids one. No
19+
* module-level declaration of `protocol.ts` is read by these bodies, so none
20+
* moved with them.
21+
*
22+
* ## How the moved bodies reach the class
23+
*
24+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
25+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
26+
* The class keeps every method — the published `dist/*.d.ts` lists each one's
27+
* name, docblock and declared signature — and each body is one literal of
28+
* exactly the {@link CredentialCarryForwardHost} members that method reads,
29+
* plus a `.call`. A method is handed over as a closure that calls it on the
30+
* protocol at call time, so a test double or spy set on the instance is still
31+
* the one the body reaches, and the moved bodies that call one another do so
32+
* through the protocol, exactly as before. `credentialChannelFor` keeps its
33+
* other readers in the class, the cut 8 restore doors among them, and they
34+
* still call the method.
35+
*
36+
* The one field the bodies read, `credentialChannels`, goes as a GETTER, so
37+
* the read happens when the body makes it, as for every field in this series.
38+
*
39+
* Everything else the bodies read is imported from the module `protocol.ts`
40+
* imports it from (`metadata-redaction.ts`, `@objectstack/spec/shared`); from
41+
* `protocol.ts` itself this module imports types only, so there is no
42+
* runtime module cycle. Nothing re-exports this module, so the published
43+
* surface does not move.
44+
*/
45+
46+
import type { MetadataCredentialChannel, ObjectStackProtocolImplementation } from './protocol.js';
47+
import type { SysMetadataRepository } from './sys-metadata-repository.js';
48+
import { carryForwardRedactedValues, hasMetadataRedactor, redactedPathsCarriedForward } from './metadata-redaction.js';
49+
import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
50+
51+
/**
52+
* The `ObjectStackProtocolImplementation` members the functions below read —
53+
* every one of them, and no other; each function's `this` is the `Pick` of the
54+
* members IT reads. Each is typed AS the class member (an indexed access on
55+
* the class), so a change to a member's signature on the class is a change
56+
* here too, and naming a member the class does not have is a compile error.
57+
* See the module header for which members are getters.
58+
*/
59+
export interface CredentialCarryForwardHost {
60+
credentialChannelFor: ObjectStackProtocolImplementation['credentialChannelFor'];
61+
readonly credentialChannels: ObjectStackProtocolImplementation['credentialChannels'];
62+
getOverlayRepo: ObjectStackProtocolImplementation['getOverlayRepo'];
63+
readCodeLayerForCarryForward: ObjectStackProtocolImplementation['readCodeLayerForCarryForward'];
64+
storedBodyForCarryForward: ObjectStackProtocolImplementation['storedBodyForCarryForward'];
65+
}
66+
67+
/** The body of `ObjectStackProtocolImplementation#credentialChannelFor`; the method's docblock there is its contract. */
68+
export function credentialChannelFor(
69+
this: Pick<CredentialCarryForwardHost, 'credentialChannels'>,
70+
type: string): MetadataCredentialChannel | undefined {
71+
return this.credentialChannels.get(PLURAL_TO_SINGULAR[type] ?? type);
72+
}
73+
74+
/** The body of `ObjectStackProtocolImplementation#carryForwardRedactedCredentials`; the method's docblock there is its contract. */
75+
export async function carryForwardRedactedCredentials(
76+
this: Pick<CredentialCarryForwardHost, 'storedBodyForCarryForward'>,
77+
args: {
78+
type: string;
79+
repo: SysMetadataRepository;
80+
ref: Parameters<SysMetadataRepository['get']>[0];
81+
state: 'draft' | 'active';
82+
packageId: string | null;
83+
item: any;
84+
/** [#22877] The bundle member the save addresses (its body's). */
85+
member?: string;
86+
}): Promise<any> {
87+
if (!hasMetadataRedactor(args.type)) return args.item;
88+
const body = await this.storedBodyForCarryForward(args);
89+
if (!body) return args.item;
90+
return carryForwardRedactedValues(args.type, args.item, body);
91+
}
92+
93+
/** The body of `ObjectStackProtocolImplementation#storedBodyForCarryForward`; the method's docblock there is its contract. */
94+
export async function storedBodyForCarryForward(
95+
this: Pick<CredentialCarryForwardHost, 'readCodeLayerForCarryForward'>,
96+
args: {
97+
type: string;
98+
repo: SysMetadataRepository;
99+
ref: Parameters<SysMetadataRepository['get']>[0];
100+
state: 'draft' | 'active';
101+
packageId: string | null;
102+
member?: string;
103+
}): Promise<unknown> {
104+
// [#22128] The row this save overwrites, and with no draft row yet the
105+
// active row the draft overlays: the one in the package the draft is
106+
// stamped into (`packageId`, the inherited binding included).
107+
const target = await args.repo.headAt(args.ref, {
108+
state: args.state,
109+
packageId: args.packageId,
110+
member: args.member,
111+
});
112+
let stored = target.head;
113+
if (!stored && args.state === 'draft') {
114+
stored = (await args.repo.headAt(args.ref, {
115+
state: 'active',
116+
packageId: target.packageId,
117+
member: args.member,
118+
})).head;
119+
}
120+
// [#20552] NO overlay row at either state: the body the read served is
121+
// the CODE layer, so that is what the incoming body is compared with.
122+
// Without this a code-authored item — an inbound flow whose secret is a
123+
// literal in the app's source, the shipped reference shape — had its
124+
// FIRST save persist an overlay row with the credential dropped, and
125+
// an overlay wins every later merge.
126+
return stored?.body ?? await this.readCodeLayerForCarryForward(args.type, args.ref.name, args.packageId);
127+
}
128+
129+
/** The body of `ObjectStackProtocolImplementation#restoredCredentialPathsFor`; the method's docblock there is its contract. */
130+
export async function restoredCredentialPathsFor(
131+
this: Pick<CredentialCarryForwardHost, 'credentialChannelFor' | 'getOverlayRepo' | 'storedBodyForCarryForward'>,
132+
args: {
133+
type: string;
134+
name: string;
135+
packageId: string | null;
136+
item: unknown;
137+
}): Promise<readonly string[]> {
138+
if (!hasMetadataRedactor(args.type)) return [];
139+
const repo = this.getOverlayRepo();
140+
const ref = {
141+
type: args.type,
142+
name: args.name,
143+
org: 'env',
144+
} as Parameters<typeof repo.get>[0];
145+
const body = await this.storedBodyForCarryForward({
146+
type: args.type,
147+
repo,
148+
ref,
149+
// The gate judges `active` writes only (#4463 D1), so this is the
150+
// state a gated save's carry-forward reads.
151+
state: 'active',
152+
packageId: args.packageId,
153+
});
154+
const carried = body ? redactedPathsCarriedForward(args.type, args.item, body) : [];
155+
// [#20790] A credential the write-only channel holds is withheld from
156+
// the stored body too, so the carry-forward restores nothing there —
157+
// and it is still present: the channel keeps it across this save.
158+
const held = await this.credentialChannelFor(args.type)?.heldPaths({
159+
name: args.name,
160+
state: 'active',
161+
item: args.item,
162+
});
163+
return held && held.length > 0 ? [...new Set([...carried, ...held])] : carried;
164+
}

‎packages/metadata-protocol/src/protocol-package-install.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
/** [#25467 cut 7] HEADER PLACEHOLDER */
3+
/**
4+
* [#25467 cut 7] Package install and update of `ObjectStackProtocolImplementation`
5+
* — the install primitive (`installPackage`), the manifest edit behind
6+
* `PATCH /packages/:id` (`updatePackage`), the `package` service's write half
7+
* both of them persist through (`packagePersistService`) and the undo a failed
8+
* install runs (`packageInstallUndo`) — moved out of `protocol.ts` whole. They
9+
* were the tail of the class.
10+
*
11+
* A MOVE, not a redesign. Each function below is the body of the
12+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
13+
* re-indented one level and otherwise unedited: the same parses, writes and
14+
* undo, refusing exactly where they did with the same codes and messages. The
15+
* module-level declarations only these bodies read (`persistPackageManifest`,
16+
* `snapshotPackageRow`, `restorePackageRow`) moved with them, also unedited.
17+
*
18+
* ## How the moved bodies reach the class
19+
*
20+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
21+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
22+
* The class keeps every method — the published `dist/*.d.ts` lists each one's
23+
* name, docblock and declared signature — and each body is one literal of
24+
* exactly the {@link PackageInstallHost} members that method reads, plus a
25+
* `.call`. A method is handed over as a closure that calls it on the protocol
26+
* at call time, so a test double or spy set on the instance is still the one
27+
* the body reaches, and the moved bodies that call one another
28+
* (`packagePersistService`, `packageInstallUndo`) do so through the protocol,
29+
* exactly as before.
30+
*
31+
* The fields the bodies read, `engine` and `getServicesRegistry`, go as
32+
* GETTERS, so each read happens when the body makes it: tests replace them on
33+
* a constructed protocol.
34+
*
35+
* The other module-level declarations the bodies read stay in `protocol.ts`,
36+
* where code that did not move reads them too, and are imported from there:
37+
* `packagePersistFailureError`, which the package delete also throws, and the
38+
* type `PackagePersistService`, which the class's own signatures name. The
39+
* module cycle is evaluation-safe because the one runtime import is read only
40+
* inside a function body, never while the two modules are first evaluated.
41+
* `index.ts` names what it publishes from `protocol.ts` one by one, so the
42+
* `export` they gained for this keeps them off the published surface, and
43+
* nothing re-exports this module, so the published surface does not move.
44+
*/
445

546
import type { ObjectStackProtocolImplementation, PackagePersistService } from './protocol.js';
647
import { packagePersistFailureError } from './protocol.js';

0 commit comments

Comments
 (0)