@tatara/cli 0.5.0 → 0.6.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.
Files changed (30) hide show
  1. package/dist/bin.mjs +200 -24
  2. package/package.json +1 -1
  3. package/templates/features/auth-keycloak/files+authz/src/core/auth/tests/keycloak-authz.test.ts +2 -2
  4. package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +5 -5
  5. package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +2 -2
  6. package/templates/features/authz/files/src/core/authz/db/seed.ts +2 -2
  7. package/templates/features/authz/files/src/core/authz/index.ts +9 -0
  8. package/templates/features/authz/files/src/core/authz/routes/authorization.routes.ts +9 -280
  9. package/templates/features/authz/files/src/core/authz/system-roles.ts +0 -2
  10. package/templates/features/authz/files/src/core/authz/tests/authz.test.ts +1 -2
  11. package/templates/features/authz/files/tests/helpers/authz.ts +6 -8
  12. package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +3 -3
  13. package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +3 -3
  14. package/templates/generators/permission/files/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
  15. package/templates/generators/resource/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
  16. package/templates/generators/route/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
  17. package/templates/versions.json +2 -1
  18. package/templates/features/authz/files/src/core/authz/cache.ts +0 -31
  19. package/templates/features/authz/files/src/core/authz/catalog.ts +0 -85
  20. package/templates/features/authz/files/src/core/authz/constants.ts +0 -32
  21. package/templates/features/authz/files/src/core/authz/middleware.ts +0 -21
  22. package/templates/features/authz/files/src/core/authz/permissions.ts +0 -31
  23. package/templates/features/authz/files/src/core/authz/resolver.ts +0 -98
  24. package/templates/features/authz/files/src/core/authz/services/audit.ts +0 -24
  25. package/templates/features/authz/files/src/core/authz/services/grants.ts +0 -29
  26. package/templates/features/authz/files/src/core/authz/services/members.ts +0 -84
  27. package/templates/features/authz/files/src/core/authz/services/roles.ts +0 -73
  28. package/templates/features/authz/files/src/core/authz/services/sync.ts +0 -34
  29. package/templates/features/authz/files/src/core/authz/types.ts +0 -87
  30. package/templates/features/authz/files/src/core/authz/validators.ts +0 -99
