@percepta/create 3.1.5 → 3.3.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 (77) hide show
  1. package/dist/index.js +53 -46
  2. package/dist/index.js.map +1 -1
  3. package/dist/{init-OeK4Yk6_.js → init-CtCp7Tv2.js} +3 -3
  4. package/dist/init-CtCp7Tv2.js.map +1 -0
  5. package/dist/{status-DC8mvHZj.js → status-CKe4aKso.js} +2 -2
  6. package/dist/{status-DC8mvHZj.js.map → status-CKe4aKso.js.map} +1 -1
  7. package/dist/{sync-C5Pd32VM.js → sync-D1vkoofl.js} +2 -2
  8. package/dist/{sync-C5Pd32VM.js.map → sync-D1vkoofl.js.map} +1 -1
  9. package/dist/{upstream-F6m8zRBQ.js → upstream-D-LH_1z4.js} +2 -2
  10. package/dist/{upstream-F6m8zRBQ.js.map → upstream-D-LH_1z4.js.map} +1 -1
  11. package/package.json +2 -2
  12. package/template-versions.json +1 -1
  13. package/templates/monorepo/.github/workflows/access-control.yml +38 -0
  14. package/templates/monorepo/README.md +42 -2
  15. package/templates/monorepo/access/README.md +39 -0
  16. package/templates/monorepo/access/bootstrap-grants.yaml.example +9 -0
  17. package/templates/monorepo/access/dev-grants.yaml.example +19 -0
  18. package/templates/monorepo/access/dev-groups.yaml.example +8 -0
  19. package/templates/monorepo/access/reconcile.yaml.example +11 -0
  20. package/templates/monorepo/auth/README.md +27 -0
  21. package/templates/monorepo/auth/drizzle.config.ts +13 -0
  22. package/templates/monorepo/auth/package.json +29 -0
  23. package/templates/monorepo/auth/scripts/setup-database.ts +11 -0
  24. package/templates/monorepo/auth/src/auth.ts +47 -0
  25. package/templates/monorepo/auth/src/config/database.ts +15 -0
  26. package/templates/monorepo/auth/src/drizzle/db.ts +8 -0
  27. package/templates/monorepo/auth/src/drizzle/migrations/0000_shared_auth.sql +89 -0
  28. package/templates/monorepo/auth/src/drizzle/migrations/meta/_journal.json +13 -0
  29. package/templates/monorepo/auth/src/drizzle/schema/auth/accounts.ts +7 -0
  30. package/templates/monorepo/auth/src/drizzle/schema/auth/sessions.ts +7 -0
  31. package/templates/monorepo/auth/src/drizzle/schema/auth/verifications.ts +6 -0
  32. package/templates/monorepo/auth/src/drizzle/schema/groups.ts +16 -0
  33. package/templates/monorepo/auth/src/drizzle/schema/index.ts +5 -0
  34. package/templates/monorepo/auth/src/drizzle/schema/users.ts +6 -0
  35. package/templates/monorepo/auth/src/index.ts +1 -0
  36. package/templates/monorepo/auth/src/scim/README.md +6 -0
  37. package/templates/monorepo/auth/tsconfig.json +12 -0
  38. package/templates/monorepo/package.json.template +18 -6
  39. package/templates/monorepo/pnpm-workspace.yaml +1 -0
  40. package/templates/webapp/AGENTS.md +13 -6
  41. package/templates/webapp/README.md +34 -18
  42. package/templates/webapp/agent-skills/access-control.md +301 -0
  43. package/templates/webapp/agent-skills/database.md +1 -1
  44. package/templates/webapp/docker-compose.yml +16 -0
  45. package/templates/webapp/env.example.template +9 -0
  46. package/templates/webapp/next.config.ts +1 -0
  47. package/templates/webapp/package.json.template +8 -4
  48. package/templates/webapp/scripts/seed.ts +87 -36
  49. package/templates/webapp/scripts/setup-database.ts +7 -1
  50. package/templates/webapp/scripts/start.sh +0 -9
  51. package/templates/webapp/src/access/access.manifest.ts +15 -0
  52. package/templates/webapp/src/access/schema.zed +7 -0
  53. package/templates/webapp/src/app/(app)/admin/_lib/PrincipalRoleTable.tsx +113 -0
  54. package/templates/webapp/src/app/(app)/admin/_lib/accessAdmin.ts +85 -0
  55. package/templates/webapp/src/app/(app)/admin/groups/page.tsx +117 -0
  56. package/templates/webapp/src/app/(app)/admin/users/page.tsx +79 -0
  57. package/templates/webapp/src/app/(app)/layout.tsx +16 -2
  58. package/templates/webapp/src/app/(app)/page.tsx +1 -12
  59. package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +2 -5
  60. package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +2 -5
  61. package/templates/webapp/src/config/getEnvConfig.ts +8 -0
  62. package/templates/webapp/src/drizzle/db.ts +3 -4
  63. package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +1 -57
  64. package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +1 -347
  65. package/templates/webapp/src/drizzle/schema/index.ts +3 -4
  66. package/templates/webapp/src/lib/auth/index.ts +6 -81
  67. package/templates/webapp/src/server/api/root.ts +4 -1
  68. package/templates/webapp/src/server/api/routers/access.ts +13 -0
  69. package/templates/webapp/src/server/trpc.ts +42 -8
  70. package/templates/webapp/src/services/DatabaseService.ts +4 -5
  71. package/templates/webapp/src/services/access/AppAccessControl.ts +39 -0
  72. package/dist/init-OeK4Yk6_.js.map +0 -1
  73. package/templates/webapp/scripts/create-user.ts +0 -47
  74. package/templates/webapp/src/drizzle/schema/auth/accounts.ts +0 -33
  75. package/templates/webapp/src/drizzle/schema/auth/sessions.ts +0 -25
  76. package/templates/webapp/src/drizzle/schema/auth/users.ts +0 -38
  77. package/templates/webapp/src/drizzle/schema/auth/verifications.ts +0 -19
