@zerotal/core 1.13.3 → 1.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,26 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.13.4] — 2026-09-01
12
+
13
+ ### Fixed
14
+
15
+ - **The site gate's staff bypass was a denylist, and failed open.** 1.13.3 admitted any
16
+ authenticated user whose role was not literally `"customer"`. In an app whose roles are
17
+ `user` and `admin` — which is most of them — that is every signed-in visitor, so a
18
+ private preview showed the site to anyone with an account. A gate that fails open is
19
+ worse than no gate, because it reports success while doing nothing.
20
+
21
+ It is now an allowlist: `gate.staffRoles`, defaulting to `["admin"]`. An app whose staff
22
+ role is named something else gets no bypass and notices, which is the safe direction to
23
+ be wrong in. The token path is unchanged.
24
+
25
+ - **The same code broke `tsc` for apps that do not use the gate.** `role !== "customer"`
26
+ is a type error when an app's role union has no such member, and the framework ships TS
27
+ source, so the error landed on a feature the app never touched. The role is read as
28
+ `string` now, because the framework cannot know an app's role names and must not narrow
29
+ to them.
30
+
11
31
  ## [1.13.3] — 2026-08-31
12
32
 
13
33
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/core",
3
- "version": "1.13.3",
3
+ "version": "1.13.4",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -49,6 +49,22 @@ interface GateConfig {
49
49
  * of your routes a third party calls, so it has to be declared.
50
50
  */
51
51
  allow?: string[];
52
+ /**
53
+ * Roles admitted to a preview without a token.
54
+ *
55
+ * An **allowlist**, defaulting to `["admin"]`. An app whose staff role is named
56
+ * something else declares it here:
57
+ *
58
+ * ```ts
59
+ * // config/gate.ts
60
+ * export default { staffRoles: ["admin", "editor"] };
61
+ * ```
62
+ *
63
+ * It is a list rather than a rule because the framework cannot know an app's
64
+ * role names, and the shape it replaced — "anyone who is not a customer" —
65
+ * admitted every signed-in user in an app that has no role called `customer`.
66
+ */
67
+ staffRoles?: string[];
52
68
  /**
53
69
  * What the public gets during a **preview** — not maintenance, which is always
54
70
  * 503.
@@ -200,8 +216,25 @@ function _isStaff(ctx: HttpContext): boolean {
200
216
  if (!("user" in ctx)) return false;
201
217
  const user = ctx.user;
202
218
  if (typeof user !== "object" || user === null || !("role" in user)) return false;
203
- const role = user.role;
204
- return typeof role === "string" && role !== "customer";
219
+
220
+ // Widened to `string` deliberately. Comparing against an app's own role union
221
+ // is a type error when the app has no such member — 1.13.3 shipped
222
+ // `role !== "customer"` and broke `tsc` for every app whose roles are, say,
223
+ // `"user" | "admin"`, on a feature they were not using. The framework cannot
224
+ // know an app's role names, so it must not narrow to them.
225
+ const role: string = typeof user.role === "string" ? user.role : "";
226
+ if (!role) return false;
227
+
228
+ // An **allowlist**, and this is the load-bearing part. 1.13.3 asked
229
+ // `role !== "customer"`, which reads as "everyone except customers is staff" —
230
+ // so in an app whose roles are `user` and `admin`, every signed-in visitor was
231
+ // staff and the gate let the public straight through. A gate that fails *open*
232
+ // is worse than no gate, because it reports success.
233
+ //
234
+ // Defaulting to `admin` alone rather than to something broad, for the same
235
+ // reason: an app that names its staff role something else gets no bypass and
236
+ // notices, which is the safe direction to be wrong in.
237
+ return config.safe<string[]>("gate.staffRoles", ["admin"]).includes(role);
205
238
  }
206
239
 
207
240
  /** HMAC of the token hash under the app key, so the cookie cannot be forged. */