package/dist/bin.mjs CHANGED
@@ -273,13 +273,12 @@ async function sync(project = loadProject()) {
273
273
  const permissions = await find(root, ["src/modules/*/permissions/*.permissions.ts"]);
274
274
  const imports = permissions.map((f) => `import ${identifier(f, "permissions")} from "${specifier(f)}";`);
275
275
  const names = permissions.map((f) => identifier(f, "permissions"));
276
- await writeIfChanged(join(out, "permissions.ts"), `${HEADER}import { mergeCatalogs, type PermissionKeysOf } from "#core/authz/catalog";
277
- import corePermissions from "#core/authz/permissions";
276
+ await writeIfChanged(join(out, "permissions.ts"), `${HEADER}import { authzPermissions, mergeCatalogs, type PermissionKeysOf } from "@tatara/authz";
278
277
  ${imports.join("\n")}${imports.length ? "\n" : ""}
279
- export const catalog = mergeCatalogs(corePermissions${names.map((n) => `, ${n}`).join("")});
278
+ export const catalog = mergeCatalogs(authzPermissions${names.map((n) => `, ${n}`).join("")});
280
279
 
281
280
  export type PermissionKey =
282
- | PermissionKeysOf<typeof corePermissions>${names.map((n) => `\n\t| PermissionKeysOf<typeof ${n}>`).join("")};
281
+ | PermissionKeysOf<typeof authzPermissions>${names.map((n) => `\n\t| PermissionKeysOf<typeof ${n}>`).join("")};
283
282
  `, written);
284
283
  }
285
284
  if (project.config.database !== "none") {
@@ -682,6 +681,7 @@ const authz = {
682
681
  id: "authz",
683
682
  kind: "addon",
684
683
  label: "Permission system",
684
+ dependencies: ["@tatara/authz"],
685
685
  slots: [
686
686
  {
687
687
  file: "tests/helpers/auth.ts",
@@ -711,9 +711,9 @@ const authz = {
711
711
 
712
712
  - Permissions are \`resource:action\` (optionally scoped \`own | team | org\`, where a wider scope
713
713
  satisfies a narrower one). Each module declares its resources and the grants built-in roles get
714
- in \`permissions/<name>.permissions.ts\` (\`definePermissions\`); \`tatara sync\` merges them into a
714
+ in \`permissions/<name>.permissions.ts\` (\`definePermissions\` from \`@tatara/authz\`); \`tatara sync\` merges them into a
715
715
  typed catalog.
716
- - Guard routes with \`middleware: [requirePermission("project:read")]\` from \`#core/authz/middleware\`
716
+ - Guard routes with \`middleware: [requirePermission("project:read")]\` from \`#core/authz/index\`
717
717
  and declare \`security: [{ bearerAuth: ["project:read"] }]\`. For scoped actions, call
718
718
  \`resolveScope(principal, "doc:read")\` in the service and filter rows by the returned scope.
719
719
  - Effective permissions = role grants (assigned roles + built-in roles whose key matches an
@@ -1454,7 +1454,7 @@ const authOrgs = {
1454
1454
  file: "src/core/auth/auth.ts",
1455
1455
  slot: "imports",
1456
1456
  when: ["authz"],
1457
- content: "import { removeMemberAccess } from \"#core/authz/services/members\";"
1457
+ content: "import { removeMemberAccess } from \"#core/authz/index\";"
1458
1458
  },
1459
1459
  {
1460
1460
  file: "tests/helpers/auth.ts",
@@ -2452,7 +2452,7 @@ function guardSlots(project, n) {
2452
2452
  order: fills.length
2453
2453
  });
2454
2454
  if (project.has("authz")) {
2455
- add("imports", "import { requirePermission } from \"#core/authz/middleware\";");
2455
+ add("imports", "import { requirePermission } from \"#core/authz/index\";");
2456
2456
  for (const [route, action] of Object.entries(ACTIONS)) add(`guard-${route}`, `middleware: [requirePermission("${n.kebab}:${action}")] as const,\nsecurity: [{ bearerAuth: ["${n.kebab}:${action}"] }],`);
2457
2457
  add("auth-errors", "...errorResponses(401, 403),");
2458
2458
  } else if (project.has("auth")) {
@@ -3186,6 +3186,36 @@ function createBaseline(project, deps) {
3186
3186
  } };
3187
3187
  }
3188
3188
  //#endregion
3189
+ //#region src/upgrade/codemods/imports.ts
3190
+ const escapeRegExp$1 = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3191
+ /**
3192
+ * The names `text` imports (or re-exports) from `specifier`, or null when it doesn't use it.
3193
+ * A namespace, default, side-effect or dynamic import gives ["*"]: it could reach any name.
3194
+ */
3195
+ function importsOf(text, specifier) {
3196
+ const spec = `["']${escapeRegExp$1(specifier)}["']`;
3197
+ const names = [];
3198
+ let found = false;
3199
+ const fromClause = new RegExp(`(?:import|export)\\s+(?:type\\s+)?([^;]*?)\\s+from\\s+${spec}`, "g");
3200
+ for (const match of text.matchAll(fromClause)) {
3201
+ found = true;
3202
+ const braces = (match[1] ?? "").match(/^\{([^}]*)\}$/);
3203
+ if (!braces) {
3204
+ names.push("*");
3205
+ continue;
3206
+ }
3207
+ for (const part of (braces[1] ?? "").split(",")) {
3208
+ const name = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
3209
+ if (name) names.push(name);
3210
+ }
3211
+ }
3212
+ if (new RegExp(`import\\s*\\(\\s*${spec}\\s*\\)|import\\s+${spec}`).test(text)) {
3213
+ found = true;
3214
+ names.push("*");
3215
+ }
3216
+ return found ? names : null;
3217
+ }
3218
+ //#endregion
3189
3219
  //#region src/upgrade/codemods/auth-package.ts
3190
3220
  const AUTH = "@tatara/auth";
3191
3221
  const KEYCLOAK = "@tatara/keycloak";
@@ -3262,27 +3292,21 @@ function groups(keycloak) {
3262
3292
  });
3263
3293
  return all;
3264
3294
  }
