@@ -1222,8 +1222,9 @@ export class RestServer {
12221222 }
12231223
12241224 /**
1225- * Reject anonymous requests with HTTP 401 when `api.requireAuth` is set.
1226- * Returns `true` if the response was sent and the caller should stop
1225+ * Reject anonymous requests with HTTP 401 — unconditionally (#3963: the
1226+ * `api.requireAuth` opt-out is retired). Returns `true` if the response was
1227+ * sent and the caller should stop
12271228 * processing. Returns `false` to continue.
12281229 *
12291230 * The check is intentionally narrow: only `context?.userId` counts as
@@ -1232,7 +1233,7 @@ export class RestServer {
12321233 */
12331234 private enforceAuth ( req : any , res : any , context : any ) : boolean {
12341235 // ADR-0069 — authentication-policy gate (password expiry, enforced MFA).
1235- // Independent of `requireAuth` : a gated session (carrying `authGate`) is
1236+ // Independent of the anonymous-deny : a gated session (carrying `authGate`) is
12361237 // blocked from protected resources, while the core allow-list keeps auth
12371238 // + remediation reachable. Runs before the anonymous check.
12381239 const gate = context ?. authGate ;
@@ -2544,23 +2545,22 @@ export class RestServer {
25442545 }
25452546
25462547 /**
2547- * Register the metadata routes behind the SAME `requireAuth` gate the
2548+ * Register the metadata routes behind the SAME anonymous-deny gate the
25482549 * `/data` routes use.
25492550 *
25502551 * `registerMetadataEndpoints` builds ~17 `/meta/*` routes but — unlike the
25512552 * `/data` handlers — never calls {@link enforceAuth}: its handlers assumed
2552- * the `requireAuth` gate rejected anonymous callers "upstream", yet nothing
2553- * upstream covers `/meta`, so on a `requireAuth` deployment an anonymous
2554- * caller could read object / field schemas. On a tenant-less runtime host
2555- * those are SYSTEM-object schemas and the host is publicly reachable — a
2556- * real leak.
2553+ * the anonymous-deny rejected anonymous callers "upstream", yet nothing
2554+ * upstream covers `/meta`, so an anonymous caller could read object / field
2555+ * schemas. On a tenant-less runtime host those are SYSTEM-object schemas and
2556+ * the host is publicly reachable — a real leak.
25572557 *
25582558 * Rather than add the gate to every handler (and have the next new route
25592559 * forget it — the exact failure mode that caused this), wrap the route
25602560 * registrar for the duration of registration so every meta route, present
2561- * and future, inherits it. The check is a no-op when `requireAuth` is off
2562- * (demo / single-tenant), so the previously- public metadata surface there
2563- * is unchanged; an authenticated user passes exactly as on `/data` .
2561+ * and future, inherits it. An authenticated user passes exactly as on
2562+ * `/data`; the one exception is the declaration-derived public-book read
2563+ * (#3963), handled just below .
25642564 */
25652565 private registerMetadataEndpoints ( basePath : string ) : void {
25662566 const realRouteManager = this . routeManager ;
@@ -2579,7 +2579,7 @@ export class RestServer {
25792579 const context = await this . resolveExecCtx ( environmentId , req ) . catch ( ( ) => undefined ) ;
25802580 // [#3963] `audience: 'public'` is a DECLARED capability, so it
25812581 // must not depend on a deployment flipping its whole data plane
2582- // open (`requireAuth: false`) . An anonymous read of the
2582+ // open. An anonymous read of the
25832583 // book/doc surface skips the anonymous-deny and is authorized
25842584 // instead by the ADR-0046 §6.7 audience gate inside the handler
25852585 // — the same declaration-derived shape ADR-0056 Option A chose
@@ -2756,7 +2756,7 @@ export class RestServer {
27562756 // privileged apps (Studio, Setup, etc.) and gated nav
27572757 // items are stripped before reaching the client. We
27582758 // intentionally leave anonymous responses untouched —
2759- // the existing `requireAuth` gate (when enabled) blocks
2759+ // the anonymous-deny gate blocks
27602760 // them upstream; when disabled, the demo / public
27612761 // surface keeps its prior behaviour.
27622762 //
@@ -5091,14 +5091,14 @@ export class RestServer {
50915091 * GET {basePath}/forms/:slug → resolved form spec
50925092 * POST {basePath}/forms/:slug/submit → INSERT record (no auth required)
50935093 *
5094- * Both routes bypass `enforceAuth` even when `requireAuth=true` on the
5094+ * Both routes bypass `enforceAuth` even though anonymous-deny is on for the
50955095 * deployment (e.g. ObjectOS multi-tenant). Security is delegated to the
50965096 * `guest_portal` permission set carried on the execution context — the
50975097 * SecurityPlugin enforces INSERT-only access to the target object. If
50985098 * the deployment hasn't registered a `guest_portal` profile, the
50995099 * security middleware falls open with `permissions: []` (no userId),
51005100 * matching the existing anonymous-access semantics; deployers must
5101- * keep `requireAuth=true` deployments paired with a `guest_portal`
5101+ * keep secure-by-default deployments paired with a `guest_portal`
51025102 * profile (the CRM example does this) to enforce the INSERT-only
51035103 * contract.
51045104 *
@@ -5370,7 +5370,7 @@ export class RestServer {
53705370 // form — a narrow create grant scoped to exactly this form's target
53715371 // object. The SecurityPlugin honors `publicFormGrant` (create + the
53725372 // immediate read-back, that object ONLY), so public forms work under
5373- // secure-by-default (requireAuth ) WITHOUT a deployment-configured
5373+ // secure-by-default (anonymous-deny ) WITHOUT a deployment-configured
53745374 // `guest_portal`. `guest_portal` + `anonymous` are kept for back-compat
53755375 // with object hooks (guest detection via falsy `ctx.user?.id`).
53765376 const context : any = {
@@ -5728,8 +5728,8 @@ export class RestServer {
57285728 const context = await this . resolveExecCtx ( environmentId , req ) ;
57295729 if ( this . enforceAuth ( req , res , context ) ) return ;
57305730 if ( ! context ?. userId ) {
5731- // Even on requireAuth=false deployments the explain surface
5732- // stays authenticated-only — it is an admin diagnosis tool.
5731+ // The explain surface stays authenticated-only — it is an
5732+ // admin diagnosis tool. (Anonymous is already 401ed above.)
57335733 return res . status ( 401 ) . json ( {
57345734 code : 'UNAUTHORIZED' ,
57355735 message : 'The access-explanation endpoint requires an authenticated caller.' ,
@@ -7125,7 +7125,7 @@ export class RestServer {
71257125 // (ADR-0049) was enforced on A while B was written. Zod also
71267126 // strips unknown keys, which keeps a body `context` from
71277127 // becoming the execution context on a deployment where none
7128- // resolves (anonymous-reachable `requireAuth: false` ).
7128+ // resolves (e.g. an anonymous public-book read, #3963 ).
71297129 const { UpdateManyDataRequestSchema } = await import ( '@objectstack/spec/api' ) ;
71307130 const updateManyInput = { ...( req . body ?? { } ) , object : req . params . object } ;
71317131 const parsedUpdate = ( UpdateManyDataRequestSchema as any ) . safeParse ( updateManyInput ) ;
0 commit comments