@@ -649,37 +649,12 @@ async function answerMetaItem(
649649 }
650650}
651651
652- /** [#20320] This transport's save-door admission of `:type/:name` — see {@link metaSaveVerdictOf }. */
652+ /** [#20320] This transport's save-door admission of `:type/:name` — see `saveVerdict` in {@link handleMetadataRequest }. */
653653type MetaSaveVerdict = (
654654 canonicalType : string ,
655655 activeOrganizationId : string | undefined ,
656656) => ReturnType < typeof metaWriteCapabilityVerdict > ;
657657
658- /**
659- * [#12702 · #20320 · #20478] May this caller SAVE `:type/:name` on THIS
660- * transport? The admission of the item branch's `PUT`, spelled ONCE: that door
661- * asks it, and so does every read door that honours the author exemption —
662- * the `?state=draft` read and the layered view, on both of its spellings
663- * (ruling 5856774816, item 1: 「whoever can save it must see it whole, or a
664- * save drops entries silently」), which `MetaReadGateCaller.mayWriteItem` says
665- * must be the transport's own save-door answer. A second spelling at a read
666- * could drift from the door it stands for. `canonicalType` is the folded
667- * segment and `activeOrganizationId` the one resolution each caller already
668- * made, so authorization and scope read one value.
669- */
670- function metaSaveVerdictOf ( context : HttpProtocolContext ) : MetaSaveVerdict {
671- return ( canonicalType , activeOrganizationId ) => {
672- const ec : any = context . executionContext ;
673- return metaWriteCapabilityVerdict ( {
674- isSystem : ec ?. isSystem === true ,
675- systemPermissions : ec ?. systemPermissions ,
676- canonicalType,
677- activeOrganizationId,
678- operation : 'save' ,
679- } ) ;
680- } ;
681- }
682-
683658/**
684659 * [#20320] `GET /meta/:type/:name?state=draft` for a caller who may read
685660 * drafts — the pending draft ROW (ADR-0033), answered exactly as `RestServer`'s
@@ -782,12 +757,12 @@ type MetaLayeredProtocol = MetaDomainProtocol & Required<Pick<MetaDomainProtocol
782757 * ({@link metaReadOrganizationId}, the partition the plain read reads — ⛔ never
783758 * the session's claim as stored) and to `?package=` (ADR-0048). The chain
784759 * judges every present layer under `STORED_VERSION_DOOR_POLICY` — whole for a
785- * caller this transport's save door admits ({ @link metaSaveVerdictOf}, carried
786- * as `mayWriteItem`), pruned as the plain read prunes it for everyone else
787- * (ruling 5856774816) — and projects every layer through the ADR-0106 mask
788- * under the posture resolved before the read. The dispatcher served neither
789- * spelling: the route answered a located `404 ROUTE_NOT_FOUND`, and the flag
790- * answered the PLAIN read's `{ type, name, item }` with a `200`.
760+ * caller this transport's save door admits (`saveVerdict`, the `PUT` branch's
761+ * own admission, carried as `mayWriteItem`), pruned as the plain read prunes
762+ * it for everyone else (ruling 5856774816) — and projects every layer through
763+ * the ADR-0106 mask under the posture resolved before the read. The dispatcher
764+ * served neither spelling: the route answered a located `404 ROUTE_NOT_FOUND`,
765+ * and the flag answered the PLAIN read's `{ type, name, item }` with a `200`.
791766 *
792767 * The answers, in this transport's envelope, with `RestServer`'s status and
793768 * code: the layered answer (no `Vary`: it is not translated; `private,
@@ -805,12 +780,13 @@ async function answerMetaLayered(
805780 context : HttpProtocolContext ,
806781 protocol : MetaLayeredProtocol ,
807782 request : { type : string ; name : string ; packageId : string | undefined ; maskPosture : ObjectSchemaMaskPosture } ,
783+ saveVerdict : MetaSaveVerdict ,
808784 headers : Readonly < Record < string , string | undefined > > = { } ,
809785) : Promise < HttpDispatcherResult > {
810786 const { type, name, packageId, maskPosture } = request ;
811787 const caller = context . executionContext as MetaReadGateCaller | undefined ;
812788 const organizationId = metaReadOrganizationId ( type , caller ) ;
813- const mayWriteItem = metaSaveVerdictOf ( context ) ( canonicalMetaUrlType ( type ) , metaCallerOrganizationId ( caller ) ) . allowed ;
789+ const mayWriteItem = saveVerdict ( canonicalMetaUrlType ( type ) , metaCallerOrganizationId ( caller ) ) . allowed ;
814790 let answer : MetaLayeredAnswer ;
815791 try {
816792 const layered = await protocol . getMetaItemLayered ( {
@@ -881,6 +857,29 @@ function decodeMetaNameSegment(segment: string): string {
881857 */
882858export async function handleMetadataRequest ( deps : DomainHandlerDeps , path : string , _context : HttpProtocolContext , method ?: string , body ?: any , query ?: any ) : Promise < HttpDispatcherResult > {
883859 const parts = path . replace ( / ^ \/ + / , '' ) . split ( '/' ) . filter ( Boolean ) ;
860+
861+ // [#12702 · #20320] May this caller SAVE `:type/:name` on THIS transport?
862+ // The admission of the item branch's `PUT`, spelled ONCE: that door asks
863+ // it, and so does every read door that honours the author exemption — the
864+ // `?state=draft` read and [#20478] the layered view, on both of its
865+ // spellings (ruling 5856774816, item 1: 「whoever can save it must see it
866+ // whole, or a save drops entries silently」), which
867+ // `MetaReadGateCaller.mayWriteItem` says must be the transport's own
868+ // save-door answer. A second spelling at a read could drift from the door
869+ // it stands for. `canonicalType` is the folded segment and
870+ // `activeOrganizationId` the one resolution each caller already made, so
871+ // authorization and scope read one value. [#20478] Declared here, above
872+ // every branch, so the `/layers` branch asks the same one.
873+ const saveVerdict : MetaSaveVerdict = ( canonicalType , activeOrganizationId ) => {
874+ const ec : any = _context . executionContext ;
875+ return metaWriteCapabilityVerdict ( {
876+ isSystem : ec ?. isSystem === true ,
877+ systemPermissions : ec ?. systemPermissions ,
878+ canonicalType,
879+ activeOrganizationId,
880+ operation : 'save' ,
881+ } ) ;
882+ } ;
884883 // Defense-in-depth: the metadata catch-all must honour the same
885884 // anonymous-deny (#2567) as the REST `/meta` routes (which serve `/meta` on
886885 // the cloud runtime). Object/field schemas — SYSTEM-object schemas on a
@@ -1009,7 +1008,7 @@ export async function handleMetadataRequest(deps: DomainHandlerDeps, path: strin
10091008 // path fell to the located `ROUTE_NOT_FOUND` tail below. The whole answer is
10101009 // `createMetaLayeredAnswer` in `@objectstack/rest` ({@link
10111010 // answerMetaLayered}), the one `RestServer`'s handler calls. EXACTLY three
1012- // segments, like `/published` beside it (#12195) . The anonymous gate above
1011+ // segments, like `/published` beside it. The anonymous gate above
10131012 // keeps its deny here (`metaReadRouteOf` names no route), as `RestServer`'s
10141013 // does.
10151014 if ( parts . length === 3 && parts [ 2 ] === 'layers' && ( ! method || method . toUpperCase ( ) === 'GET' ) ) {
@@ -1038,7 +1037,7 @@ export async function handleMetadataRequest(deps: DomainHandlerDeps, path: strin
10381037 const packageId = query ?. package || undefined ;
10391038 return answerMetaLayered (
10401039 deps , _context , protocol as MetaLayeredProtocol ,
1041- { type, name, packageId, maskPosture } ,
1040+ { type, name, packageId, maskPosture } , saveVerdict ,
10421041 ) ;
10431042 }
10441043
@@ -1189,12 +1188,6 @@ export async function handleMetadataRequest(deps: DomainHandlerDeps, path: strin
11891188 // Extract optional package filter from query string
11901189 const packageId = query ?. package || undefined ;
11911190
1192- // [#12702 · #20320] May this caller SAVE `:type/:name` on THIS
1193- // transport? The admission of the `PUT` branch below, spelled ONCE
1194- // ({@link metaSaveVerdictOf}): that branch asks it, and so do the read
1195- // doors that honour the author exemption — the `?state=draft` read
1196- // below and [#20478] the layered view.
1197- const saveVerdict : MetaSaveVerdict = metaSaveVerdictOf ( _context ) ;
11981191
11991192 // PUT /metadata/:type/:name (Save)
12001193 //
@@ -1529,7 +1522,7 @@ export async function handleMetadataRequest(deps: DomainHandlerDeps, path: strin
15291522 if ( protocol && typeof protocol . getMetaItemLayered === 'function' ) {
15301523 return await answerMetaLayered (
15311524 deps , _context , protocol as MetaLayeredProtocol ,
1532- { type, name, packageId, maskPosture } ,
1525+ { type, name, packageId, maskPosture } , saveVerdict ,
15331526 metaItemLayersDeprecationHeaders ( requestedItemPath ( _context ) ) ,
15341527 ) ;
15351528 }
0 commit comments