3265
- async function read$2(ctx, path) {
3295
+ async function read$3(ctx, path) {
3266
3296
  return ctx.read(path).catch(() => void 0);
3267
3297
  }
3268
- const escapeRegExp$1 = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3269
3298
  /** True when `text` imports `entry` (or one of its gone names). */
3270
3299
  function usesExport(text, entry) {
3271
- const from = new RegExp(`import\\s+(?:type\\s+)?([^;]*?)\\s+from\\s+["']${escapeRegExp$1(entry.specifier)}["']`, "g");
3300
+ const names = importsOf(text, entry.specifier);
3301
+ if (!names) return false;
3272
3302
  const gone = entry.names;
3273
- for (const match of text.matchAll(from)) {
3274
- if (!gone) return true;
3275
- const braces = (match[1] ?? "").match(/\{([^}]*)\}/);
3276
- if (!braces) return true;
3277
- if ((braces[1] ?? "").split(",").map((part) => part.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim()).some((name) => name && gone.includes(name))) return true;
3278
- }
3279
- return false;
3303
+ return !gone || names.some((name) => name === "*" || gone.includes(name));
3280
3304
  }
3281
3305
  /** Why the group can't move, or [] when it can. */
3282
3306
  async function blockersOf(ctx, group, sources) {
3283
3307
  const blockers = [];
3284
3308
  for (const path of [...group.regenerate, ...group.remove]) {
3285
- const text = await read$2(ctx, path);
3309
+ const text = await read$3(ctx, path);
3286
3310
  if (text === void 0) continue;
3287
3311
  if (!await ctx.baseline.isUnchanged(path, text)) blockers.push(`${path} is customised`);
3288
3312
  }
@@ -3316,7 +3340,7 @@ const authPackage = {
3316
3340
  }
3317
3341
  for (const group of ready) {
3318
3342
  for (const path of group.regenerate) {
3319
- if (await read$2(ctx, path) === void 0) continue;
3343
+ if (await read$3(ctx, path) === void 0) continue;
3320
3344
  const next = await ctx.renderCurrent(path);
3321
3345
  if (next !== void 0) await ctx.write(path, next);
3322
3346
  }
@@ -3327,6 +3351,151 @@ const authPackage = {
3327
3351
  }
3328
3352
  };
3329
3353
  //#endregion
3354
+ //#region src/upgrade/codemods/authz-package.ts
3355
+ const AUTHZ = "@tatara/authz";
3356
+ const INDEX = "#core/authz/index";
3357
+ const DIR = "src/core/authz";
3358
+ /** Copies the package replaces. */
3359
+ const REMOVED = [
3360
+ "cache.ts",
3361
+ "catalog.ts",
3362
+ "constants.ts",
3363
+ "middleware.ts",
3364
+ "permissions.ts",
3365
+ "resolver.ts",
3366
+ "types.ts",
3367
+ "validators.ts",
3368
+ "services/audit.ts",
3369
+ "services/grants.ts",
3370
+ "services/members.ts",
3371
+ "services/roles.ts",
3372
+ "services/sync.ts"
3373
+ ].map((f) => `${DIR}/${f}`);
3374
+ /** App files that stay (or are new), and become what this CLI generates. system-roles.ts is the app's config and stays as it is. */
3375
+ const REGENERATED$1 = [
3376
+ `${DIR}/index.ts`,
3377
+ `${DIR}/db/seed.ts`,
3378
+ `${DIR}/commands/grant-role.ts`,
3379
+ `${DIR}/commands/sync-permissions.ts`,
3380
+ `${DIR}/routes/authorization.routes.ts`,
3381
+ `${DIR}/tests/authz.test.ts`,
3382
+ "src/core/auth/tests/keycloak-authz.test.ts",
3383
+ "tests/helpers/authz.ts"
3384
+ ];
3385
+ /** Where other files' imports go, and the names that exist there. undefined: every name. */
3386
+ const MOVES = [
3387
+ {
3388
+ from: "#core/authz/constants",
3389
+ to: AUTHZ
3390
+ },
3391
+ {
3392
+ from: "#core/authz/types",
3393
+ to: AUTHZ
3394
+ },
3395
+ {
3396
+ from: "#core/authz/catalog",
3397
+ to: AUTHZ
3398
+ },
3399
+ {
3400
+ from: "#core/authz/validators",
3401
+ to: AUTHZ
3402
+ },
3403
+ {
3404
+ from: "#core/authz/middleware",
3405
+ to: INDEX,
3406
+ names: ["requirePermission"]
3407
+ },
3408
+ {
3409
+ from: "#core/authz/resolver",
3410
+ to: INDEX,
3411
+ names: ["checkPermission", "resolveScope"]
3412
+ },
3413
+ {
3414
+ from: "#core/authz/services/members",
3415
+ to: INDEX,
3416
+ names: ["removeMemberAccess"]
3417
+ },
3418
+ {
3419
+ from: "#core/authz/cache",
3420
+ to: INDEX,
3421
+ names: []
3422
+ },
3423
+ {
3424
+ from: "#core/authz/permissions",
3425
+ to: INDEX,
3426
+ names: []
3427
+ },
3428
+ ...[
3429
+ "audit",
3430
+ "grants",
3431
+ "roles",
3432
+ "sync"
3433
+ ].map((s) => ({
3434
+ from: `#core/authz/services/${s}`,
3435
+ to: INDEX,
3436
+ names: []
3437
+ }))
3438
+ ];
3439
+ /** Names the 0.5 constants file had that the package dropped. */
3440
+ const DROPPED_CONSTANTS = ["CACHE_TTL_MS"];
3441
+ async function read$2(ctx, path) {
3442
+ return ctx.read(path).catch(() => void 0);
3443
+ }
3444
+ const authzPackage = {
3445
+ version: "0.6.0",
3446
+ id: "authz-package",
3447
+ description: "Use @tatara/authz (catalog, resolver, guards, services, routes)",
3448
+ async run(ctx) {
3449
+ if (!ctx.project.config.addons.includes("authz")) return;
3450
+ await ctx.addDependency(AUTHZ, versionOf(AUTHZ));
3451
+ const blockers = [];
3452
+ const regenerate = [];
3453
+ for (const path of REGENERATED$1) {
3454
+ const text = await read$2(ctx, path);
3455
+ if (text === void 0) {
3456
+ if (path === `${DIR}/index.ts`) regenerate.push(path);
3457
+ continue;
3458
+ }
3459
+ if (await ctx.baseline.isUnchanged(path, text)) regenerate.push(path);
3460
+ else blockers.push(`${path} is customised`);
3461
+ }
3462
+ const removed = [];
3463
+ for (const path of REMOVED) {
3464
+ const text = await read$2(ctx, path);
3465
+ if (text === void 0) continue;
3466
+ removed.push(path);
3467
+ if (!await ctx.baseline.isUnchanged(path, text)) blockers.push(`${path} is customised`);
3468
+ }
3469
+ const own = /* @__PURE__ */ new Set([...REGENERATED$1, ...REMOVED]);
3470
+ const sources = await glob(["src/**/*.ts", "tests/**/*.ts"], {
3471
+ cwd: ctx.project.root,
3472
+ ignore: ["src/generated/**"]
3473
+ });
3474
+ for (const path of sources) {
3475
+ if (own.has(path)) continue;
3476
+ const text = await ctx.read(path);
3477
+ for (const move of MOVES) {
3478
+ const names = importsOf(text, move.from);
3479
+ if (!names) continue;
3480
+ const allowed = move.names;
3481
+ const missing = names.filter((n) => n === "*" || (allowed ? !allowed.includes(n) : move.from.endsWith("/constants") && DROPPED_CONSTANTS.includes(n)));
3482
+ if (missing.length > 0) blockers.push(`${path} imports ${missing.join(", ")} from ${move.from}`);
3483
+ }
3484
+ }
3485
+ if (blockers.length > 0) {
3486
+ ctx.report.notes.push(`Nothing in ${DIR} was changed: ${blockers.join("; ")}. Compare with a newly generated app and move onto @tatara/authz (createAuthz in ${DIR}/index.ts) by hand.`);
3487
+ ctx.report.blocked.push("authz-package");
3488
+ return;
3489
+ }
3490
+ for (const path of regenerate) {
3491
+ const next = await ctx.renderCurrent(path);
3492
+ if (next !== void 0) await ctx.write(path, next);
3493
+ }
3494
+ for (const path of removed) await ctx.removeIfUnchanged(path);
3495
+ for (const move of MOVES) await ctx.rewriteImports(move.from, move.to);
3496
+ }
3497
+ };
3498
+ //#endregion
3330
3499
  //#region src/upgrade/codemods/core-package.ts
3331
3500
  const CORE = "@tatara/core";
3332
3501
  /** Files @tatara/core replaces, with the import specifier apps used for each. */
@@ -3586,6 +3755,7 @@ const CODEMODS = [
3586
3755
  for (const path of await outsideImporters(ctx)) blockers.push(`${path} imports one of the moved files`);
3587
3756
  if (blockers.length > 0) {
3588
3757
  ctx.report.notes.push(`Nothing in src/core/http was changed: ${blockers.join("; ")}. Move these onto @tatara/http (createApp, mountRoutes, serveOpenApi, handleErrors, startServer and its error classes) by hand, then delete the old files.`);
3758
+ ctx.report.blocked.push("http-package");
3589
3759
  return;
3590
3760
  }
3591
3761
  for (const path of replace) {
@@ -3601,7 +3771,8 @@ const CODEMODS = [
3601
3771
  }
3602
3772
  },
3603
3773
  dbTesting,
3604
- authPackage
3774
+ authPackage,
3775
+ authzPackage
3605
3776
  ];
3606
3777
  //#endregion
3607
3778
  //#region src/upgrade/context.ts
@@ -3613,7 +3784,8 @@ function createContext(project, baseline, opts = {}) {
3613
3784
  regenerated: [],
3614
3785
  removed: [],
3615
3786
  kept: [],
3616
- notes: []
3787
+ notes: [],
3788
+ blocked: []
3617
3789
  };
3618
3790
  const abs = (path) => join(project.root, path);
3619
3791
  let rendered;
@@ -3725,13 +3897,16 @@ async function upgrade(project, opts = {}) {
3725
3897
  const from = project.config.version;
3726
3898
  const ctx = createContext(project, opts.baseline ?? createBaseline(project), { dryRun: opts.dryRun });
3727
3899
  const due = [...opts.codemods ?? CODEMODS].filter((m) => compareVersions(m.version, from) > 0 && compareVersions(m.version, target) <= 0).sort((a, b) => compareVersions(a.version, b.version));
3900
+ let reached = target;
3728
3901
  for (const mod of due) {
3902
+ const blockedBefore = ctx.report.blocked.length;
3729
3903
  try {
3730
3904
  await mod.run(ctx);
3731
3905
  } catch (error) {
3732
3906
  throw new Error(`codemod ${mod.id} (${mod.version}) failed: ${error.message}`, { cause: error });
3733
3907
  }
3734
3908
  ctx.report.ran.push(mod.id);
3909
+ if (ctx.report.blocked.length > blockedBefore && reached === target) reached = due.slice(0, due.indexOf(mod)).at(-1)?.version ?? from;
3735
3910
  }
3736
3911
  if (opts.dryRun) return ctx.report;
3737
3912
  const format = opts.formatFile ?? biomeFormat;
@@ -3741,9 +3916,9 @@ async function upgrade(project, opts = {}) {
3741
3916
  const config = JSON.parse(await readFile(path, "utf8"));
3742
3917
  const next = {
3743
3918
  ...config,
3744
- version: target,
3919
+ version: reached,
3745
3920
  generated: {
3746
- cli: target,
3921
+ cli: reached,
3747
3922
  files: await nextRecord(project, ctx.report)
3748
3923
  }
3749
3924
  };
@@ -3763,6 +3938,7 @@ function printUpgrade(report, from, to) {
3763
3938
  for (const path of report.removed) console.log(pc.dim(` removed ${path} (now provided by a package)`));
3764
3939
  for (const { path } of report.kept) console.log(pc.yellow(` kept ${path}: changed in this app; move the change into options or eject`));
3765
3940
  for (const note of report.notes) console.log(` ${note}`);
3941
+ if (report.blocked.length > 0) console.log(pc.yellow(` ${report.blocked.join(", ")} did not run: make the changes above, then run tatara upgrade again`));
3766
3942
  }
3767
3943
  //#endregion
3768
3944
  //#region src/bin.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tatara/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Create and grow opinionated Hono backends",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  import { beforeEach, describe, expect, it } from "vitest";
2
- import { syncSystemRoles } from "#core/authz/services/sync";
2
+ import { authz } from "#core/authz/index";
3
3
  import { api } from "#tests/helpers/api";
4
4
  import { keycloakToken } from "#tests/helpers/auth";
5
5
  import { resetDatabase } from "#tests/helpers/db";
@@ -12,7 +12,7 @@ beforeEach(async () => {
12
12
 
13
13
  describe("Keycloak access tokens with authz", () => {
14
14
  it("lets token roles match built-in authz roles", async () => {
15
- await syncSystemRoles();
15
+ await authz.syncSystemRoles();
16
16
  const admin = await keycloakToken({ roles: ["admin"] });
17
17
  const user = await keycloakToken({ roles: ["user"] });
18
18
  expect((await api.get("/api/authorization/roles", { headers: bearer(admin) })).status).toBe(
@@ -1,7 +1,6 @@
1
1
  import { z } from "zod";
2
- import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "#core/authz/constants";
3
- import { store } from "#core/authz/db/store";
4
- import { assignRole } from "#core/authz/services/members";
2
+ import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "@tatara/authz";
3
+ import { authz } from "#core/authz/index";
5
4
  import { defineCommand } from "@tatara/core";
6
5
 
7
6
  export default defineCommand({
@@ -13,9 +12,10 @@ export default defineCommand({
13
12
  org: z.string().default(DEFAULT_ORG_ID),
14
13
  }),
15
14
  async run({ user, role, org }, { logger }) {
16
- const found = (await store.findRoleByKey(org, role)) ?? (await store.findRoleByKey(SYSTEM_ORG_ID, role));
15
+ const found = (await authz.store.findRoleByKey(org, role)) ??
16
+ (await authz.store.findRoleByKey(SYSTEM_ORG_ID, role));
17
17
  if (!found) throw new Error(`No role "${role}" (run authz:sync-permissions first for built-in roles)`);
18
- const added = await assignRole(null, org, user, found.id);
18
+ const added = await authz.assignRole(null, org, user, found.id);
19
19
  logger.info({ user, role, org, added }, added ? "role assigned" : "role already assigned");
20
20
  },
21
21
  });
@@ -1,11 +1,11 @@
1
1
  import { defineCommand } from "@tatara/core";
2
- import { syncSystemRoles } from "#core/authz/services/sync";
2
+ import { authz } from "#core/authz/index";
3
3
 
4
4
  export default defineCommand({
5
5
  name: "authz:sync-permissions",
6
6
  description: "Create/update built-in roles so their grants match the code (run on every deploy)",
7
7
  async run(_args, { logger }) {
8
- const result = await syncSystemRoles();
8
+ const result = await authz.syncSystemRoles();
9
9
  logger.info(result, "built-in roles in sync");
10
10
  },
11
11
  });
@@ -1,10 +1,10 @@
1
- import { syncSystemRoles } from "#core/authz/services/sync";
1
+ import { authz } from "#core/authz/index";
2
2
  import { defineSeed } from "@tatara/db";
3
3
 
4
4
  export default defineSeed({
5
5
  name: "authz:system-roles",
6
6
  order: -10,
7
7
  async run({ logger }) {
8
- logger.info(await syncSystemRoles(), "built-in roles in sync");
8
+ logger.info(await authz.syncSystemRoles(), "built-in roles in sync");
9
9
  },
10
10
  });
@@ -0,0 +1,9 @@
1
+ import { createAuthz } from "@tatara/authz";
2
+ import { store } from "#core/authz/db/store";
3
+ import { SYSTEM_ROLES } from "#core/authz/system-roles";
4
+ import { catalog, type PermissionKey } from "#generated/permissions";
5
+
6
+ /** The app's authorization: its store, the permission catalog from `tatara sync`, built-in roles. */
7
+ export const authz = createAuthz<PermissionKey>({ store, catalog, systemRoles: SYSTEM_ROLES });
8
+
9
+ export const { checkPermission, removeMemberAccess, requirePermission, resolveScope } = authz;