@@ -86,8 +86,9 @@ export function appDefaultPermissionSetName(permissions: unknown): string | unde
8686}
8787
8888/**
89- * [ADR-0130 D4, #15007] Every permission set a stack config DECLARES — from the
90- * flattened top level, and from `packages[]`.
89+ * [ADR-0130 D4, #15007] The app-declared default permission-set NAME, resolved
90+ * from wherever the artifact carries the declaration — the flattened top
91+ * level, or `packages[]`.
9192 *
9293 * ## What this exists to stop
9394 *
@@ -114,52 +115,70 @@ export function appDefaultPermissionSetName(permissions: unknown): string | unde
114115 * this function has to be a superset of the old read rather than a replacement
115116 * for it: for every artifact the platform emits today the flattened level
116117 * answers first and this returns exactly what it returned before. The
117- * `packages[]` pass only supplies a set where the top level had none — which is
118- * precisely the option-B artifact. That is what makes this card revertible on
119- * its own and safe to land before the emitter half (#14512).
118+ * `packages[]` pass is consulted ONLY where the top level named no default —
119+ * which is precisely the option-B artifact. That is what makes this card
120+ * revertible on its own and safe to land before the emitter half (#14512).
121+ *
122+ * ## The condition is the ANSWER, never the container
123+ *
124+ * "The top level had none" is spelled as `appDefaultPermissionSetName` coming
125+ * back `undefined`, and deliberately NOT as the `permissions` array being
126+ * absent or empty. Branching on the container re-creates the silent loss this
127+ * card exists to remove, one shape further along: a config whose flattened
128+ * level carries permission sets but marks none of them `isDefault` — legal
129+ * today, and expressible by hand in any `objectstack.config.ts` — would
130+ * short-circuit the whole `packages[]` pass and resolve `undefined`, with
131+ * nothing thrown and nothing logged. Reading the container also hands back the
132+ * `[]`-is-truthy trap for free. The answer is the only condition that cannot
133+ * be wrong in either direction, so the answer is what this branches on.
120134 *
121135 * ## The order is `resolveArtifactPackageOrder`'s, not the array's
122136 *
123- * `appDefaultPermissionSetName` resolves the FIRST `isDefault` set , so with more
124- * than one package declaring one, "first" has to mean the same thing here as it
125- * does everywhere else the artifact is read. `resolveArtifactPackageOrder`
137+ * The first package body that names a default wins , so with more than one
138+ * package declaring one, "first" has to mean the same thing here as it does
139+ * everywhere else the artifact is read. `resolveArtifactPackageOrder`
126140 * (`@objectstack/core`, ADR-0130 D4+D5, #14643) is the ONE place that turns an
127141 * artifact into its ordered package list — dependency-topological, so a package
128142 * that extends another is read after it regardless of which array slot it
129143 * occupies. ⛔ Do not iterate `config.packages` directly here; a second
130144 * traversal is a second ordering, and the depended-upon package would win or
131145 * lose by authoring accident.
132146 *
133- * ## Two things it deliberately does NOT do
134- *
135- * • It does not look inside the SINGULAR `manifest`. That constraint is
136- * #7001's and it still holds — the harness must not honour a declaration
137- * `serve.ts` ignores. Note this is not a special case bolted on: an
138- * artifact carrying no `packages` key makes `resolveArtifactPackageOrder`
139- * return the caller's own object as the single package body (D4's second
140- * branch, D7's compatibility term), so that branch reads `permissions` from
141- * exactly where the old code read it and nowhere else.
142- * • It does not catch `resolveArtifactPackageOrder`'s refusals. A malformed
143- * `packages` (not an array, an unwrapped entry, a duplicate package id)
144- * raises an ADR-0112 envelope here, the same one the manifest service
145- * raises when it registers that artifact moments later. Swallowing it would
146- * resolve a permission surface out of an artifact the loader refuses to
147- * load — the gate travels with the read.
147+ * ## The package order is resolved BEFORE the top level is consulted
148+ *
149+ * Reading that line as a misplaced statement is the expected mistake, so: it is
150+ * placed there on purpose, and moving it below the early return is a behaviour
151+ * change. `resolveArtifactPackageOrder` REFUSES a malformed `packages` (not an
152+ * array, an unwrapped entry, a duplicate package id) with an ADR-0112 envelope,
153+ * and this reader does not catch it — swallowing it would resolve a permission
154+ * surface out of an artifact the loader refuses to load. Resolving the order
155+ * first is what keeps that refusal unconditional: an artifact is either
156+ * loadable or refused, and which answer this reader gives about it must not
157+ * depend on whether its flattened level happened to name a default first.
158+ *
159+ * ## One thing it deliberately does NOT do
160+ *
161+ * It does not look inside the SINGULAR `manifest`. That constraint is #7001's
162+ * and it still holds — the harness must not honour a declaration `serve.ts`
163+ * ignores. Note this is not a special case bolted on: an artifact carrying no
164+ * `packages` key never reaches the package pass at all, so that branch reads
165+ * `permissions` from exactly where the old code read it and nowhere else.
148166 */
149- function declaredPermissionSets ( config : unknown ) : unknown [ ] {
150- const sets : unknown [ ] = [ ] ;
167+ function declaredDefaultPermissionSetName ( config : unknown ) : string | undefined {
168+ const packages = ( config as { packages ?: unknown } | null | undefined ) ?. packages ;
169+ const bodies =
170+ packages === undefined || packages === null ? [ ] : resolveArtifactPackageOrder ( config ) ;
151171
152172 const flattened = ( config as { permissions ?: unknown } | null | undefined ) ?. permissions ;
153- if ( Array . isArray ( flattened ) ) sets . push ( ...flattened ) ;
154-
155- const packages = ( config as { packages ?: unknown } | null | undefined ) ?. packages ;
156- if ( packages === undefined || packages === null ) return sets ;
173+ const fromFlattened = appDefaultPermissionSetName ( flattened ) ;
174+ if ( fromFlattened !== undefined ) return fromFlattened ;
157175
158- for ( const body of resolveArtifactPackageOrder ( config ) ) {
176+ for ( const body of bodies ) {
159177 const declared = ( body as { permissions ?: unknown } | null | undefined ) ?. permissions ;
160- if ( Array . isArray ( declared ) ) sets . push ( ...declared ) ;
178+ const fromPackage = appDefaultPermissionSetName ( declared ) ;
179+ if ( fromPackage !== undefined ) return fromPackage ;
161180 }
162- return sets ;
181+ return undefined ;
163182}
164183
165184/**
@@ -191,13 +210,14 @@ function declaredPermissionSets(config: unknown): unknown[] {
191210 * the result straight through — `new SecurityPlugin(appSecurityPluginOptions(config))`
192211 * — and a caller cannot get the undefined case subtly wrong.
193212 *
194- * Reads the sets through {@link declaredPermissionSets} — the flattened top
195- * level `serve.ts` has always read, and, for a multi-package artifact, the
196- * `packages[]` bodies that carry the same declaration under ADR-0130 D4.
213+ * Resolves the name through {@link declaredDefaultPermissionSetName} — the
214+ * flattened top level `serve.ts` has always read, and, for a multi-package
215+ * artifact, the `packages[]` bodies that carry the same declaration under
216+ * ADR-0130 D4.
197217 */
198218export function appSecurityPluginOptions (
199219 config : unknown ,
200220) : { fallbackPermissionSet : string } | undefined {
201- const name = appDefaultPermissionSetName ( declaredPermissionSets ( config ) ) ;
221+ const name = declaredDefaultPermissionSetName ( config ) ;
202222 return name ? { fallbackPermissionSet : name } : undefined ;
203223}
0 commit comments