@tatara/cli 0.12.0 → 0.13.0

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/dist/bin.mjs CHANGED
@@ -84,6 +84,18 @@ const authz = {
84
84
  label: "Permission system",
85
85
  dependencies: ["@tatara/authz"],
86
86
  slots: [
87
+ {
88
+ file: "src/core/authz/system-roles.ts",
89
+ slot: "admin",
90
+ unless: ["auth-keycloak"],
91
+ content: "{ key: \"admin\", name: \"Admin\", description: \"Full access, including roles and members.\" },"
92
+ },
93
+ {
94
+ file: "src/core/authz/system-roles.ts",
95
+ slot: "admin",
96
+ when: ["auth-keycloak"],
97
+ content: "{ key: \"admin\", name: \"Admin\", description: \"Full access, including roles and members.\", heldBy: \"identity\" },"
98
+ },
87
99
  {
88
100
  file: "src/core/authz/index.ts",
89
101
  slot: "options",
@@ -136,8 +148,11 @@ const authz = {
136
148
  - Guard routes with \`middleware: [requirePermission("project:read")]\` from \`#core/authz/index\`
137
149
  and declare \`security: [{ bearerAuth: ["project:read"] }]\`. For scoped actions, call
138
150
  \`resolveScope(principal, "doc:read")\` in the service and filter rows by the returned scope.
139
- - Effective permissions = role grants (assigned roles + built-in roles whose key matches an
140
- identity-provider role) + grant overlays − deny overlays. Deny always wins.
151
+ - Effective permissions = grants of the caller's roles + grant overlays − deny overlays. Deny
152
+ always wins. Roles are assigned in authz (the org's creator gets \`admin\`); a built-in role with
153
+ \`heldBy: "members"\` goes to every member, \`heldBy: "identity"\` to holders of that identity role.
154
+ - better-auth's member role is membership only (\`owner\` or \`member\`): \`authzPlugin\` in
155
+ \`src/modules/users/services/auth.ts\` refuses others and gives the creator \`admin\`.
141
156
  - Custom roles and grant overlays can never grant \`role\`, \`member\` or \`audit_log\`.
142
157
  - Built-in roles live in \`src/core/authz/system-roles.ts\`. \`authz:sync-permissions\` brings the database
143
158
  in line with them; \`deploy\` runs it every deploy, \`db:seed\` locally.
@@ -476,7 +491,7 @@ const admin = {
476
491
  {
477
492
  file: "src/core/authz/system-roles.ts",
478
493
  slot: "roles",
479
- content: "{ key: \"staff\", name: \"Staff\", description: \"Internal staff: platform permissions on the admin app.\" },"
494
+ content: "{ key: \"staff\", name: \"Staff\", description: \"Internal staff: platform permissions on the admin app.\", heldBy: \"identity\" },"
480
495
  },
481
496
  {
482
497
  file: "src/modules/staff/services/auth.ts",
@@ -1546,16 +1561,15 @@ const authOrgs = {
1546
1561
  slot: "plugins",
1547
1562
  when: ["authz"],
1548
1563
  content: [
1549
- "organization({",
1550
- " organizationHooks: {",
1551
- " // whoever creates an org administers it (pick another built-in role, or drop this)",
1552
- " afterCreateOrganization: async ({ organization, member }) => {",
1553
- " await authz.grantSystemRole(organization.id, member.userId, \"admin\", member.userId);",
1554
- " },",
1555
- " // authz roles and overlays go with the membership, so re-adding someone starts clean",
1556
- " afterRemoveMember: async ({ member }) => {",
1557
- " await authz.removeMemberAccess(member.organizationId, member.userId);",
1558
- " },",
1564
+ "organization(),",
1565
+ "// better-auth keeps who is in which org (owner or member); authz decides what they may do.",
1566
+ "// The creator gets admin (`creatorRole`). better-auth shows these lists, emails included,",
1567
+ "// to any member: `endpoints` puts them behind an authz permission",
1568
+ "authzPlugin(authz, {",
1569
+ " endpoints: {",
1570
+ " \"/organization/list-members\": \"member:read\",",
1571
+ " \"/organization/get-full-organization\": \"member:read\",",
1572
+ " \"/organization/list-invitations\": \"member:read\",",
1559
1573
  " },",
1560
1574
  "}),",
1561
1575
  "admin(),"
@@ -1566,7 +1580,7 @@ const authOrgs = {
1566
1580
  file: "src/modules/users/services/auth.ts",
1567
1581
  slot: "imports",
1568
1582
  when: ["authz"],
1569
- content: "import { authz } from \"#core/authz/index\";"
1583
+ content: "import { authzPlugin } from \"@tatara/authz/better-auth\";\nimport { authz } from \"#core/authz/index\";"
1570
1584
  },
1571
1585
  {
1572
1586
  file: "tests/helpers/auth.ts",
@@ -3484,6 +3498,41 @@ const authzPackage = {
3484
3498
  }
3485
3499
  };
3486
3500
  //#endregion
3501
+ //#region src/upgrade/codemods/authz-roles.ts
3502
+ const PATH = "src/core/authz/system-roles.ts";
3503
+ /** Adds `heldBy` to the role literal `{ key: "<key>", … }`, unless it has one. */
3504
+ function holdRole(text, key, heldBy) {
3505
+ const start = text.search(new RegExp(`\\{\\s*key:\\s*"${key}"`));
3506
+ if (start === -1) return text;
3507
+ const end = text.indexOf("}", start);
3508
+ if (end === -1) return text;
3509
+ const literal = text.slice(start, end);
3510
+ if (literal.includes("heldBy")) return text;
3511
+ const body = literal.replace(/,?\s*$/, "");
3512
+ return `${text.slice(0, start)}${body}, heldBy: "${heldBy}" ${text.slice(end)}`;
3513
+ }
3514
+ /** Roles only follow better-auth's member or user role where the app says so (`heldBy`). */
3515
+ const authzRoles = {
3516
+ version: "0.13.0",
3517
+ id: "authz-roles",
3518
+ description: "Built-in roles say who holds them (heldBy); better-auth's member role is membership only",
3519
+ async run(ctx) {
3520
+ const { config } = ctx.project;
3521
+ if (!config.addons.includes("authz")) return;
3522
+ const text = await ctx.read(PATH).catch(() => void 0);
3523
+ if (text !== void 0) {
3524
+ let next = holdRole(text, "member", "members");
3525
+ next = holdRole(next, "staff", "identity");
3526
+ if (config.authMethods.includes("keycloak")) {
3527
+ for (const [, key] of text.matchAll(/\{\s*key:\s*"([^"]+)"/g)) if (key && key !== "member") next = holdRole(next, key, "identity");
3528
+ }
3529
+ if (next !== text) await ctx.write(PATH, next);
3530
+ }
3531
+ ctx.report.notes.push("Built-in roles no longer follow better-auth's member role (or the user role, unless the role has heldBy: \"identity\"). Give access with authz roles (grantSystemRole, assignRole); `member` is held by every member (heldBy: \"members\").");
3532
+ if (config.authMethods.includes("orgs")) ctx.report.notes.push("In src/modules/users/services/auth.ts, replace the organizationHooks that grant the creator's role and remove member access with authzPlugin(authz, { endpoints: { \"/organization/list-members\": \"member:read\", \"/organization/get-full-organization\": \"member:read\", \"/organization/list-invitations\": \"member:read\" } }) from @tatara/authz/better-auth, after organization(). It also keeps member roles to owner and member.");
3533
+ }
3534
+ };
3535
+ //#endregion
3487
3536
  //#region src/upgrade/codemods/authz-stores.ts
3488
3537
  const group = {
3489
3538
  name: "authz store",
@@ -3985,7 +4034,8 @@ const CODEMODS = [
3985
4034
  httpLimits,
3986
4035
  jobsDrivers,
3987
4036
  emailV2,
3988
- deployLedger
4037
+ deployLedger,
4038
+ authzRoles
3989
4039
  ];
3990
4040
  //#endregion
3991
4041
  //#region src/upgrade/context.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tatara/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Create and grow opinionated Hono backends",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,10 @@
1
- /** Built-in roles. Add a key here, then grant it permissions in module permission files. */
1
+ /**
2
+ * Built-in roles. Add a key here, then grant it permissions in module permission files. A role is
3
+ * held only by those it's assigned to, unless `heldBy` says every org member or an identity role.
4
+ */
2
5
  export const SYSTEM_ROLES = [
3
- { key: "admin", name: "Admin", description: "Full access, including roles and members." },
4
- { key: "member", name: "Member", description: "Default role for every member." },
6
+ // tatara:slot admin
7
+ { key: "member", name: "Member", description: "Every member of the org.", heldBy: "members" },
5
8
  // tatara:slot roles
6
9
  ] as const;
7
10
 
@@ -35,9 +35,9 @@
35
35
  "@tatara/http": "0.1.2",
36
36
  "@tatara/db": "0.2.2",
37
37
  "@tatara/testing": "0.1.1",
38
- "@tatara/auth": "0.2.0",
38
+ "@tatara/auth": "0.2.1",
39
39
  "@tatara/keycloak": "0.1.2",
40
- "@tatara/authz": "0.4.0",
40
+ "@tatara/authz": "0.5.0",
41
41
  "@tatara/storage": "0.2.0",
42
42
  "@tatara/email": "0.3.0",
43
43
  "@tatara/i18n": "0.1.0",