@aglyn/aglyn 1.0.0-beta.150 → 1.0.0-beta.151

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.
@@ -16,3 +16,4 @@
16
16
  */
17
17
  export * from './server';
18
18
  export * from './contexts';
19
+ export * from './staff-role-gate';
@@ -19,5 +19,6 @@
19
19
  // reached via `@aglyn/aglyn`, keeps both for client consumers unchanged.
20
20
  export * from "./server.js";
21
21
  export * from "./contexts.js";
22
+ export * from "./staff-role-gate.js";
22
23
 
23
24
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Full app-utils surface = server-safe utils + the client-only React\n// contexts. Split so `@aglyn/aglyn/server` can re-export just `./server`\n// (no `createContext`) for tenant Server Components (AGL-405); this barrel,\n// reached via `@aglyn/aglyn`, keeps both for client consumers unchanged.\nexport * from './server'\nexport * from './contexts'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,qEAAqE;AACrE,yEAAyE;AACzE,4EAA4E;AAC5E,yEAAyE;AACzE,cAAc,cAAU;AACxB,cAAc,gBAAY"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Full app-utils surface = server-safe utils + the client-only React\n// contexts. Split so `@aglyn/aglyn/server` can re-export just `./server`\n// (no `createContext`) for tenant Server Components (AGL-405); this barrel,\n// reached via `@aglyn/aglyn`, keeps both for client consumers unchanged.\nexport * from './server'\nexport * from './contexts'\nexport * from './staff-role-gate'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,qEAAqE;AACrE,yEAAyE;AACzE,4EAA4E;AAC5E,yEAAyE;AACzE,cAAc,cAAU;AACxB,cAAc,gBAAY;AAC1B,cAAc,uBAAmB"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ export interface StaffRoleGate {
18
+ /**
19
+ * `false` while the claim is still resolving. Nothing may be BLOCKED in
20
+ * that window: rendering a refusal there would flash a disabled button at
21
+ * every super-staff member on every admin page load.
22
+ */
23
+ ready: boolean;
24
+ /** Whether the viewer's role is one the route admits. `false` until `ready`. */
25
+ admitted: boolean;
26
+ /**
27
+ * The one thing call sites branch on: a RESOLVED claim the route would
28
+ * refuse. Deliberately not `!admitted` — that is true during the
29
+ * unresolved window too, and would disable the control for everyone for a
30
+ * beat.
31
+ */
32
+ blocked: boolean;
33
+ /** The reason to show when `blocked`, else `undefined`. */
34
+ reason?: string;
35
+ }
36
+ /**
37
+ * The viewer's standing against the set of roles a route admits (AGL-2131,
38
+ * moved out of the console app by AGL-3080 so a plugin's staff page can ask
39
+ * the same question).
40
+ *
41
+ * Pure, and takes the role rather than reading it: the claim lives in the
42
+ * app's session, and a staff page the shell mounts is handed the resolved
43
+ * value. `null` means "still reading the token", and it is also what a
44
+ * non-staff viewer resolves to — who never reaches an `/admin` page at all,
45
+ * because the staff gate 404s them first. Treating both as unresolved is
46
+ * therefore right for the only population that gets here.
47
+ *
48
+ * Not every gated act is super-only: `/api/admin/org-override` admits
49
+ * `billing` for plan and quota writes and reserves only `releaseFlags` for
50
+ * `super`. A gate that could only say "super" would disable that dialog for
51
+ * the role whose entire purpose it is — trading one dishonest control for
52
+ * another.
53
+ */
54
+ export declare function resolveStaffRoleGate(role: string | null | undefined, allowed: readonly string[]): StaffRoleGate;
55
+ /** The single sentence every blocked super-only control says. */
56
+ export declare const SUPER_STAFF_ONLY_REASON = "This action requires the super staff role. Ask someone who holds it.";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The viewer's standing against the set of roles a route admits (AGL-2131,
18
+ * moved out of the console app by AGL-3080 so a plugin's staff page can ask
19
+ * the same question).
20
+ *
21
+ * Pure, and takes the role rather than reading it: the claim lives in the
22
+ * app's session, and a staff page the shell mounts is handed the resolved
23
+ * value. `null` means "still reading the token", and it is also what a
24
+ * non-staff viewer resolves to — who never reaches an `/admin` page at all,
25
+ * because the staff gate 404s them first. Treating both as unresolved is
26
+ * therefore right for the only population that gets here.
27
+ *
28
+ * Not every gated act is super-only: `/api/admin/org-override` admits
29
+ * `billing` for plan and quota writes and reserves only `releaseFlags` for
30
+ * `super`. A gate that could only say "super" would disable that dialog for
31
+ * the role whose entire purpose it is — trading one dishonest control for
32
+ * another.
33
+ */ export function resolveStaffRoleGate(role, allowed) {
34
+ const ready = role !== null && role !== undefined;
35
+ const admitted = ready && allowed.includes(String(role));
36
+ const blocked = ready && !admitted;
37
+ return {
38
+ ready,
39
+ admitted,
40
+ blocked,
41
+ reason: blocked ? `This action requires the ${allowed.join(' or ')} staff role. ` + 'Ask someone who holds it.' : undefined
42
+ };
43
+ }
44
+ /** The single sentence every blocked super-only control says. */ export const SUPER_STAFF_ONLY_REASON = 'This action requires the super staff role. Ask someone who holds it.';
45
+
46
+ //# sourceMappingURL=staff-role-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/staff-role-gate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport interface StaffRoleGate {\n /**\n * `false` while the claim is still resolving. Nothing may be BLOCKED in\n * that window: rendering a refusal there would flash a disabled button at\n * every super-staff member on every admin page load.\n */\n ready: boolean\n /** Whether the viewer's role is one the route admits. `false` until `ready`. */\n admitted: boolean\n /**\n * The one thing call sites branch on: a RESOLVED claim the route would\n * refuse. Deliberately not `!admitted` — that is true during the\n * unresolved window too, and would disable the control for everyone for a\n * beat.\n */\n blocked: boolean\n /** The reason to show when `blocked`, else `undefined`. */\n reason?: string\n}\n\n/**\n * The viewer's standing against the set of roles a route admits (AGL-2131,\n * moved out of the console app by AGL-3080 so a plugin's staff page can ask\n * the same question).\n *\n * Pure, and takes the role rather than reading it: the claim lives in the\n * app's session, and a staff page the shell mounts is handed the resolved\n * value. `null` means \"still reading the token\", and it is also what a\n * non-staff viewer resolves to — who never reaches an `/admin` page at all,\n * because the staff gate 404s them first. Treating both as unresolved is\n * therefore right for the only population that gets here.\n *\n * Not every gated act is super-only: `/api/admin/org-override` admits\n * `billing` for plan and quota writes and reserves only `releaseFlags` for\n * `super`. A gate that could only say \"super\" would disable that dialog for\n * the role whose entire purpose it is — trading one dishonest control for\n * another.\n */\nexport function resolveStaffRoleGate(\n role: string | null | undefined,\n allowed: readonly string[],\n): StaffRoleGate {\n const ready = role !== null && role !== undefined\n const admitted = ready && allowed.includes(String(role))\n const blocked = ready && !admitted\n return {\n ready,\n admitted,\n blocked,\n reason: blocked\n ? `This action requires the ${allowed.join(' or ')} staff role. ` +\n 'Ask someone who holds it.'\n : undefined,\n }\n}\n\n/** The single sentence every blocked super-only control says. */\nexport const SUPER_STAFF_ONLY_REASON =\n 'This action requires the super staff role. Ask someone who holds it.'\n"],"names":["resolveStaffRoleGate","role","allowed","ready","undefined","admitted","includes","String","blocked","reason","join","SUPER_STAFF_ONLY_REASON"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAsBD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASA,qBACdC,IAA+B,EAC/BC,OAA0B;IAE1B,MAAMC,QAAQF,SAAS,QAAQA,SAASG;IACxC,MAAMC,WAAWF,SAASD,QAAQI,QAAQ,CAACC,OAAON;IAClD,MAAMO,UAAUL,SAAS,CAACE;IAC1B,OAAO;QACLF;QACAE;QACAG;QACAC,QAAQD,UACJ,CAAC,yBAAyB,EAAEN,QAAQQ,IAAI,CAAC,QAAQ,aAAa,CAAC,GAC/D,8BACAN;IACN;AACF;AAEA,+DAA+D,GAC/D,OAAO,MAAMO,0BACX,uEAAsE"}
@@ -1242,6 +1242,48 @@ export interface ConsoleUpgradeNotice {
1242
1242
  export interface ConsoleStaffPageProps {
1243
1243
  /** The page's own console path, `/admin/{id}`. */
1244
1244
  basePath: string;
1245
+ /**
1246
+ * Path segments beneath {@link basePath}, `[]` on the page's own URL
1247
+ * (AGL-3080). Only ever non-empty for a page that declared
1248
+ * {@link ConsoleStaffPage.ownsSubtree}.
1249
+ *
1250
+ * The staff twin of {@link ConsolePluginPageProps.segments}, and for the
1251
+ * same case: a queue is a list, and a row of it is a page. A staff page
1252
+ * without this could only ever BE the list.
1253
+ */
1254
+ segments?: readonly string[];
1255
+ /**
1256
+ * The viewer's staff ROLE, or `null` while the claim is still resolving
1257
+ * (AGL-3080).
1258
+ *
1259
+ * Not every staff act is open to every staff role — six are `super`-only
1260
+ * on the server — and a page that cannot tell renders live controls to a
1261
+ * `support` engineer who clicks them and gets a raw 403. Pass it to
1262
+ * `resolveStaffRoleGate` and render the verdict with
1263
+ * `BlockedControl`; `null` must never be treated as a refusal, or every
1264
+ * page flashes a disabled button at the people who may use it.
1265
+ *
1266
+ * ⚠️ NOT the boundary. The routes verify the decoded token per request and
1267
+ * refuse regardless of what rendered. This exists so the console stops
1268
+ * promising what the server will refuse.
1269
+ */
1270
+ staffRole?: string | null;
1271
+ /**
1272
+ * Console destinations a staff page may link ACROSS to, built by the shell
1273
+ * (AGL-3080) — the staff twin of {@link ConsolePluginOrgMount}'s paths,
1274
+ * and for the same reason: a plugin cannot import the console's route
1275
+ * table, and a plugin that rebuilt one of these from a string would break
1276
+ * silently the day the console moved it.
1277
+ */
1278
+ staffPaths?: ConsoleStaffPagePaths;
1279
+ }
1280
+ export interface ConsoleStaffPagePaths {
1281
+ /**
1282
+ * The staff console's page for one workspace. `undefined` on a deployment
1283
+ * that has no such page, which a caller renders as no link rather than a
1284
+ * dead one.
1285
+ */
1286
+ orgDetail(orgId: string): string | undefined;
1245
1287
  }
1246
1288
  /**
1247
1289
  * A page a plugin adds to the STAFF area (AGL-2939): a tab in the staff
@@ -1274,6 +1316,25 @@ export interface ConsoleStaffPage {
1274
1316
  icon?: MdiIconProps;
1275
1317
  docsTopic?: string;
1276
1318
  };
1319
+ /**
1320
+ * Whether this page claims every path beneath `/admin/{id}` too
1321
+ * (AGL-3080) — the staff twin of {@link ConsoleNavItem.ownsSubtree}, with
1322
+ * the same trade and the same duty.
1323
+ *
1324
+ * The case is a staff QUEUE: the list is the page, and each row opens one
1325
+ * submission. The set of ids is a property of the data, so no static list
1326
+ * could enumerate them, and without this every row's URL is a 404.
1327
+ *
1328
+ * A page that claims its subtree can no longer tell a typo from an id, so
1329
+ * it takes on saying "no such thing" itself — which a queue has to be able
1330
+ * to do anyway for a submission withdrawn while a link to it was still in
1331
+ * a reviewer's inbox.
1332
+ *
1333
+ * The console's own staff routes keep winning their segments either way:
1334
+ * a static path beats a dynamic one segment by segment, so `/admin/orgs/1`
1335
+ * is still the orgs route and never a staff page's subtree.
1336
+ */
1337
+ ownsSubtree?: boolean;
1277
1338
  Component: ComponentType<ConsoleStaffPageProps>;
1278
1339
  }
1279
1340
  export interface ConsoleExtension {