@@ -1 +1 @@
1
- {"version":3,"file":"upstream-F6m8zRBQ.js","names":[],"sources":["../src/commands/upstream.ts"],"sourcesContent":["import path from \"node:path\";\nimport chalk from \"chalk\";\nimport fs from \"fs-extra\";\nimport { getFileAtTag } from \"../utils/git-ops.js\";\nimport {\n readManifest,\n resolveMosaicTemplatePath,\n type MosaicManifest,\n} from \"../utils/manifest.js\";\n\nexport interface UpstreamOptions {\n mosaicTemplatePath?: string;\n files?: string[];\n}\n\nasync function generateUpstreamContext(\n manifest: MosaicManifest,\n mosaicTemplatePath: string,\n tag: string,\n appDir: string,\n files: string[],\n): Promise<string> {\n let content = `# Mosaic Upstream Context\n\n## App Info\n- **App name:** ${manifest.placeholders.__APP_NAME__ || \"unknown\"}\n- **Template:** ${manifest.templateType}\n- **Template version:** ${manifest.templateVersion}\n\n## Placeholder Mappings\n\nWhen generalizing app code back to template, replace these values with placeholder tokens:\n\n| Value | Placeholder |\n|-------|------------|\n${Object.entries(manifest.placeholders)\n .sort((a, b) => b[1].length - a[1].length) // longest first to avoid partial matches\n .map(([k, v]) => `| \\`${v}\\` | \\`${k}\\` |`)\n .join(\"\\n\")}\n\n## Files to Review\n\n`;\n\n for (const file of files) {\n const appFilePath = path.resolve(appDir, file);\n const templateRelPath = `${manifest.source.templatePath}/${file}`;\n\n const appContent = (await fs.pathExists(appFilePath))\n ? await fs.readFile(appFilePath, \"utf-8\")\n : null;\n const templateContent = getFileAtTag(\n mosaicTemplatePath,\n tag,\n templateRelPath,\n );\n\n content += `### ${file}\\n\\n`;\n\n if (!templateContent && appContent) {\n content += `**New file** (not in template at ${manifest.templateVersion})\\n\\n`;\n content += `\\`\\`\\`\\n${appContent}\\n\\`\\`\\`\\n\\n`;\n } else if (templateContent && !appContent) {\n content += `**Deleted** (exists in template but not in app)\\n\\n`;\n } else if (appContent && templateContent) {\n content += `**App version:**\\n\\`\\`\\`\\n${appContent}\\n\\`\\`\\`\\n\\n`;\n content += `**Template version (at ${manifest.templateVersion}):**\\n\\`\\`\\`\\n${templateContent}\\n\\`\\`\\`\\n\\n`;\n } else {\n content += `**Not found** (file does not exist in app or template)\\n\\n`;\n }\n }\n\n content += `## Instructions\n\n1. Review each file above\n2. Determine which changes are generalizable (useful for all apps) vs app-specific\n3. For generalizable changes: apply them to the template at \\`${manifest.source.templatePath}/\\`\n4. When applying, replace app-specific values with placeholders using the mapping table above (replace longest values first)\n5. After applying, bump the version in \\`packages/create-mosaic-module/template-versions.json\\`\n6. Run \\`pnpm template:tag\\` to create the new version tag\n7. Delete this file (\\`.mosaic-upstream-context.md\\`) when done\n`;\n\n return content;\n}\n\nexport async function upstreamCommand(options: UpstreamOptions): Promise<void> {\n const cwd = process.cwd();\n\n try {\n const manifest = await readManifest(cwd);\n const mosaicTemplatePath = resolveMosaicTemplatePath(options);\n\n if (!options.files || options.files.length === 0) {\n console.error(\n chalk.red(\"Specify files with --files <file1> <file2> ...\"),\n );\n console.log(\n chalk.dim(\n \" Example: create upstream --files src/config/getEnvConfig.ts\",\n ),\n );\n process.exit(1);\n }\n\n const tag = `template/${manifest.templateType}/${manifest.templateVersion}`;\n\n const context = await generateUpstreamContext(\n manifest,\n mosaicTemplatePath,\n tag,\n cwd,\n options.files,\n );\n const contextPath = path.join(cwd, \".mosaic-upstream-context.md\");\n await fs.writeFile(contextPath, context);\n\n console.log();\n console.log(chalk.bold(\"Upstream Context Generated\"));\n console.log();\n console.log(chalk.dim(\" Files:\"), options.files.join(\", \"));\n console.log(chalk.dim(\" Context file:\"), \".mosaic-upstream-context.md\");\n console.log();\n console.log(\"Next steps:\");\n console.log(chalk.dim(\" 1.\"), \"Open Claude Code in the mosaic repo\");\n console.log(\n chalk.dim(\" 2.\"),\n `Tell Claude: \"Read ${path.resolve(cwd, \".mosaic-upstream-context.md\")} and apply generalizable changes to the template\"`,\n );\n console.log(chalk.dim(\" 3.\"), \"Review Claude's changes to the template\");\n console.log();\n } catch (error) {\n console.error(chalk.red((error as Error).message));\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;AAeA,eAAe,wBACb,UACA,oBACA,KACA,QACA,OACiB;CACjB,IAAI,UAAU;;;kBAGE,SAAS,aAAa,gBAAgB,UAAU;kBAChD,SAAS,aAAa;0BACd,SAAS,gBAAgB;;;;;;;;EAQjD,OAAO,QAAQ,SAAS,aAAa,CACpC,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,GAAG,OAAO,CACzC,KAAK,CAAC,GAAG,OAAO,OAAO,EAAE,SAAS,EAAE,MAAM,CAC1C,KAAK,KAAK,CAAC;;;;;AAMZ,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,cAAc,KAAK,QAAQ,QAAQ,KAAK;EAC9C,MAAM,kBAAkB,GAAG,SAAS,OAAO,aAAa,GAAG;EAE3D,MAAM,aAAc,MAAM,GAAG,WAAW,YAAY,GAChD,MAAM,GAAG,SAAS,aAAa,QAAQ,GACvC;EACJ,MAAM,kBAAkB,aACtB,oBACA,KACA,gBACD;AAED,aAAW,OAAO,KAAK;AAEvB,MAAI,CAAC,mBAAmB,YAAY;AAClC,cAAW,oCAAoC,SAAS,gBAAgB;AACxE,cAAW,WAAW,WAAW;aACxB,mBAAmB,CAAC,WAC7B,YAAW;WACF,cAAc,iBAAiB;AACxC,cAAW,6BAA6B,WAAW;AACnD,cAAW,0BAA0B,SAAS,gBAAgB,gBAAgB,gBAAgB;QAE9F,YAAW;;AAIf,YAAW;;;;gEAImD,SAAS,OAAO,aAAa;;;;;;AAO3F,QAAO;;AAGT,eAAsB,gBAAgB,SAAyC;CAC7E,MAAM,MAAM,QAAQ,KAAK;AAEzB,KAAI;EACF,MAAM,WAAW,MAAM,aAAa,IAAI;EACxC,MAAM,qBAAqB,0BAA0B,QAAQ;AAE7D,MAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,WAAW,GAAG;AAChD,WAAQ,MACN,MAAM,IAAI,iDAAiD,CAC5D;AACD,WAAQ,IACN,MAAM,IACJ,gEACD,CACF;AACD,WAAQ,KAAK,EAAE;;EAKjB,MAAM,UAAU,MAAM,wBACpB,UACA,oBACA,YALsB,SAAS,aAAa,GAAG,SAAS,mBAMxD,KACA,QAAQ,MACT;EACD,MAAM,cAAc,KAAK,KAAK,KAAK,8BAA8B;AACjE,QAAM,GAAG,UAAU,aAAa,QAAQ;AAExC,UAAQ,KAAK;AACb,UAAQ,IAAI,MAAM,KAAK,6BAA6B,CAAC;AACrD,UAAQ,KAAK;AACb,UAAQ,IAAI,MAAM,IAAI,WAAW,EAAE,QAAQ,MAAM,KAAK,KAAK,CAAC;AAC5D,UAAQ,IAAI,MAAM,IAAI,kBAAkB,EAAE,8BAA8B;AACxE,UAAQ,KAAK;AACb,UAAQ,IAAI,cAAc;AAC1B,UAAQ,IAAI,MAAM,IAAI,OAAO,EAAE,sCAAsC;AACrE,UAAQ,IACN,MAAM,IAAI,OAAO,EACjB,sBAAsB,KAAK,QAAQ,KAAK,8BAA8B,CAAC,mDACxE;AACD,UAAQ,IAAI,MAAM,IAAI,OAAO,EAAE,0CAA0C;AACzE,UAAQ,KAAK;UACN,OAAO;AACd,UAAQ,MAAM,MAAM,IAAK,MAAgB,QAAQ,CAAC;AAClD,UAAQ,KAAK,EAAE"}
1
+ {"version":3,"file":"upstream-D-LH_1z4.js","names":[],"sources":["../src/commands/upstream.ts"],"sourcesContent":["import path from \"node:path\";\nimport chalk from \"chalk\";\nimport fs from \"fs-extra\";\nimport { getFileAtTag } from \"../utils/git-ops.js\";\nimport {\n readManifest,\n resolveMosaicTemplatePath,\n type MosaicManifest,\n} from \"../utils/manifest.js\";\n\nexport interface UpstreamOptions {\n mosaicTemplatePath?: string;\n files?: string[];\n}\n\nasync function generateUpstreamContext(\n manifest: MosaicManifest,\n mosaicTemplatePath: string,\n tag: string,\n appDir: string,\n files: string[],\n): Promise<string> {\n let content = `# Mosaic Upstream Context\n\n## App Info\n- **App name:** ${manifest.placeholders.__APP_NAME__ || \"unknown\"}\n- **Template:** ${manifest.templateType}\n- **Template version:** ${manifest.templateVersion}\n\n## Placeholder Mappings\n\nWhen generalizing app code back to template, replace these values with placeholder tokens:\n\n| Value | Placeholder |\n|-------|------------|\n${Object.entries(manifest.placeholders)\n .sort((a, b) => b[1].length - a[1].length) // longest first to avoid partial matches\n .map(([k, v]) => `| \\`${v}\\` | \\`${k}\\` |`)\n .join(\"\\n\")}\n\n## Files to Review\n\n`;\n\n for (const file of files) {\n const appFilePath = path.resolve(appDir, file);\n const templateRelPath = `${manifest.source.templatePath}/${file}`;\n\n const appContent = (await fs.pathExists(appFilePath))\n ? await fs.readFile(appFilePath, \"utf-8\")\n : null;\n const templateContent = getFileAtTag(\n mosaicTemplatePath,\n tag,\n templateRelPath,\n );\n\n content += `### ${file}\\n\\n`;\n\n if (!templateContent && appContent) {\n content += `**New file** (not in template at ${manifest.templateVersion})\\n\\n`;\n content += `\\`\\`\\`\\n${appContent}\\n\\`\\`\\`\\n\\n`;\n } else if (templateContent && !appContent) {\n content += `**Deleted** (exists in template but not in app)\\n\\n`;\n } else if (appContent && templateContent) {\n content += `**App version:**\\n\\`\\`\\`\\n${appContent}\\n\\`\\`\\`\\n\\n`;\n content += `**Template version (at ${manifest.templateVersion}):**\\n\\`\\`\\`\\n${templateContent}\\n\\`\\`\\`\\n\\n`;\n } else {\n content += `**Not found** (file does not exist in app or template)\\n\\n`;\n }\n }\n\n content += `## Instructions\n\n1. Review each file above\n2. Determine which changes are generalizable (useful for all apps) vs app-specific\n3. For generalizable changes: apply them to the template at \\`${manifest.source.templatePath}/\\`\n4. When applying, replace app-specific values with placeholders using the mapping table above (replace longest values first)\n5. After applying, bump the version in \\`packages/create-mosaic-module/template-versions.json\\`\n6. Run \\`pnpm template:tag\\` to create the new version tag\n7. Delete this file (\\`.mosaic-upstream-context.md\\`) when done\n`;\n\n return content;\n}\n\nexport async function upstreamCommand(options: UpstreamOptions): Promise<void> {\n const cwd = process.cwd();\n\n try {\n const manifest = await readManifest(cwd);\n const mosaicTemplatePath = resolveMosaicTemplatePath(options);\n\n if (!options.files || options.files.length === 0) {\n console.error(\n chalk.red(\"Specify files with --files <file1> <file2> ...\"),\n );\n console.log(\n chalk.dim(\n \" Example: create upstream --files src/config/getEnvConfig.ts\",\n ),\n );\n process.exit(1);\n }\n\n const tag = `template/${manifest.templateType}/${manifest.templateVersion}`;\n\n const context = await generateUpstreamContext(\n manifest,\n mosaicTemplatePath,\n tag,\n cwd,\n options.files,\n );\n const contextPath = path.join(cwd, \".mosaic-upstream-context.md\");\n await fs.writeFile(contextPath, context);\n\n console.log();\n console.log(chalk.bold(\"Upstream Context Generated\"));\n console.log();\n console.log(chalk.dim(\" Files:\"), options.files.join(\", \"));\n console.log(chalk.dim(\" Context file:\"), \".mosaic-upstream-context.md\");\n console.log();\n console.log(\"Next steps:\");\n console.log(chalk.dim(\" 1.\"), \"Open Claude Code in the mosaic repo\");\n console.log(\n chalk.dim(\" 2.\"),\n `Tell Claude: \"Read ${path.resolve(cwd, \".mosaic-upstream-context.md\")} and apply generalizable changes to the template\"`,\n );\n console.log(chalk.dim(\" 3.\"), \"Review Claude's changes to the template\");\n console.log();\n } catch (error) {\n console.error(chalk.red((error as Error).message));\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;AAeA,eAAe,wBACb,UACA,oBACA,KACA,QACA,OACiB;CACjB,IAAI,UAAU;;;kBAGE,SAAS,aAAa,gBAAgB,UAAU;kBAChD,SAAS,aAAa;0BACd,SAAS,gBAAgB;;;;;;;;EAQjD,OAAO,QAAQ,SAAS,aAAa,CACpC,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,GAAG,OAAO,CACzC,KAAK,CAAC,GAAG,OAAO,OAAO,EAAE,SAAS,EAAE,MAAM,CAC1C,KAAK,KAAK,CAAC;;;;;AAMZ,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,cAAc,KAAK,QAAQ,QAAQ,KAAK;EAC9C,MAAM,kBAAkB,GAAG,SAAS,OAAO,aAAa,GAAG;EAE3D,MAAM,aAAc,MAAM,GAAG,WAAW,YAAY,GAChD,MAAM,GAAG,SAAS,aAAa,QAAQ,GACvC;EACJ,MAAM,kBAAkB,aACtB,oBACA,KACA,gBACD;AAED,aAAW,OAAO,KAAK;AAEvB,MAAI,CAAC,mBAAmB,YAAY;AAClC,cAAW,oCAAoC,SAAS,gBAAgB;AACxE,cAAW,WAAW,WAAW;aACxB,mBAAmB,CAAC,WAC7B,YAAW;WACF,cAAc,iBAAiB;AACxC,cAAW,6BAA6B,WAAW;AACnD,cAAW,0BAA0B,SAAS,gBAAgB,gBAAgB,gBAAgB;QAE9F,YAAW;;AAIf,YAAW;;;;gEAImD,SAAS,OAAO,aAAa;;;;;;AAO3F,QAAO;;AAGT,eAAsB,gBAAgB,SAAyC;CAC7E,MAAM,MAAM,QAAQ,KAAK;AAEzB,KAAI;EACF,MAAM,WAAW,MAAM,aAAa,IAAI;EACxC,MAAM,qBAAqB,0BAA0B,QAAQ;AAE7D,MAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,WAAW,GAAG;AAChD,WAAQ,MACN,MAAM,IAAI,iDAAiD,CAC5D;AACD,WAAQ,IACN,MAAM,IACJ,gEACD,CACF;AACD,WAAQ,KAAK,EAAE;;EAKjB,MAAM,UAAU,MAAM,wBACpB,UACA,oBACA,YALsB,SAAS,aAAa,GAAG,SAAS,mBAMxD,KACA,QAAQ,MACT;EACD,MAAM,cAAc,KAAK,KAAK,KAAK,8BAA8B;AACjE,QAAM,GAAG,UAAU,aAAa,QAAQ;AAExC,UAAQ,KAAK;AACb,UAAQ,IAAI,MAAM,KAAK,6BAA6B,CAAC;AACrD,UAAQ,KAAK;AACb,UAAQ,IAAI,MAAM,IAAI,WAAW,EAAE,QAAQ,MAAM,KAAK,KAAK,CAAC;AAC5D,UAAQ,IAAI,MAAM,IAAI,kBAAkB,EAAE,8BAA8B;AACxE,UAAQ,KAAK;AACb,UAAQ,IAAI,cAAc;AAC1B,UAAQ,IAAI,MAAM,IAAI,OAAO,EAAE,sCAAsC;AACrE,UAAQ,IACN,MAAM,IAAI,OAAO,EACjB,sBAAsB,KAAK,QAAQ,KAAK,8BAA8B,CAAC,mDACxE;AACD,UAAQ,IAAI,MAAM,IAAI,OAAO,EAAE,0CAA0C;AACzE,UAAQ,KAAK;UACN,OAAO;AACd,UAAQ,MAAM,MAAM,IAAK,MAAgB,QAAQ,CAAC;AAClD,UAAQ,KAAK,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percepta/create",
3
- "version": "3.1.5",
3
+ "version": "3.3.0",
4
4
  "description": "Scaffold a new Mosaic package",
5
5
  "keywords": [
6
6
  "cli",
@@ -38,7 +38,7 @@
38
38
  "@types/node": "^24.1.0",
39
39
  "@types/validate-npm-package-name": "^4.0.2",
40
40
  "vitest": "^4.0.0",
41
- "@percepta/build": "0.5.0"
41
+ "@percepta/build": "1.0.0"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=18.0.0"
@@ -1,4 +1,4 @@
1
1
  {
2
- "webapp": "1.0.0",
2
+ "webapp": "1.1.0",
3
3
  "library": "1.0.0"
4
4
  }
@@ -0,0 +1,38 @@
1
+ name: Access Control
2
+
3
+ on:
4
+ pull_request: {}
5
+
6
+ env:
7
+ PNPM_VERSION: 10.x
8
+
9
+ jobs:
10
+ access-control:
11
+ name: Merge and Validate Access Schema
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout repository
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup PNPM
19
+ uses: pnpm/action-setup@v4
20
+ with:
21
+ version: ${{ env.PNPM_VERSION }}
22
+
23
+ - name: Setup Node.js
24
+ uses: actions/setup-node@v4
25
+ with:
26
+ node-version: 22
27
+ cache: pnpm
28
+
29
+ - name: Install dependencies
30
+ run: pnpm install --frozen-lockfile
31
+
32
+ - name: Merge and validate access schema
33
+ run: |
34
+ if find packages -path '*/src/access/access.manifest.ts' | grep -q .; then
35
+ pnpm access:validate
36
+ else
37
+ echo "No app access manifests found yet; skipping access schema validation."
38
+ fi
@@ -1,6 +1,6 @@
1
1
  # __APP_TITLE__
2
2
 
3
- A monorepo powered by [pnpm workspaces](https://pnpm.io/workspaces).
3
+ A customer monorepo powered by [pnpm workspaces](https://pnpm.io/workspaces).
4
4
 
5
5
  ## Getting Started
6
6
 
@@ -11,6 +11,12 @@ pnpm install
11
11
  # Run development mode for all packages
12
12
  pnpm dev
13
13
 
14
+ # Set up local services, access-control topology, databases, and seed users
15
+ pnpm run setup
16
+
17
+ # Merge and validate customer access-control schema
18
+ pnpm access:validate
19
+
14
20
  # Build all packages
15
21
  pnpm build
16
22
 
@@ -24,10 +30,44 @@ pnpm lint
24
30
  ## Structure
25
31
 
26
32
  ```
33
+ access/ # Customer-level SpiceDB fixtures and generated merge artifacts
34
+ auth/ # Shared Better Auth users/groups package for this customer
27
35
  packages/
28
- └── your-package/ # Add your packages here
36
+ └── your-package/ # Application and library packages
37
+ ```
38
+
39
+ ## Access Control
40
+
41
+ Application builders define app-local Zed schemas in each package's
42
+ `src/access/` directory. The root access scripts merge those schemas with the
43
+ shared `core/*` schema and apply customer-owned grants such
44
+ as application owners, application members, and bootstrap customer admins.
45
+
46
+ ```bash
47
+ pnpm access:merge
48
+ pnpm access:validate
49
+ pnpm access:apply
29
50
  ```
30
51
 
52
+ PR CI merges the customer schema and runs static schema/manifest validation.
53
+ `access:apply` is reserved for trusted deploy jobs and should run once per
54
+ target environment with that environment's SpiceDB credentials.
55
+
56
+ For local development, copy the example fixture files in `access/`, fill in
57
+ customer-global user/group IDs from `auth/`, then run:
58
+
59
+ ```bash
60
+ pnpm access:seed-grants
61
+ pnpm access:bootstrap-customer-admin -- --subject core/user:<user-id>
62
+ ```
63
+
64
+ ## Shared Auth
65
+
66
+ The `auth/` workspace wires the customer-global Better Auth schema from
67
+ `@percepta/auth`, including `users`, `groups`, and `group_members`. Apps should
68
+ consume this shared identity layer instead of creating app-local users or
69
+ groups.
70
+
31
71
  ## Adding a new package
32
72
 
33
73
  Create a new directory in `packages/` with its own `package.json`:
@@ -0,0 +1,39 @@
1
+ # Customer Access Control
2
+
3
+ This directory owns the customer-level SpiceDB deployment surface:
4
+
5
+ - `pnpm access:merge` combines every app's `src/access/schema.zed` with the shared core schema.
6
+ - `pnpm access:validate` validates the merged schema and app manifests.
7
+ - `pnpm access:apply` writes the merged schema and stable application topology links to SpiceDB.
8
+ - `pnpm access:seed-grants` and `pnpm access:apply-bootstrap-grants` apply YAML fixture grants.
9
+ - `pnpm access:bootstrap-customer-admin` creates the first direct customer-admin grant.
10
+ - `pnpm access:reconcile` repairs SpiceDB from an explicit local projection.
11
+
12
+ The source of truth for app-specific permissions remains the app's authored Zed
13
+ file. This package owns only customer-level composition and customer-admin
14
+ bootstrap.
15
+
16
+ ## Local Bootstrap
17
+
18
+ ```bash
19
+ pnpm access:merge
20
+ pnpm access:validate
21
+ cp access/bootstrap-grants.yaml.example access/bootstrap-grants.yaml
22
+ pnpm access:apply-local
23
+ pnpm access:apply-bootstrap-grants -- --endpoint localhost:50051 --insecure --key dev-spicedb-token
24
+ ```
25
+
26
+ Use `core/user:<users.id>` and `core/group:<groups.id>#member` subjects. The IDs
27
+ come from the shared `auth/` package tables, not from per-app user tables.
28
+
29
+ ## Production Promotion
30
+
31
+ Run `pnpm access:validate` in PR CI. Run `pnpm access:apply` only from trusted deploy jobs
32
+ with the target environment's SpiceDB credentials. Promote the same merged
33
+ schema artifact through environments before app code that depends on new
34
+ relations or permissions.
35
+
36
+ Use expand/contract for destructive changes: add the new shape, deploy
37
+ dual-write/dual-read code, backfill with idempotent relationship writes,
38
+ reconcile, then remove old relationships and schema definitions in a later
39
+ deploy.
@@ -0,0 +1,9 @@
1
+ # Production bootstrap grants for first deploy / break-glass access.
2
+ # Copy to bootstrap-grants.yaml, replace IDs with auth.users.id values, then run:
3
+ # pnpm access:apply-bootstrap-grants
4
+ customerAdmins:
5
+ - userId: "00000000-0000-0000-0000-000000000000"
6
+
7
+ applications: []
8
+ appRoles: []
9
+ resourceRelations: []
@@ -0,0 +1,19 @@
1
+ # Local development grants. Copy to dev-grants.yaml and replace IDs with rows
2
+ # from the shared auth.users / auth.groups tables.
3
+ customerAdmins:
4
+ - userId: "00000000-0000-0000-0000-000000000000"
5
+
6
+ applications:
7
+ - appNamespace: "people_app"
8
+ owners:
9
+ - userId: "00000000-0000-0000-0000-000000000000"
10
+ members:
11
+ - groupId: "11111111-1111-1111-1111-111111111111"
12
+
13
+ appRoles:
14
+ - appNamespace: "people_app"
15
+ role: "admin"
16
+ subjects:
17
+ - groupId: "11111111-1111-1111-1111-111111111111"
18
+
19
+ resourceRelations: []
@@ -0,0 +1,8 @@
1
+ # Future fixture source for the auth-owned groupSync adapter.
2
+ # SCIM/JIT is the production source of truth; this file is only for local dev.
3
+ groups:
4
+ - externalId: "dev-group-admins"
5
+ name: "Development Admins"
6
+ source: "fixture"
7
+ members:
8
+ - userExternalId: "dev-admin"
@@ -0,0 +1,11 @@
1
+ # Explicit local projection for access:reconcile.
2
+ applications:
3
+ - appNamespace: "people_app"
4
+
5
+ groupMemberships:
6
+ - groupId: "11111111-1111-1111-1111-111111111111"
7
+ userIds:
8
+ - "00000000-0000-0000-0000-000000000000"
9
+
10
+ deletedUserIds: []
11
+ deletedGroupIds: []
@@ -0,0 +1,27 @@
1
+ # Shared Auth
2
+
3
+ This workspace wires the customer-global Better Auth database schema from
4
+ `@percepta/auth`. Apps in the customer monorepo should import this package for
5
+ session validation and user / group table references instead of creating
6
+ app-local auth tables.
7
+
8
+ Import auth as `@__APP_NAME__/auth`, the database handle as
9
+ `@__APP_NAME__/auth/db`, and table definitions as `@__APP_NAME__/auth/schema`
10
+ from app packages.
11
+
12
+ The important identity invariant is:
13
+
14
+ - `core/user:<id>` uses `users.id` from this package.
15
+ - `core/group:<id>#member` uses `groups.id` from this package.
16
+ - `group_members` is the local projection that reconcile uses to repair
17
+ SpiceDB group membership relationships.
18
+
19
+ SCIM/JIT protocol handlers are intentionally not implemented here yet; they
20
+ should feed the access-control `groupSync` ingestion contract when that adapter
21
+ lands.
22
+
23
+ ## Database
24
+
25
+ By default this package uses the customer monorepo database name generated at
26
+ scaffold time. Override with `AUTH_DATABASE_NAME` or `AUTH_DATABASE_URL` when
27
+ the shared auth database lives somewhere else.
@@ -0,0 +1,13 @@
1
+ import type { Config } from "drizzle-kit";
2
+ import { getAuthDatabaseConnectionString } from "./src/config/database";
3
+
4
+ const config: Config = {
5
+ schema: "./src/drizzle/schema",
6
+ out: "./src/drizzle/migrations",
7
+ dialect: "postgresql",
8
+ dbCredentials: {
9
+ url: getAuthDatabaseConnectionString(),
10
+ },
11
+ };
12
+
13
+ export default config;
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@__APP_NAME__/auth",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "description": "Shared customer identity package.",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./db": "./src/drizzle/db.ts",
10
+ "./schema": "./src/drizzle/schema/index.ts"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "db:generate": "drizzle-kit generate",
15
+ "db:migrate": "drizzle-kit migrate",
16
+ "db:setup": "tsx ./scripts/setup-database.ts",
17
+ "db:setup-and-migrate": "pnpm db:setup && pnpm db:migrate"
18
+ },
19
+ "dependencies": {
20
+ "@percepta/auth": "0.1.0",
21
+ "drizzle-orm": "^0.45.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.1.0",
25
+ "drizzle-kit": "^0.31.4",
26
+ "tsx": "^4.20.3",
27
+ "typescript": "^5.8.3"
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ import { setupAuthDatabase } from "@percepta/auth";
2
+ import { getAuthDatabaseConfig } from "../src/config/database";
3
+
4
+ async function main(): Promise<void> {
5
+ await setupAuthDatabase({ config: getAuthDatabaseConfig() });
6
+ }
7
+
8
+ void main().catch((error) => {
9
+ console.error("Shared auth database setup failed:", error);
10
+ process.exit(1);
11
+ });
@@ -0,0 +1,47 @@
1
+ import { createLazyAuth, createPerceptaAuth } from "@percepta/auth/better-auth";
2
+ import { db } from "./drizzle/db";
3
+ import { accounts } from "./drizzle/schema/auth/accounts";
4
+ import { sessions } from "./drizzle/schema/auth/sessions";
5
+ import { verifications } from "./drizzle/schema/auth/verifications";
6
+ import { users } from "./drizzle/schema/users";
7
+
8
+ // eslint-disable-next-line n/no-process-env -- detecting Next.js build phase
9
+ const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
10
+
11
+ function requiredEnv(name: string): string {
12
+ const value = process.env[name];
13
+ if (value == null || value.length === 0) {
14
+ throw new Error(`${name} is required.`);
15
+ }
16
+ return value;
17
+ }
18
+
19
+ function getSecret(): string {
20
+ if (isBuildPhase) {
21
+ return "build-placeholder-not-used-at-runtime";
22
+ }
23
+
24
+ return requiredEnv("BETTER_AUTH_SECRET");
25
+ }
26
+
27
+ function createAuth() {
28
+ return createPerceptaAuth({
29
+ baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
30
+ database: db,
31
+ schema: {
32
+ user: users,
33
+ session: sessions,
34
+ account: accounts,
35
+ verification: verifications,
36
+ },
37
+ secret: getSecret(),
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Lazy proxy so app builds can import the shared auth package without requiring
43
+ * runtime secrets until Better Auth is actually used.
44
+ */
45
+ export const auth = createLazyAuth(createAuth);
46
+
47
+ export type BetterAuthSession = typeof auth.$Infer.Session;
@@ -0,0 +1,15 @@
1
+ import {
2
+ createAuthDatabaseConnectionString,
3
+ readAuthDatabaseConfig,
4
+ type AuthDatabaseConfig,
5
+ } from "@percepta/auth";
6
+
7
+ export type { AuthDatabaseConfig } from "@percepta/auth";
8
+
9
+ export function getAuthDatabaseConfig(): AuthDatabaseConfig {
10
+ return readAuthDatabaseConfig({ defaultDatabaseName: "__DB_NAME__" });
11
+ }
12
+
13
+ export function getAuthDatabaseConnectionString(): string {
14
+ return createAuthDatabaseConnectionString(getAuthDatabaseConfig());
15
+ }
@@ -0,0 +1,8 @@
1
+ import { createAuthDatabase } from "@percepta/auth/drizzle";
2
+ import { getAuthDatabaseConnectionString } from "../config/database";
3
+ import * as schema from "./schema";
4
+
5
+ export const { client, db } = createAuthDatabase({
6
+ connectionString: getAuthDatabaseConnectionString(),
7
+ schema,
8
+ });
@@ -0,0 +1,89 @@
1
+ CREATE TABLE "users" (
2
+ "id" uuid PRIMARY KEY NOT NULL,
3
+ "external_id" text,
4
+ "name" text NOT NULL,
5
+ "email" text NOT NULL,
6
+ "email_verified" boolean DEFAULT false NOT NULL,
7
+ "image" text,
8
+ "role" text DEFAULT 'user' NOT NULL,
9
+ "banned" boolean DEFAULT false,
10
+ "ban_reason" text,
11
+ "ban_expires" timestamp,
12
+ "created_at" timestamp DEFAULT now() NOT NULL,
13
+ "updated_at" timestamp DEFAULT now() NOT NULL,
14
+ CONSTRAINT "users_email_unique" UNIQUE("email")
15
+ );
16
+ --> statement-breakpoint
17
+ CREATE TABLE "account" (
18
+ "id" text PRIMARY KEY NOT NULL,
19
+ "user_id" uuid NOT NULL,
20
+ "account_id" text NOT NULL,
21
+ "provider_id" text NOT NULL,
22
+ "access_token" text,
23
+ "refresh_token" text,
24
+ "expires_at" integer,
25
+ "access_token_expires_at" timestamp,
26
+ "refresh_token_expires_at" timestamp,
27
+ "scope" text,
28
+ "id_token" text,
29
+ "password" text,
30
+ "created_at" timestamp NOT NULL,
31
+ "updated_at" timestamp NOT NULL
32
+ );
33
+ --> statement-breakpoint
34
+ CREATE TABLE "session" (
35
+ "id" text PRIMARY KEY NOT NULL,
36
+ "user_id" uuid NOT NULL,
37
+ "token" text NOT NULL,
38
+ "expires_at" timestamp NOT NULL,
39
+ "ip_address" text,
40
+ "user_agent" text,
41
+ "impersonated_by" text,
42
+ "created_at" timestamp NOT NULL,
43
+ "updated_at" timestamp NOT NULL,
44
+ CONSTRAINT "session_token_unique" UNIQUE("token")
45
+ );
46
+ --> statement-breakpoint
47
+ CREATE TABLE "verification" (
48
+ "id" text PRIMARY KEY NOT NULL,
49
+ "identifier" text NOT NULL,
50
+ "value" text NOT NULL,
51
+ "expires_at" timestamp NOT NULL,
52
+ "created_at" timestamp,
53
+ "updated_at" timestamp
54
+ );
55
+ --> statement-breakpoint
56
+ CREATE TABLE "groups" (
57
+ "id" uuid PRIMARY KEY NOT NULL,
58
+ "external_id" text NOT NULL,
59
+ "name" text NOT NULL,
60
+ "source" text NOT NULL,
61
+ "deleted_at" timestamp,
62
+ "created_at" timestamp DEFAULT now() NOT NULL,
63
+ "updated_at" timestamp DEFAULT now() NOT NULL
64
+ );
65
+ --> statement-breakpoint
66
+ CREATE TABLE "group_members" (
67
+ "group_id" uuid NOT NULL,
68
+ "user_id" uuid NOT NULL,
69
+ "created_at" timestamp DEFAULT now() NOT NULL,
70
+ CONSTRAINT "group_members_pkey" PRIMARY KEY("group_id","user_id")
71
+ );
72
+ --> statement-breakpoint
73
+ ALTER TABLE "account" ADD CONSTRAINT "account_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
74
+ --> statement-breakpoint
75
+ ALTER TABLE "session" ADD CONSTRAINT "session_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
76
+ --> statement-breakpoint
77
+ ALTER TABLE "group_members" ADD CONSTRAINT "group_members_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "groups"("id") ON DELETE cascade ON UPDATE no action;
78
+ --> statement-breakpoint
79
+ ALTER TABLE "group_members" ADD CONSTRAINT "group_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
80
+ --> statement-breakpoint
81
+ CREATE UNIQUE INDEX "users_lower_email_index" ON "users" USING btree (lower("email"));
82
+ --> statement-breakpoint
83
+ CREATE UNIQUE INDEX "users_external_id_index" ON "users" USING btree ("external_id") WHERE "users"."external_id" IS NOT NULL;
84
+ --> statement-breakpoint
85
+ CREATE UNIQUE INDEX "groups_live_external_id_index" ON "groups" USING btree ("external_id") WHERE "groups"."deleted_at" IS NULL;
86
+ --> statement-breakpoint
87
+ CREATE INDEX "groups_source_index" ON "groups" USING btree ("source");
88
+ --> statement-breakpoint
89
+ CREATE INDEX "group_members_user_id_index" ON "group_members" USING btree ("user_id");
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1777511400000,
9
+ "tag": "0000_shared_auth",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ import { createAccountsTable } from "@percepta/auth/drizzle";
2
+ import { users } from "../users";
3
+
4
+ export const accounts = createAccountsTable({ usersTable: users });
5
+
6
+ export type Account = typeof accounts.$inferSelect;
7
+ export type NewAccount = typeof accounts.$inferInsert;
@@ -0,0 +1,7 @@
1
+ import { createSessionsTable } from "@percepta/auth/drizzle";
2
+ import { users } from "../users";
3
+
4
+ export const sessions = createSessionsTable({ usersTable: users });
5
+
6
+ export type Session = typeof sessions.$inferSelect;
7
+ export type NewSession = typeof sessions.$inferInsert;
@@ -0,0 +1,6 @@
1
+ import { createVerificationsTable } from "@percepta/auth/drizzle";
2
+
3
+ export const verifications = createVerificationsTable();
4
+
5
+ export type Verification = typeof verifications.$inferSelect;
6
+ export type NewVerification = typeof verifications.$inferInsert;
@@ -0,0 +1,16 @@
1
+ import {
2
+ createGroupMembersTable,
3
+ createGroupsTable,
4
+ } from "@percepta/auth/drizzle";
5
+ import { users } from "./users";
6
+
7
+ export const groups = createGroupsTable();
8
+ export const groupMembers = createGroupMembersTable({
9
+ groupsTable: groups,
10
+ usersTable: users,
11
+ });
12
+
13
+ export type Group = typeof groups.$inferSelect;
14
+ export type NewGroup = typeof groups.$inferInsert;
15
+ export type GroupMember = typeof groupMembers.$inferSelect;
16
+ export type NewGroupMember = typeof groupMembers.$inferInsert;
@@ -0,0 +1,5 @@
1
+ export { accounts } from "./auth/accounts";
2
+ export { sessions } from "./auth/sessions";
3
+ export { verifications } from "./auth/verifications";
4
+ export { groupMembers, groups } from "./groups";
5
+ export { users } from "./users";
@@ -0,0 +1,6 @@
1
+ import { createUsersTable } from "@percepta/auth/drizzle";
2
+
3
+ export const users = createUsersTable();
4
+
5
+ export type User = typeof users.$inferSelect;
6
+ export type NewUser = typeof users.$inferInsert;
@@ -0,0 +1 @@
1
+ export { auth, type BetterAuthSession } from "./auth";
@@ -0,0 +1,6 @@
1
+ # SCIM Placeholder
2
+
3
+ SCIM `/Users` and `/Groups` endpoints are intentionally a follow-up project.
4
+ When implemented, they should update the local `users`, `groups`, and
5
+ `group_members` projection through the access-control `groupSync` ingestion
6
+ contract so SpiceDB and Postgres stay aligned.
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": false,
5
+ "declarationMap": false,
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "noEmit": true,
9
+ "types": ["node"]
10
+ },
11
+ "include": ["drizzle.config.ts", "src/**/*.ts"]
12
+ }
@@ -5,18 +5,30 @@
5
5
  "description": "__APP_TITLE__",
6
6
  "scripts": {
7
7
  "preinstall": "npx only-allow pnpm",
8
- "dev": "pnpm -r --parallel run dev",
9
- "build": "pnpm -r run build",
10
- "clean": "pnpm -r run clean",
11
- "lint": "pnpm -r --parallel --no-bail run lint",
12
- "lint:fix": "pnpm -r --no-bail run lint:fix",
13
- "test": "pnpm -r run test"
8
+ "setup": "pnpm -r --filter './packages/*' --if-present run docker:up && pnpm run access:apply-local && pnpm run auth:db:setup-and-migrate && pnpm -r --filter './packages/*' --if-present run db:setup-and-migrate && pnpm -r --filter './packages/*' --if-present run db:seed",
9
+ "dev": "pnpm -r --parallel --if-present run dev",
10
+ "build": "pnpm -r --if-present run build",
11
+ "clean": "pnpm -r --if-present run clean",
12
+ "lint": "pnpm -r --parallel --no-bail --if-present run lint",
13
+ "lint:fix": "pnpm -r --no-bail --if-present run lint:fix",
14
+ "test": "pnpm -r --if-present run test",
15
+ "access:merge": "percepta-access-control merge --apps-dir \"$PWD/packages\" --out-dir access",
16
+ "access:validate": "percepta-access-control validate --apps-dir \"$PWD/packages\"",
17
+ "access:apply": "pnpm run access:merge && percepta-access-control apply --schema access/merged.zed --applications access/applications.generated.json",
18
+ "access:apply-local": "pnpm run access:merge && percepta-access-control apply --schema access/merged.zed --applications access/applications.generated.json --endpoint localhost:50051 --insecure --key dev-spicedb-token",
19
+ "access:seed-grants": "percepta-access-control seed-grants --fixture access/dev-grants.yaml",
20
+ "access:seed-groups": "percepta-access-control seed-groups",
21
+ "access:bootstrap-customer-admin": "percepta-access-control bootstrap-customer-admin",
22
+ "access:apply-bootstrap-grants": "percepta-access-control apply-bootstrap-grants --fixture access/bootstrap-grants.yaml",
23
+ "access:reconcile": "percepta-access-control reconcile --input access/reconcile.yaml",
24
+ "auth:db:setup-and-migrate": "pnpm --dir auth run db:setup-and-migrate"
14
25
  },
15
26
  "engines": {
16
27
  "node": ">=20",
17
28
  "pnpm": ">=9"
18
29
  },
19
30
  "devDependencies": {
31
+ "@percepta/access-control": "0.3.2",
20
32
  "@types/node": "^24.1.0",
21
33
  "eslint": "^9.18.0",
22
34
  "rimraf": "^5.0.5",
@@ -1,2 +1,3 @@
1
1
  packages:
2
2
  - "packages/*"
3
+ - "auth"