[{"data":1,"prerenderedAt":3336},["ShallowReactive",2],{"navigation_docs":3,"-contribute-writing-a-module-access-control":587,"-contribute-writing-a-module-access-control-surround":3331},[4,50,155,441,504],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":49},"Get Started","i-lucide-rocket","\u002Fget-started","1.get-started",[10,15,20,25,30,35,40,45],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fget-started\u002Fintroduction","1.get-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Installation","\u002Fget-started\u002Finstallation","1.get-started\u002F2.installation","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":24},"Project structure","\u002Fget-started\u002Fproject-structure","1.get-started\u002F3.project-structure","i-lucide-folder-tree",{"title":26,"path":27,"stem":28,"icon":29},"Configuration","\u002Fget-started\u002Fconfiguration","1.get-started\u002F4.configuration","i-lucide-settings",{"title":31,"path":32,"stem":33,"icon":34},"Authentication","\u002Fget-started\u002Fauthentication","1.get-started\u002F5.authentication","i-lucide-lock",{"title":36,"path":37,"stem":38,"icon":39},"Adding modules","\u002Fget-started\u002Fadding-modules","1.get-started\u002F6.adding-modules","i-lucide-puzzle",{"title":41,"path":42,"stem":43,"icon":44},"Database","\u002Fget-started\u002Fdatabase","1.get-started\u002F7.database","i-lucide-database",{"title":46,"path":47,"stem":48,"icon":6},"Deployment","\u002Fget-started\u002Fdeployment","1.get-started\u002F8.deployment",false,{"title":51,"icon":52,"path":53,"stem":54,"children":55,"page":49},"Guide","i-lucide-book-open","\u002Fguide","2.guide",[56,90,115,125],{"title":57,"icon":49,"path":58,"stem":59,"children":60,"page":49},"Concepts","\u002Fguide\u002Fconcepts","2.guide\u002F1.concepts",[61,66,71,76,80,85],{"title":62,"path":63,"stem":64,"icon":65},"Architecture","\u002Fguide\u002Fconcepts\u002Farchitecture","2.guide\u002F1.concepts\u002F1.architecture","i-lucide-blocks",{"title":67,"path":68,"stem":69,"icon":70},"Access control","\u002Fguide\u002Fconcepts\u002Fauth-and-scopes","2.guide\u002F1.concepts\u002F2.auth-and-scopes","i-lucide-shield-check",{"title":72,"path":73,"stem":74,"icon":75},"Extension points","\u002Fguide\u002Fconcepts\u002Fextension-points","2.guide\u002F1.concepts\u002F3.extension-points","i-lucide-plug",{"title":77,"path":78,"stem":79,"icon":44},"Database extensions","\u002Fguide\u002Fconcepts\u002Fdatabase-extensions","2.guide\u002F1.concepts\u002F4.database-extensions",{"title":81,"path":82,"stem":83,"icon":84},"Identity","\u002Fguide\u002Fconcepts\u002Fidentity","2.guide\u002F1.concepts\u002F5.identity","i-lucide-fingerprint",{"title":86,"path":87,"stem":88,"icon":89},"Glossary","\u002Fguide\u002Fconcepts\u002Fglossary","2.guide\u002F1.concepts\u002F6.glossary","i-lucide-book-a",{"title":91,"icon":49,"path":92,"stem":93,"children":94,"page":49},"UX patterns","\u002Fguide\u002Fux-patterns","2.guide\u002F2.ux-patterns",[95,100,105,110],{"title":96,"path":97,"stem":98,"icon":99},"Layouts and navigation","\u002Fguide\u002Fux-patterns\u002Flayouts-and-navigation","2.guide\u002F2.ux-patterns\u002F1.layouts-and-navigation","i-lucide-layout-template",{"title":101,"path":102,"stem":103,"icon":104},"Pages","\u002Fguide\u002Fux-patterns\u002Fpages","2.guide\u002F2.ux-patterns\u002F2.pages","i-lucide-table",{"title":106,"path":107,"stem":108,"icon":109},"Forms and actions","\u002Fguide\u002Fux-patterns\u002Fforms-and-actions","2.guide\u002F2.ux-patterns\u002F3.forms-and-actions","i-lucide-square-pen",{"title":111,"path":112,"stem":113,"icon":114},"Text and formatting","\u002Fguide\u002Fux-patterns\u002Ftext-and-formatting","2.guide\u002F2.ux-patterns\u002F4.text-and-formatting","i-lucide-type",{"title":116,"icon":49,"path":117,"stem":118,"children":119,"page":49},"Best practices","\u002Fguide\u002Fbest-practices","2.guide\u002F3.best-practices",[120],{"title":121,"path":122,"stem":123,"icon":124},"HTTP routes","\u002Fguide\u002Fbest-practices\u002Fhttp-routes","2.guide\u002F3.best-practices\u002F1.http-routes","i-lucide-route",{"title":126,"icon":49,"path":127,"stem":128,"children":129,"page":49},"Going further","\u002Fguide\u002Fgoing-further","2.guide\u002F4.going-further",[130,135,140,145,150],{"title":131,"path":132,"stem":133,"icon":134},"Use your own OAuth server","\u002Fguide\u002Fgoing-further\u002Fown-oauth-server","2.guide\u002F4.going-further\u002F1.own-oauth-server","i-lucide-key-round",{"title":136,"path":137,"stem":138,"icon":139},"Create an OAuth application","\u002Fguide\u002Fgoing-further\u002Foauth-application","2.guide\u002F4.going-further\u002F2.oauth-application","i-lucide-app-window",{"title":141,"path":142,"stem":143,"icon":144},"Signing keys","\u002Fguide\u002Fgoing-further\u002Fsigning-keys","2.guide\u002F4.going-further\u002F3.signing-keys","i-lucide-key-square",{"title":146,"path":147,"stem":148,"icon":149},"Stripe setup","\u002Fguide\u002Fgoing-further\u002Fstripe-setup","2.guide\u002F4.going-further\u002F4.stripe-setup","i-lucide-credit-card",{"title":151,"path":152,"stem":153,"icon":154},"Migrate to the permission model","\u002Fguide\u002Fgoing-further\u002Fmigrate-access-control","2.guide\u002F4.going-further\u002F5.migrate-access-control","i-lucide-arrow-right-left",{"title":156,"icon":157,"path":158,"stem":159,"children":160},"Modules","i-lucide-boxes","\u002Fmodules","3.modules",[161,163,244,273,320,375],{"title":156,"path":158,"stem":162},"3.modules\u002Findex",{"title":81,"icon":49,"path":164,"stem":165,"children":166,"page":49},"\u002Fmodules\u002Fidentity","3.modules\u002F1.identity",[167,190,194,198,220,224,240],{"title":168,"path":169,"stem":170,"children":171},"Auth","\u002Fmodules\u002Fidentity\u002Fauth","3.modules\u002F1.identity\u002F1.auth\u002Findex",[172,174,178,182,186],{"title":173,"path":169,"stem":170},"Overview",{"title":175,"path":176,"stem":177},"Server API","\u002Fmodules\u002Fidentity\u002Fauth\u002Fserver","3.modules\u002F1.identity\u002F1.auth\u002F2.server",{"title":179,"path":180,"stem":181},"Reference","\u002Fmodules\u002Fidentity\u002Fauth\u002Freference","3.modules\u002F1.identity\u002F1.auth\u002F3.reference",{"title":183,"path":184,"stem":185},"Client mode","\u002Fmodules\u002Fidentity\u002Fauth\u002Fclient-mode","3.modules\u002F1.identity\u002F1.auth\u002F4.client-mode",{"title":187,"path":188,"stem":189},"OAuth providers","\u002Fmodules\u002Fidentity\u002Fauth\u002Foauth-providers","3.modules\u002F1.identity\u002F1.auth\u002F5.oauth-providers",{"title":191,"path":192,"stem":193},"Users","\u002Fmodules\u002Fidentity\u002Fusers","3.modules\u002F1.identity\u002F2.users",{"title":195,"path":196,"stem":197},"Account","\u002Fmodules\u002Fidentity\u002Faccount","3.modules\u002F1.identity\u002F3.account",{"title":199,"path":200,"stem":201,"children":202},"Authorization","\u002Fmodules\u002Fidentity\u002Fauthorization","3.modules\u002F1.identity\u002F4.authorization\u002Findex",[203,204,208,212,216],{"title":173,"path":200,"stem":201},{"title":205,"path":206,"stem":207},"Roles and requirements","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Froles-and-requirements","3.modules\u002F1.identity\u002F4.authorization\u002F2.roles-and-requirements",{"title":209,"path":210,"stem":211},"Policies","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Fpolicies","3.modules\u002F1.identity\u002F4.authorization\u002F3.policies",{"title":213,"path":214,"stem":215},"Client","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Fclient","3.modules\u002F1.identity\u002F4.authorization\u002F4.client",{"title":217,"path":218,"stem":219},"Admin","\u002Fmodules\u002Fidentity\u002Fauthorization\u002Fadmin","3.modules\u002F1.identity\u002F4.authorization\u002F5.admin",{"title":221,"path":222,"stem":223},"Preferences","\u002Fmodules\u002Fidentity\u002Fpreferences","3.modules\u002F1.identity\u002F5.preferences",{"title":225,"path":226,"stem":227,"children":228},"OAuth Server","\u002Fmodules\u002Fidentity\u002Foauth-server","3.modules\u002F1.identity\u002F6.oauth-server\u002Findex",[229,230,233,237],{"title":173,"path":226,"stem":227},{"title":175,"path":231,"stem":232},"\u002Fmodules\u002Fidentity\u002Foauth-server\u002Fserver","3.modules\u002F1.identity\u002F6.oauth-server\u002F2.server",{"title":234,"path":235,"stem":236},"Hooks","\u002Fmodules\u002Fidentity\u002Foauth-server\u002Fhooks","3.modules\u002F1.identity\u002F6.oauth-server\u002F3.hooks",{"title":179,"path":238,"stem":239},"\u002Fmodules\u002Fidentity\u002Foauth-server\u002Froutes-and-clients","3.modules\u002F1.identity\u002F6.oauth-server\u002F4.routes-and-clients",{"title":241,"path":242,"stem":243},"OAuth Resource","\u002Fmodules\u002Fidentity\u002Foauth-resource","3.modules\u002F1.identity\u002F8.oauth-resource",{"title":245,"icon":49,"path":246,"stem":247,"children":248,"page":49},"Commerce","\u002Fmodules\u002Fcommerce","3.modules\u002F2.commerce",[249,253,257],{"title":250,"path":251,"stem":252},"Stripe","\u002Fmodules\u002Fcommerce\u002Fstripe","3.modules\u002F2.commerce\u002F1.stripe",{"title":254,"path":255,"stem":256},"Payments","\u002Fmodules\u002Fcommerce\u002Fpayments","3.modules\u002F2.commerce\u002F2.payments",{"title":258,"path":259,"stem":260,"children":261},"Subscriptions","\u002Fmodules\u002Fcommerce\u002Fsubscriptions","3.modules\u002F2.commerce\u002F3.subscriptions\u002Findex",[262,263,266,270],{"title":173,"path":259,"stem":260},{"title":175,"path":264,"stem":265},"\u002Fmodules\u002Fcommerce\u002Fsubscriptions\u002Fserver","3.modules\u002F2.commerce\u002F3.subscriptions\u002F2.server",{"title":267,"path":268,"stem":269},"App","\u002Fmodules\u002Fcommerce\u002Fsubscriptions\u002Fapp","3.modules\u002F2.commerce\u002F3.subscriptions\u002F3.app",{"title":179,"path":271,"stem":272},"\u002Fmodules\u002Fcommerce\u002Fsubscriptions\u002Freference","3.modules\u002F2.commerce\u002F3.subscriptions\u002F4.reference",{"title":274,"icon":49,"path":275,"stem":276,"children":277,"page":49},"Features","\u002Fmodules\u002Ffeatures","3.modules\u002F3.features",[278,293,312,316],{"title":279,"path":280,"stem":281,"children":282},"Events","\u002Fmodules\u002Ffeatures\u002Fevents","3.modules\u002F3.features\u002F1.events\u002Findex",[283,284,287,290],{"title":173,"path":280,"stem":281},{"title":175,"path":285,"stem":286},"\u002Fmodules\u002Ffeatures\u002Fevents\u002Fserver","3.modules\u002F3.features\u002F1.events\u002F2.server",{"title":267,"path":288,"stem":289},"\u002Fmodules\u002Ffeatures\u002Fevents\u002Fapp","3.modules\u002F3.features\u002F1.events\u002F3.app",{"title":179,"path":291,"stem":292},"\u002Fmodules\u002Ffeatures\u002Fevents\u002Freference","3.modules\u002F3.features\u002F1.events\u002F4.reference",{"title":294,"path":295,"stem":296,"children":297},"Forms","\u002Fmodules\u002Ffeatures\u002Fforms","3.modules\u002F3.features\u002F2.forms\u002Findex",[298,299,303,306,309],{"title":173,"path":295,"stem":296},{"title":300,"path":301,"stem":302},"Definition","\u002Fmodules\u002Ffeatures\u002Fforms\u002Fdefinition","3.modules\u002F3.features\u002F2.forms\u002F2.definition",{"title":175,"path":304,"stem":305},"\u002Fmodules\u002Ffeatures\u002Fforms\u002Fserver","3.modules\u002F3.features\u002F2.forms\u002F3.server",{"title":267,"path":307,"stem":308},"\u002Fmodules\u002Ffeatures\u002Fforms\u002Fapp","3.modules\u002F3.features\u002F2.forms\u002F4.app",{"title":179,"path":310,"stem":311},"\u002Fmodules\u002Ffeatures\u002Fforms\u002Freference","3.modules\u002F3.features\u002F2.forms\u002F5.reference",{"title":313,"path":314,"stem":315},"Cookie Consent","\u002Fmodules\u002Ffeatures\u002Fcookie-consent","3.modules\u002F3.features\u002F3.cookie-consent",{"title":317,"path":318,"stem":319},"MCP","\u002Fmodules\u002Ffeatures\u002Fmcp","3.modules\u002F3.features\u002F4.mcp",{"title":321,"icon":49,"path":322,"stem":323,"children":324,"page":49},"Interface","\u002Fmodules\u002Finterface","3.modules\u002F4.interface",[325,350,353,357,361],{"title":326,"path":327,"stem":328,"children":329},"UI","\u002Fmodules\u002Finterface\u002Fui","3.modules\u002F4.interface\u002F1.ui\u002Findex",[330,331,335,339,343,347],{"title":173,"path":327,"stem":328},{"title":332,"path":333,"stem":334},"Page components","\u002Fmodules\u002Finterface\u002Fui\u002Fpage-components","3.modules\u002F4.interface\u002F1.ui\u002F2.page-components",{"title":336,"path":337,"stem":338},"App shell","\u002Fmodules\u002Finterface\u002Fui\u002Fapp-shell","3.modules\u002F4.interface\u002F1.ui\u002F3.app-shell",{"title":340,"path":341,"stem":342},"Form inputs","\u002Fmodules\u002Finterface\u002Fui\u002Fform-inputs","3.modules\u002F4.interface\u002F1.ui\u002F4.form-inputs",{"title":344,"path":345,"stem":346},"Composables and helpers","\u002Fmodules\u002Finterface\u002Fui\u002Fcomposables","3.modules\u002F4.interface\u002F1.ui\u002F5.composables",{"title":179,"path":348,"stem":349},"\u002Fmodules\u002Finterface\u002Fui\u002Freference","3.modules\u002F4.interface\u002F1.ui\u002F6.reference",{"title":217,"path":351,"stem":352},"\u002Fmodules\u002Finterface\u002Fadmin","3.modules\u002F4.interface\u002F2.admin",{"title":354,"path":355,"stem":356},"Menu","\u002Fmodules\u002Finterface\u002Fmenu","3.modules\u002F4.interface\u002F3.menu",{"title":358,"path":359,"stem":360},"Widget","\u002Fmodules\u002Finterface\u002Fwidget","3.modules\u002F4.interface\u002F4.widget",{"title":362,"path":363,"stem":364,"children":365},"Mobile","\u002Fmodules\u002Finterface\u002Fmobile","3.modules\u002F4.interface\u002F5.mobile\u002Findex",[366,367,371],{"title":173,"path":363,"stem":364},{"title":368,"path":369,"stem":370},"CLI and devices","\u002Fmodules\u002Finterface\u002Fmobile\u002Fcli-and-devices","3.modules\u002F4.interface\u002F5.mobile\u002F2.cli-and-devices",{"title":372,"path":373,"stem":374},"Splash screen","\u002Fmodules\u002Finterface\u002Fmobile\u002Fsplash-screen","3.modules\u002F4.interface\u002F5.mobile\u002F3.splash-screen",{"title":376,"icon":49,"path":377,"stem":378,"children":379,"page":49},"Platform","\u002Fmodules\u002Fplatform","3.modules\u002F5.platform",[380,402,406,423],{"title":381,"path":382,"stem":383,"children":384},"Module Kit","\u002Fmodules\u002Fplatform\u002Fmodule-kit","3.modules\u002F5.platform\u002F1.module-kit\u002Findex",[385,386,390,394,398],{"title":173,"path":382,"stem":383},{"title":387,"path":388,"stem":389},"Server toolkit","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fserver-toolkit","3.modules\u002F5.platform\u002F1.module-kit\u002F2.server-toolkit",{"title":391,"path":392,"stem":393},"Registries and menus","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fregistries-and-menus","3.modules\u002F5.platform\u002F1.module-kit\u002F3.registries-and-menus",{"title":395,"path":396,"stem":397},"Translations","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fi18n","3.modules\u002F5.platform\u002F1.module-kit\u002F4.i18n",{"title":399,"path":400,"stem":401},"Database helpers","\u002Fmodules\u002Fplatform\u002Fmodule-kit\u002Fdatabase","3.modules\u002F5.platform\u002F1.module-kit\u002F5.database",{"title":403,"path":404,"stem":405},"Notify","\u002Fmodules\u002Fplatform\u002Fnotify","3.modules\u002F5.platform\u002F2.notify",{"title":407,"path":408,"stem":409,"children":410},"Email Templates","\u002Fmodules\u002Fplatform\u002Femail-templates","3.modules\u002F5.platform\u002F3.email-templates\u002Findex",[411,412,416,419],{"title":173,"path":408,"stem":409},{"title":413,"path":414,"stem":415},"Email components","\u002Fmodules\u002Fplatform\u002Femail-templates\u002Fcomponents","3.modules\u002F5.platform\u002F3.email-templates\u002F2.components",{"title":175,"path":417,"stem":418},"\u002Fmodules\u002Fplatform\u002Femail-templates\u002Fserver","3.modules\u002F5.platform\u002F3.email-templates\u002F3.server",{"title":420,"path":421,"stem":422},"Module emails","\u002Fmodules\u002Fplatform\u002Femail-templates\u002Fmodule-emails","3.modules\u002F5.platform\u002F3.email-templates\u002F4.module-emails",{"title":424,"path":425,"stem":426,"children":427},"Base Layer","\u002Fmodules\u002Fplatform\u002Fbase-layer","3.modules\u002F5.platform\u002F4.base-layer\u002Findex",[428,429,433,437],{"title":173,"path":425,"stem":426},{"title":430,"path":431,"stem":432},"Content collections","\u002Fmodules\u002Fplatform\u002Fbase-layer\u002Fcontent-collections","3.modules\u002F5.platform\u002F4.base-layer\u002F2.content-collections",{"title":434,"path":435,"stem":436},"Layouts and routing","\u002Fmodules\u002Fplatform\u002Fbase-layer\u002Flayouts-and-routing","3.modules\u002F5.platform\u002F4.base-layer\u002F3.layouts-and-routing",{"title":438,"path":439,"stem":440},"Composables and utils","\u002Fmodules\u002Fplatform\u002Fbase-layer\u002Fcomposables-and-utils","3.modules\u002F5.platform\u002F4.base-layer\u002F4.composables-and-utils",{"title":442,"icon":443,"path":444,"stem":445,"children":446,"page":49},"Examples","i-lucide-code-xml","\u002Fexamples","4.examples",[447,466,476],{"title":448,"icon":49,"path":449,"stem":450,"children":451,"page":49},"Starters","\u002Fexamples\u002Fstarters","4.examples\u002F1.starters",[452,456,461],{"title":453,"path":454,"stem":455,"icon":6},"Workspace starter","\u002Fexamples\u002Fstarters\u002Fworkspace","4.examples\u002F1.starters\u002F1.workspace",{"title":457,"path":458,"stem":459,"icon":460},"Mobile starter","\u002Fexamples\u002Fstarters\u002Fmobile","4.examples\u002F1.starters\u002F2.mobile","i-lucide-smartphone",{"title":462,"path":463,"stem":464,"icon":465},"Standalone mobile starter","\u002Fexamples\u002Fstarters\u002Fmobile-standalone","4.examples\u002F1.starters\u002F3.mobile-standalone","i-lucide-tablet-smartphone",{"title":467,"icon":49,"path":468,"stem":469,"children":470,"page":49},"Apps","\u002Fexamples\u002Fapps","4.examples\u002F2.apps",[471],{"title":472,"path":473,"stem":474,"icon":475},"Frigear website app","\u002Fexamples\u002Fapps\u002Ffrigear-nu-website","4.examples\u002F2.apps\u002F1.frigear-nu-website","i-lucide-layout-dashboard",{"title":477,"icon":49,"path":478,"stem":479,"children":480,"page":49},"Tasks","\u002Fexamples\u002Ftasks","4.examples\u002F3.tasks",[481,485,489,494,499],{"title":482,"path":483,"stem":484,"icon":70},"Protect a route","\u002Fexamples\u002Ftasks\u002Fprotect-a-route","4.examples\u002F3.tasks\u002F1.protect-a-route",{"title":486,"path":487,"stem":488,"icon":475},"Add an admin page","\u002Fexamples\u002Ftasks\u002Fadd-an-admin-page","4.examples\u002F3.tasks\u002F2.add-an-admin-page",{"title":490,"path":491,"stem":492,"icon":493},"Sell event tickets","\u002Fexamples\u002Ftasks\u002Fsell-event-tickets","4.examples\u002F3.tasks\u002F3.sell-event-tickets","i-lucide-ticket",{"title":495,"path":496,"stem":497,"icon":498},"Build a form","\u002Fexamples\u002Ftasks\u002Fbuild-a-form","4.examples\u002F3.tasks\u002F4.build-a-form","i-lucide-clipboard-list",{"title":500,"path":501,"stem":502,"icon":503},"Send an email","\u002Fexamples\u002Ftasks\u002Fsend-an-email","4.examples\u002F3.tasks\u002F5.send-an-email","i-lucide-mail",{"title":505,"icon":506,"path":507,"stem":508,"children":509,"page":49},"Contribute","i-lucide-git-pull-request","\u002Fcontribute","5.contribute",[510,513,518,544,549,552,556,571],{"title":173,"path":511,"stem":512,"icon":506},"\u002Fcontribute\u002Foverview","5.contribute\u002F1.overview",{"title":514,"path":515,"stem":516,"icon":517},"Local development","\u002Fcontribute\u002Flocal-development","5.contribute\u002F2.local-development","i-lucide-terminal",{"title":519,"icon":49,"path":520,"stem":521,"children":522,"page":49},"Writing a module","\u002Fcontribute\u002Fwriting-a-module","5.contribute\u002F3.writing-a-module",[523,527,532,536,541],{"title":524,"path":525,"stem":526,"icon":39},"Scaffold a module","\u002Fcontribute\u002Fwriting-a-module\u002Fscaffold","5.contribute\u002F3.writing-a-module\u002F1.scaffold",{"title":528,"path":529,"stem":530,"icon":531},"Setup checklist","\u002Fcontribute\u002Fwriting-a-module\u002Fsetup-checklist","5.contribute\u002F3.writing-a-module\u002F2.setup-checklist","i-lucide-list-checks",{"title":533,"path":534,"stem":535,"icon":44},"Database tables","\u002Fcontribute\u002Fwriting-a-module\u002Fdatabase-tables","5.contribute\u002F3.writing-a-module\u002F3.database-tables",{"title":537,"path":538,"stem":539,"icon":540},"Testing","\u002Fcontribute\u002Fwriting-a-module\u002Ftesting","5.contribute\u002F3.writing-a-module\u002F4.testing","i-lucide-flask-conical",{"title":67,"path":542,"stem":543,"icon":70},"\u002Fcontribute\u002Fwriting-a-module\u002Faccess-control","5.contribute\u002F3.writing-a-module\u002F5.access-control",{"title":545,"path":546,"stem":547,"icon":548},"Publishing","\u002Fcontribute\u002Fpublishing","5.contribute\u002F4.publishing","i-lucide-package-check",{"title":448,"path":550,"stem":551,"icon":6},"\u002Fcontribute\u002Fstarters","5.contribute\u002F5.starters",{"title":553,"path":554,"stem":555,"icon":52},"Documentation","\u002Fcontribute\u002Fdocumentation","5.contribute\u002F6.documentation",{"title":557,"icon":49,"path":558,"stem":559,"children":560,"page":49},"Tooling","\u002Fcontribute\u002Ftooling","5.contribute\u002F7.tooling",[561,566],{"title":562,"path":563,"stem":564,"icon":565},"CLI","\u002Fcontribute\u002Ftooling\u002Fcli","5.contribute\u002F7.tooling\u002F1.cli","i-lucide-square-terminal",{"title":567,"path":568,"stem":569,"icon":570},"ESLint config","\u002Fcontribute\u002Ftooling\u002Feslint-config","5.contribute\u002F7.tooling\u002F2.eslint-config","i-lucide-spell-check",{"title":572,"icon":49,"path":573,"stem":574,"children":575},"Decisions","\u002Fcontribute\u002Fdecisions","5.contribute\u002F8.decisions\u002Findex",[576,578,583],{"title":572,"path":573,"stem":574,"icon":577},"i-lucide-scale",{"title":579,"path":580,"stem":581,"icon":582},"Session lifetimes per OAuth client","\u002Fcontribute\u002Fdecisions\u002Foauth-token-lifetimes","5.contribute\u002F8.decisions\u002F1.oauth-token-lifetimes","i-lucide-timer",{"title":584,"path":585,"stem":586,"icon":70},"One permission model","\u002Fcontribute\u002Fdecisions\u002Fpermission-model","5.contribute\u002F8.decisions\u002F2.permission-model",{"id":588,"title":67,"body":589,"description":3324,"extension":3325,"links":3326,"meta":3327,"navigation":3328,"path":542,"seo":3329,"stem":543,"__hash__":3330},"docs\u002F5.contribute\u002F3.writing-a-module\u002F5.access-control.md",{"type":590,"value":591,"toc":3310},"minimark",[592,613,652,657,1176,1284,1384,1390,1461,1466,1573,1584,1588,1637,1641,1738,1741,1767,1773,1776,1787,1837,1841,1848,1930,2033,2039,2103,2181,2202,2206,2209,2388,2408,2415,2419,2434,2506,2644,2648,2657,2934,3191,3197,3285,3289,3306],[593,594,595,596,600,601,604,605,608,609,612],"p",{},"This page is for module authors. A module says what can be done (permissions), who holds it by default (",[597,598,599],"code",{},"grantTo",",\nroles), what a token may be given (scopes), and how a record narrows it (policies). The host app decides the rest in\n",[597,602,603],{},"authorization.roles",". The model is in ",[606,607,67],"a",{"href":68},"; the reasons are in the\n",[606,610,611],{"href":585},"decision record",".",[593,614,615,616,619,620,623,624,627,628,631,632,635,636,639,640,643,644,647,648,651],{},"A module declares all of it in one typed file, ",[597,617,618],{},"src\u002Fruntime\u002Faccess.ts",", with ",[597,621,622],{},"defineAccess"," from\n",[597,625,626],{},"@frigear-nu\u002Fauth\u002Faccess",", and registers it in ",[597,629,630],{},"setup()"," with ",[597,633,634],{},"registerAccess"," from ",[597,637,638],{},"@frigear-nu\u002Fauth\u002Futils",". List\n",[597,641,642],{},"@frigear-nu\u002Fauth"," in ",[597,645,646],{},"moduleDependencies"," and in ",[597,649,650],{},"package.json",". All registrations of all modules and the host merge\nonce every module has run, and any error fails the build.",[653,654,656],"h2",{"id":655},"declare-access","Declare access",[658,659,664],"pre",{"className":660,"code":661,"filename":618,"language":662,"meta":663,"style":663},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { defineAccess } from '@frigear-nu\u002Fauth\u002Faccess'\n\nexport const eventsAccess = defineAccess('events', {\n  permissions: {\n    \u002F\u002F Quote every key once one has a colon (eslint's `quote-props`).\n    'manage': { label: 'Manage events', grantTo: ['admin', 'eventmanager'] }, \u002F\u002F events:manage\n    'pii': { label: 'See attendee details', sensitive: true, grantTo: ['admin'] }, \u002F\u002F events:pii\n    'refund': { label: 'Refund tickets', sessionOnly: true, grantTo: ['admin'] }, \u002F\u002F events:refund\n    'tickets:read': { label: 'Read your tickets', implicit: true }, \u002F\u002F a full name: another resource\n    'tickets:write': { label: 'Buy tickets', implicit: true },\n  },\n  scopes: {\n    tickets: { label: 'Your tickets', icon: 'i-lucide-ticket', permissions: ['tickets:read', 'tickets:write'] },\n  },\n  roles: {\n    eventmanager: { label: 'Event manager', grant: ['events:manage', 'events:refund', 'events:pii'] },\n  },\n})\n","ts","",[597,665,666,698,705,741,753,760,822,879,934,974,1010,1016,1026,1088,1093,1103,1162,1167],{"__ignoreMap":663},[667,668,671,675,679,683,686,689,692,695],"span",{"class":669,"line":670},"line",1,[667,672,674],{"class":673},"s7zQu","import",[667,676,678],{"class":677},"sMK4o"," {",[667,680,682],{"class":681},"sTEyZ"," defineAccess",[667,684,685],{"class":677}," }",[667,687,688],{"class":673}," from",[667,690,691],{"class":677}," '",[667,693,626],{"class":694},"sfazB",[667,696,697],{"class":677},"'\n",[667,699,701],{"class":669,"line":700},2,[667,702,704],{"emptyLinePlaceholder":703},true,"\n",[667,706,708,711,715,718,721,724,727,730,733,735,738],{"class":669,"line":707},3,[667,709,710],{"class":673},"export",[667,712,714],{"class":713},"spNyl"," const",[667,716,717],{"class":681}," eventsAccess ",[667,719,720],{"class":677},"=",[667,722,682],{"class":723},"s2Zo4",[667,725,726],{"class":681},"(",[667,728,729],{"class":677},"'",[667,731,732],{"class":694},"events",[667,734,729],{"class":677},[667,736,737],{"class":677},",",[667,739,740],{"class":677}," {\n",[667,742,744,748,751],{"class":669,"line":743},4,[667,745,747],{"class":746},"swJcz","  permissions",[667,749,750],{"class":677},":",[667,752,740],{"class":677},[667,754,756],{"class":669,"line":755},5,[667,757,759],{"class":758},"sHwdD","    \u002F\u002F Quote every key once one has a colon (eslint's `quote-props`).\n",[667,761,763,766,769,771,773,775,778,780,782,785,787,789,792,794,797,799,802,804,806,808,811,813,816,819],{"class":669,"line":762},6,[667,764,765],{"class":677},"    '",[667,767,768],{"class":746},"manage",[667,770,729],{"class":677},[667,772,750],{"class":677},[667,774,678],{"class":677},[667,776,777],{"class":746}," label",[667,779,750],{"class":677},[667,781,691],{"class":677},[667,783,784],{"class":694},"Manage events",[667,786,729],{"class":677},[667,788,737],{"class":677},[667,790,791],{"class":746}," grantTo",[667,793,750],{"class":677},[667,795,796],{"class":681}," [",[667,798,729],{"class":677},[667,800,801],{"class":694},"admin",[667,803,729],{"class":677},[667,805,737],{"class":677},[667,807,691],{"class":677},[667,809,810],{"class":694},"eventmanager",[667,812,729],{"class":677},[667,814,815],{"class":681},"] ",[667,817,818],{"class":677},"},",[667,820,821],{"class":758}," \u002F\u002F events:manage\n",[667,823,825,827,830,832,834,836,838,840,842,845,847,849,852,854,858,860,862,864,866,868,870,872,874,876],{"class":669,"line":824},7,[667,826,765],{"class":677},[667,828,829],{"class":746},"pii",[667,831,729],{"class":677},[667,833,750],{"class":677},[667,835,678],{"class":677},[667,837,777],{"class":746},[667,839,750],{"class":677},[667,841,691],{"class":677},[667,843,844],{"class":694},"See attendee details",[667,846,729],{"class":677},[667,848,737],{"class":677},[667,850,851],{"class":746}," sensitive",[667,853,750],{"class":677},[667,855,857],{"class":856},"sfNiH"," true",[667,859,737],{"class":677},[667,861,791],{"class":746},[667,863,750],{"class":677},[667,865,796],{"class":681},[667,867,729],{"class":677},[667,869,801],{"class":694},[667,871,729],{"class":677},[667,873,815],{"class":681},[667,875,818],{"class":677},[667,877,878],{"class":758}," \u002F\u002F events:pii\n",[667,880,882,884,887,889,891,893,895,897,899,902,904,906,909,911,913,915,917,919,921,923,925,927,929,931],{"class":669,"line":881},8,[667,883,765],{"class":677},[667,885,886],{"class":746},"refund",[667,888,729],{"class":677},[667,890,750],{"class":677},[667,892,678],{"class":677},[667,894,777],{"class":746},[667,896,750],{"class":677},[667,898,691],{"class":677},[667,900,901],{"class":694},"Refund tickets",[667,903,729],{"class":677},[667,905,737],{"class":677},[667,907,908],{"class":746}," sessionOnly",[667,910,750],{"class":677},[667,912,857],{"class":856},[667,914,737],{"class":677},[667,916,791],{"class":746},[667,918,750],{"class":677},[667,920,796],{"class":681},[667,922,729],{"class":677},[667,924,801],{"class":694},[667,926,729],{"class":677},[667,928,815],{"class":681},[667,930,818],{"class":677},[667,932,933],{"class":758}," \u002F\u002F events:refund\n",[667,935,937,939,942,944,946,948,950,952,954,957,959,961,964,966,968,971],{"class":669,"line":936},9,[667,938,765],{"class":677},[667,940,941],{"class":746},"tickets:read",[667,943,729],{"class":677},[667,945,750],{"class":677},[667,947,678],{"class":677},[667,949,777],{"class":746},[667,951,750],{"class":677},[667,953,691],{"class":677},[667,955,956],{"class":694},"Read your tickets",[667,958,729],{"class":677},[667,960,737],{"class":677},[667,962,963],{"class":746}," implicit",[667,965,750],{"class":677},[667,967,857],{"class":856},[667,969,970],{"class":677}," },",[667,972,973],{"class":758}," \u002F\u002F a full name: another resource\n",[667,975,977,979,982,984,986,988,990,992,994,997,999,1001,1003,1005,1007],{"class":669,"line":976},10,[667,978,765],{"class":677},[667,980,981],{"class":746},"tickets:write",[667,983,729],{"class":677},[667,985,750],{"class":677},[667,987,678],{"class":677},[667,989,777],{"class":746},[667,991,750],{"class":677},[667,993,691],{"class":677},[667,995,996],{"class":694},"Buy tickets",[667,998,729],{"class":677},[667,1000,737],{"class":677},[667,1002,963],{"class":746},[667,1004,750],{"class":677},[667,1006,857],{"class":856},[667,1008,1009],{"class":677}," },\n",[667,1011,1013],{"class":669,"line":1012},11,[667,1014,1015],{"class":677},"  },\n",[667,1017,1019,1022,1024],{"class":669,"line":1018},12,[667,1020,1021],{"class":746},"  scopes",[667,1023,750],{"class":677},[667,1025,740],{"class":677},[667,1027,1029,1032,1034,1036,1038,1040,1042,1045,1047,1049,1052,1054,1056,1058,1060,1062,1065,1067,1069,1071,1073,1075,1077,1079,1081,1083,1085],{"class":669,"line":1028},13,[667,1030,1031],{"class":746},"    tickets",[667,1033,750],{"class":677},[667,1035,678],{"class":677},[667,1037,777],{"class":746},[667,1039,750],{"class":677},[667,1041,691],{"class":677},[667,1043,1044],{"class":694},"Your tickets",[667,1046,729],{"class":677},[667,1048,737],{"class":677},[667,1050,1051],{"class":746}," icon",[667,1053,750],{"class":677},[667,1055,691],{"class":677},[667,1057,493],{"class":694},[667,1059,729],{"class":677},[667,1061,737],{"class":677},[667,1063,1064],{"class":746}," permissions",[667,1066,750],{"class":677},[667,1068,796],{"class":681},[667,1070,729],{"class":677},[667,1072,941],{"class":694},[667,1074,729],{"class":677},[667,1076,737],{"class":677},[667,1078,691],{"class":677},[667,1080,981],{"class":694},[667,1082,729],{"class":677},[667,1084,815],{"class":681},[667,1086,1087],{"class":677},"},\n",[667,1089,1091],{"class":669,"line":1090},14,[667,1092,1015],{"class":677},[667,1094,1096,1099,1101],{"class":669,"line":1095},15,[667,1097,1098],{"class":746},"  roles",[667,1100,750],{"class":677},[667,1102,740],{"class":677},[667,1104,1106,1109,1111,1113,1115,1117,1119,1122,1124,1126,1129,1131,1133,1135,1138,1140,1142,1144,1147,1149,1151,1153,1156,1158,1160],{"class":669,"line":1105},16,[667,1107,1108],{"class":746},"    eventmanager",[667,1110,750],{"class":677},[667,1112,678],{"class":677},[667,1114,777],{"class":746},[667,1116,750],{"class":677},[667,1118,691],{"class":677},[667,1120,1121],{"class":694},"Event manager",[667,1123,729],{"class":677},[667,1125,737],{"class":677},[667,1127,1128],{"class":746}," grant",[667,1130,750],{"class":677},[667,1132,796],{"class":681},[667,1134,729],{"class":677},[667,1136,1137],{"class":694},"events:manage",[667,1139,729],{"class":677},[667,1141,737],{"class":677},[667,1143,691],{"class":677},[667,1145,1146],{"class":694},"events:refund",[667,1148,729],{"class":677},[667,1150,737],{"class":677},[667,1152,691],{"class":677},[667,1154,1155],{"class":694},"events:pii",[667,1157,729],{"class":677},[667,1159,815],{"class":681},[667,1161,1087],{"class":677},[667,1163,1165],{"class":669,"line":1164},17,[667,1166,1015],{"class":677},[667,1168,1170,1173],{"class":669,"line":1169},18,[667,1171,1172],{"class":677},"}",[667,1174,1175],{"class":681},")\n",[658,1177,1180],{"className":660,"code":1178,"filename":1179,"language":662,"meta":663,"style":663},"import { registerAccess } from '@frigear-nu\u002Fauth\u002Futils'\nimport { eventsAccess } from '.\u002Fruntime\u002Faccess'\n\n\u002F\u002F in setup(): the opt-in role only with `settings.roles`\nregisterAccess(nuxt, eventsAccess, { roles: !!options.settings?.roles, from: '@frigear-nu\u002Fevents' })\n","src\u002Fmodule.ts",[597,1181,1182,1201,1221,1225,1230],{"__ignoreMap":663},[667,1183,1184,1186,1188,1191,1193,1195,1197,1199],{"class":669,"line":670},[667,1185,674],{"class":673},[667,1187,678],{"class":677},[667,1189,1190],{"class":681}," registerAccess",[667,1192,685],{"class":677},[667,1194,688],{"class":673},[667,1196,691],{"class":677},[667,1198,638],{"class":694},[667,1200,697],{"class":677},[667,1202,1203,1205,1207,1210,1212,1214,1216,1219],{"class":669,"line":700},[667,1204,674],{"class":673},[667,1206,678],{"class":677},[667,1208,1209],{"class":681}," eventsAccess",[667,1211,685],{"class":677},[667,1213,688],{"class":673},[667,1215,691],{"class":677},[667,1217,1218],{"class":694},".\u002Fruntime\u002Faccess",[667,1220,697],{"class":677},[667,1222,1223],{"class":669,"line":707},[667,1224,704],{"emptyLinePlaceholder":703},[667,1226,1227],{"class":669,"line":743},[667,1228,1229],{"class":758},"\u002F\u002F in setup(): the opt-in role only with `settings.roles`\n",[667,1231,1232,1234,1237,1239,1241,1243,1245,1248,1250,1253,1256,1258,1261,1264,1267,1269,1271,1273,1275,1278,1280,1282],{"class":669,"line":755},[667,1233,634],{"class":723},[667,1235,1236],{"class":681},"(nuxt",[667,1238,737],{"class":677},[667,1240,1209],{"class":681},[667,1242,737],{"class":677},[667,1244,678],{"class":677},[667,1246,1247],{"class":746}," roles",[667,1249,750],{"class":677},[667,1251,1252],{"class":677}," !!",[667,1254,1255],{"class":681},"options",[667,1257,612],{"class":677},[667,1259,1260],{"class":681},"settings",[667,1262,1263],{"class":677},"?.",[667,1265,1266],{"class":681},"roles",[667,1268,737],{"class":677},[667,1270,688],{"class":746},[667,1272,750],{"class":677},[667,1274,691],{"class":677},[667,1276,1277],{"class":694},"@frigear-nu\u002Fevents",[667,1279,729],{"class":677},[667,1281,685],{"class":677},[667,1283,1175],{"class":681},[1285,1286,1287,1305,1326,1368],"ul",{},[1288,1289,1290,1294,1295,1297,1298,1300,1301,1304],"li",{},[1291,1292,1293],"strong",{},"Names come from the keys."," ",[597,1296,768],{}," becomes ",[597,1299,1137],{},". A key with a colon is a full name, for a resource the\nmodule owns besides its own (events owns ",[597,1302,1303],{},"tickets:*",").",[1288,1306,1307,1294,1310,1313,1314,1317,1318,1321,1322,1325],{},[1291,1308,1309],{},"The names are typed.",[597,1311,1312],{},"eventsAccess.permissions.manage"," has the type ",[597,1315,1316],{},"'events:manage'",": use it in routes, MCP\ntools, menus and tests instead of a string, so a rename is one edit. ",[597,1319,1320],{},"eventsAccess.scopes.tickets"," and\n",[597,1323,1324],{},"eventsAccess.roles.eventmanager"," work the same way.",[1288,1327,1328,1331,1332,1335,1336,1339,1340,1321,1343,1346,1347,1346,1350,1353,1354,1356,1357,1360,1361,1364,1365,612],{},[1291,1329,1330],{},"i18n keys are derived"," unless you pass ",[597,1333,1334],{},"labelKey"," \u002F ",[597,1337,1338],{},"descriptionKey",": ",[597,1341,1342],{},"\u003Cresource>.permissions.\u003Ckey>",[597,1344,1345],{},"\u003Cresource>.permissions.\u003Ckey>Description",", ",[597,1348,1349],{},"\u003Cresource>.scopes.\u003Ckey>",[597,1351,1352],{},"\u003Cresource>.roles.\u003Ckey>",". A full-name key is\ncamel-cased (",[597,1355,941],{}," → ",[597,1358,1359],{},"events.permissions.ticketsRead","). Pass ",[597,1362,1363],{},"i18n: 'oauthServer'"," when the module's\ntranslations live under another namespace than the resource. A key that is missing from the locale files shows the\n",[597,1366,1367],{},"label",[1288,1369,1370,1376,1377,1380,1381,1383],{},[1291,1371,1372,1373],{},"The file lives in ",[597,1374,1375],{},"src\u002Fruntime\u002F",", so server routes, app code and tests can import it as well as ",[597,1378,1379],{},"module.ts",".\n",[597,1382,626],{}," has no Nuxt, h3 or Vue imports, so it is safe everywhere.",[593,1385,1386,1389],{},[597,1387,1388],{},"registerAccess(nuxt, access, options?)"," registers everything unless narrowed:",[1391,1392,1393,1406],"table",{},[1394,1395,1396],"thead",{},[1397,1398,1399,1403],"tr",{},[1400,1401,1402],"th",{},"Option",[1400,1404,1405],{},"Meaning",[1407,1408,1409,1424,1437,1451],"tbody",{},[1397,1410,1411,1417],{},[1412,1413,1414],"td",{},[597,1415,1416],{},"permissions",[1412,1418,1419,1420,1423],{},"Only these permission keys (",[597,1421,1422],{},"['profile:read', 'profile:write']"," when the admin API is off).",[1397,1425,1426,1431],{},[1412,1427,1428],{},[597,1429,1430],{},"scopes",[1412,1432,1433,1436],{},[597,1434,1435],{},"false"," for none, or a list of scope keys.",[1397,1438,1439,1443],{},[1412,1440,1441],{},[597,1442,1266],{},[1412,1444,1445,1447,1448,1450],{},[597,1446,1435],{}," for none, or a list of role keys. A ",[597,1449,599],{}," naming one of the access's own roles that is left out is dropped with it, so an opt-in role needs no second permission list.",[1397,1452,1453,1458],{},[1412,1454,1455],{},[597,1456,1457],{},"from",[1412,1459,1460],{},"The roles' provenance on the roles admin pages. Defaults to the resource.",[1462,1463,1465],"h3",{"id":1464},"permission-fields","Permission fields",[1391,1467,1468,1477],{},[1394,1469,1470],{},[1397,1471,1472,1475],{},[1400,1473,1474],{},"Field",[1400,1476,1405],{},[1407,1478,1479,1501,1522,1532,1545,1561],{},[1397,1480,1481,1495],{},[1412,1482,1483,1346,1485,1346,1487,1346,1490,1346,1492],{},[597,1484,1367],{},[597,1486,1334],{},[597,1488,1489],{},"description",[597,1491,1338],{},[597,1493,1494],{},"group",[1412,1496,1497,1498,1500],{},"What the roles admin and the token form show. ",[597,1499,1494],{}," defaults to the resource of the name.",[1397,1502,1503,1508],{},[1412,1504,1505],{},[597,1506,1507],{},"sensitive",[1412,1509,1510,1511,1514,1515,1518,1519,612],{},"Never covered by ",[597,1512,1513],{},"*"," or ",[597,1516,1517],{},"resource:*",": held only when granted by name. On by default for ",[597,1520,1521],{},":pii",[1397,1523,1524,1529],{},[1412,1525,1526],{},[597,1527,1528],{},"sessionOnly",[1412,1530,1531],{},"Only a cookie session may use it. Never held by a token, never offered as a scope.",[1397,1533,1534,1539],{},[1412,1535,1536],{},[597,1537,1538],{},"implicit",[1412,1540,1541,1542,612],{},"Held by every signed-in user, also without ",[597,1543,1544],{},"@frigear-nu\u002Fauthorization",[1397,1546,1547,1552],{},[1412,1548,1549],{},[597,1550,1551],{},"aliases",[1412,1553,1554,1555,1557,1558,1560],{},"Old names that, when held, also hold this permission. Expanded on the held side only: a token with ",[597,1556,1137],{}," keeps working after you split it, while code that checks ",[597,1559,1137],{}," fails.",[1397,1562,1563,1567],{},[1412,1564,1565],{},[597,1566,599],{},[1412,1568,1569,1570,612],{},"Roles that get it by default, by name. Roles that inherit them get it too. A role that is not configured is skipped, with a build note. This replaces ",[597,1571,1572],{},"aggregateTo",[593,1574,1575,1576,1346,1578,1580,1581,1583],{},"The same name registered by two sources must agree on ",[597,1577,1507],{},[597,1579,1528],{}," and ",[597,1582,1538],{},", or the build fails.",[1462,1585,1587],{"id":1586},"typed-everywhere-else","Typed everywhere else",[593,1589,1590,1591,1594,1595,1598,1599,1601,1602,1605,1606,1346,1609,1346,1612,1615,1616,1346,1619,1346,1622,1625,1626,1629,1630,1632,1633,1636],{},"Once a module registered its access, auth generates ",[597,1592,1593],{},".nuxt\u002Ftypes\u002Fauth-access.d.ts"," with every registered permission and\nrole. The ",[597,1596,1597],{},"Permission"," type (",[597,1600,626],{},") becomes their union in the app, the server and\n",[597,1603,1604],{},"nuxt.config.ts",", so a misspelled permission fails typecheck in ",[597,1607,1608],{},"authorize",[597,1610,1611],{},"allows",[597,1613,1614],{},"definePolicy",",\n",[597,1617,1618],{},"defineSharedPolicy",[597,1620,1621],{},"useAuthorization().can",[597,1623,1624],{},"\u003CCan permission>",", route rules, ",[597,1627,1628],{},"definePageMeta({ authorize })",", MCP\ndefinitions and menu, widget and tab items. In a module package's own build nothing is generated, so ",[597,1631,1597],{}," is\n",[597,1634,1635],{},"string"," there and the package still builds.",[1462,1638,1640],{"id":1639},"naming-rules","Naming rules",[1285,1642,1643,1675,1691,1711,1728],{},[1288,1644,1645,1650,1651,1346,1654,1657,1658,1346,1661,1346,1664,1346,1667,1346,1670,1346,1673,612],{},[1291,1646,1647],{},[597,1648,1649],{},"resource:action",", resource first, one colon: ",[597,1652,1653],{},"events:update",[597,1655,1656],{},"member-area:view",". Reuse the actions\n",[597,1659,1660],{},"read",[597,1662,1663],{},"list",[597,1665,1666],{},"create",[597,1668,1669],{},"update",[597,1671,1672],{},"delete",[597,1674,768],{},[1288,1676,1677,1682,1683,1615,1685,1688,1689,612],{},[1291,1678,1679,1680,612],{},"Sensitive data ends in ",[597,1681,1521],{}," A permission that exposes other people's names, emails or answers (",[597,1684,1155],{},[597,1686,1687],{},"forms:pii",") is its own permission, never a side effect of ",[597,1690,768],{},[1288,1692,1693,1698,1699,1615,1701,1346,1704,1707,1708,1710],{},[1291,1694,1695,1696],{},"Money and credentials are ",[597,1697,1528],{},", each with a permission of its own (",[597,1700,1146],{},[597,1702,1703],{},"forms:credentials",[597,1705,1706],{},"users:manage","). The permission carries the rule, so a route cannot forget it. When you split one\nout of an existing permission, roles that name the old one need the new one: say so in the changelog. ",[597,1709,1517],{},"\ncovers it.",[1288,1712,1713,1718,1719,1615,1721,1346,1724,1727],{},[1291,1714,1715,1716,612],{},"Self-service is ",[597,1717,1538],{}," What every signed-in user does with their own data (",[597,1720,941],{},[597,1722,1723],{},"profile:write",[597,1725,1726],{},"payments:read",") needs no role. Check the record with a policy, so users reach only their own.",[1288,1729,1730,1733,1734,1737],{},[1291,1731,1732],{},"Staff permissions are not implicit."," Put them in ",[597,1735,1736],{},"grantTo: ['admin']",". Add a role of your own only when staff\nof your module differ from the host's admins.",[653,1739,1740],{"id":1430},"Scopes",[593,1742,1743,1744,1321,1746,1748,1749,1752,1753,1755,1756,1758,1759,1762,1763,1580,1765,612],{},"A scope is what a token or an OAuth client may be given. A permission is a scope by itself, as are ",[597,1745,1517],{},[597,1747,1513],{}," (for clients allowed wildcards). A ",[1291,1750,1751],{},"named scope"," (the ",[597,1754,1430],{}," of ",[597,1757,622],{},") groups permissions under one\nconsent line: ",[597,1760,1761],{},"tickets"," expands to ",[597,1764,941],{},[597,1766,981],{},[593,1768,1769,1770,1772],{},"A named scope may expand only to permissions that a token can hold: no sensitive and no ",[597,1771,1528],{}," permission, and\nits name cannot be a registered permission. Scopes work for API tokens and OAuth alike, so a module does not depend on\nOAuth Server to offer one.",[653,1774,1775],{"id":1266},"Roles",[593,1777,1778,1779,1755,1781,1783,1784,612],{},"A module may ship a role (the ",[597,1780,1266],{},[597,1782,622],{},") when its staff are not the host's admins. Make it opt-in when\nmost hosts will not want it, and leave it out with ",[597,1785,1786],{},"registerAccess(nuxt, access, { roles: false })",[1285,1788,1789,1803,1828],{},[1288,1790,1791,1792,1580,1795,1798,1799,1802],{},"Two modules defining one role merge ",[597,1793,1794],{},"grant",[597,1796,1797],{},"inherit"," as unions. Conflicting ",[597,1800,1801],{},"requirements"," fail the build.",[1288,1804,1805,1806,1809,1810,1321,1812,1814,1815,1580,1817,1820,1821,1824,1825,1827],{},"The host's ",[597,1807,1808],{},"authorization.roles.\u003Cname>"," merges into a module role: its label and description win, ",[597,1811,1794],{},[597,1813,1797],{}," are unions, its ",[597,1816,1801],{},[597,1818,1819],{},"auto"," win. With ",[597,1822,1823],{},"replace: true"," it drops the module's definition and\nevery ",[597,1826,599],{}," grant into it.",[1288,1829,1830,1831,1834,1835,612],{},"Grant by name what a wildcard does not cover: a role with ",[597,1832,1833],{},"grant: ['events:*']"," lacks ",[597,1836,1155],{},[653,1838,1840],{"id":1839},"register-requirements-and-policies","Register requirements and policies",[593,1842,1843,1844,1847],{},"A ",[1291,1845,1846],{},"requirement"," lets a role count only while something holds, such as an active subscription. The file's default\nexport decides, and a throw means not met:",[658,1849,1851],{"className":660,"code":1850,"filename":1179,"language":662,"meta":663,"style":663},"import { addRoleRequirement } from '@frigear-nu\u002Fauth\u002Futils'\n\naddRoleRequirement(nuxt, { name: 'subscription', path: resolver.resolve('.\u002Fruntime\u002Fserver\u002Frequirements\u002Fsubscription') })\n",[597,1852,1853,1872,1876],{"__ignoreMap":663},[667,1854,1855,1857,1859,1862,1864,1866,1868,1870],{"class":669,"line":670},[667,1856,674],{"class":673},[667,1858,678],{"class":677},[667,1860,1861],{"class":681}," addRoleRequirement",[667,1863,685],{"class":677},[667,1865,688],{"class":673},[667,1867,691],{"class":677},[667,1869,638],{"class":694},[667,1871,697],{"class":677},[667,1873,1874],{"class":669,"line":700},[667,1875,704],{"emptyLinePlaceholder":703},[667,1877,1878,1881,1883,1885,1887,1890,1892,1894,1897,1899,1901,1904,1906,1909,1911,1914,1916,1918,1921,1923,1926,1928],{"class":669,"line":707},[667,1879,1880],{"class":723},"addRoleRequirement",[667,1882,1236],{"class":681},[667,1884,737],{"class":677},[667,1886,678],{"class":677},[667,1888,1889],{"class":746}," name",[667,1891,750],{"class":677},[667,1893,691],{"class":677},[667,1895,1896],{"class":694},"subscription",[667,1898,729],{"class":677},[667,1900,737],{"class":677},[667,1902,1903],{"class":746}," path",[667,1905,750],{"class":677},[667,1907,1908],{"class":681}," resolver",[667,1910,612],{"class":677},[667,1912,1913],{"class":723},"resolve",[667,1915,726],{"class":681},[667,1917,729],{"class":677},[667,1919,1920],{"class":694},".\u002Fruntime\u002Fserver\u002Frequirements\u002Fsubscription",[667,1922,729],{"class":677},[667,1924,1925],{"class":681},") ",[667,1927,1172],{"class":677},[667,1929,1175],{"class":681},[658,1931,1934],{"className":660,"code":1932,"filename":1933,"language":662,"meta":663,"style":663},"export default async ({ user, value }: { user: { id: number | string }, value: unknown }) => {\n  return hasActiveSubscription(user.id, value) \u002F\u002F value: what the role set, `'*'` or plan keys\n}\n","src\u002Fruntime\u002Fserver\u002Frequirements\u002Fsubscription.ts",[597,1935,1936,2001,2028],{"__ignoreMap":663},[667,1937,1938,1940,1943,1946,1949,1953,1955,1958,1961,1963,1965,1967,1969,1972,1974,1978,1981,1984,1986,1988,1990,1993,1996,1999],{"class":669,"line":670},[667,1939,710],{"class":673},[667,1941,1942],{"class":673}," default",[667,1944,1945],{"class":713}," async",[667,1947,1948],{"class":677}," ({",[667,1950,1952],{"class":1951},"sHdIc"," user",[667,1954,737],{"class":677},[667,1956,1957],{"class":1951}," value",[667,1959,1960],{"class":677}," }:",[667,1962,678],{"class":677},[667,1964,1952],{"class":746},[667,1966,750],{"class":677},[667,1968,678],{"class":677},[667,1970,1971],{"class":746}," id",[667,1973,750],{"class":677},[667,1975,1977],{"class":1976},"sBMFI"," number",[667,1979,1980],{"class":677}," |",[667,1982,1983],{"class":1976}," string",[667,1985,970],{"class":677},[667,1987,1957],{"class":746},[667,1989,750],{"class":677},[667,1991,1992],{"class":1976}," unknown",[667,1994,1995],{"class":677}," })",[667,1997,1998],{"class":713}," =>",[667,2000,740],{"class":677},[667,2002,2003,2006,2009,2011,2014,2016,2019,2021,2023,2025],{"class":669,"line":700},[667,2004,2005],{"class":673},"  return",[667,2007,2008],{"class":723}," hasActiveSubscription",[667,2010,726],{"class":746},[667,2012,2013],{"class":681},"user",[667,2015,612],{"class":677},[667,2017,2018],{"class":681},"id",[667,2020,737],{"class":677},[667,2022,1957],{"class":681},[667,2024,1925],{"class":746},[667,2026,2027],{"class":758},"\u002F\u002F value: what the role set, `'*'` or plan keys\n",[667,2029,2030],{"class":669,"line":707},[667,2031,2032],{"class":677},"}\n",[593,2034,1843,2035,2038],{},[1291,2036,2037],{},"policy"," refines one permission against a record. It can deny, never grant. Register the file; module policies run\nbefore the host's:",[658,2040,2042],{"className":660,"code":2041,"filename":1179,"language":662,"meta":663,"style":663},"import { addPolicies } from '@frigear-nu\u002Fauth\u002Futils'\n\naddPolicies(nuxt, { path: resolver.resolve('.\u002Fruntime\u002Fserver\u002Fpolicies') })\n",[597,2043,2044,2063,2067],{"__ignoreMap":663},[667,2045,2046,2048,2050,2053,2055,2057,2059,2061],{"class":669,"line":670},[667,2047,674],{"class":673},[667,2049,678],{"class":677},[667,2051,2052],{"class":681}," addPolicies",[667,2054,685],{"class":677},[667,2056,688],{"class":673},[667,2058,691],{"class":677},[667,2060,638],{"class":694},[667,2062,697],{"class":677},[667,2064,2065],{"class":669,"line":700},[667,2066,704],{"emptyLinePlaceholder":703},[667,2068,2069,2072,2074,2076,2078,2080,2082,2084,2086,2088,2090,2092,2095,2097,2099,2101],{"class":669,"line":707},[667,2070,2071],{"class":723},"addPolicies",[667,2073,1236],{"class":681},[667,2075,737],{"class":677},[667,2077,678],{"class":677},[667,2079,1903],{"class":746},[667,2081,750],{"class":677},[667,2083,1908],{"class":681},[667,2085,612],{"class":677},[667,2087,1913],{"class":723},[667,2089,726],{"class":681},[667,2091,729],{"class":677},[667,2093,2094],{"class":694},".\u002Fruntime\u002Fserver\u002Fpolicies",[667,2096,729],{"class":677},[667,2098,1925],{"class":681},[667,2100,1172],{"class":677},[667,2102,1175],{"class":681},[658,2104,2107],{"className":660,"code":2105,"filename":2106,"language":662,"meta":663,"style":663},"\u002F\u002F definePolicy is auto-imported in server code. Return false to deny, true or undefined to keep the result.\nexport const ownTicket = definePolicy\u003C{ userId: number }>('tickets:read', ({ user, record }) => record.userId === user.id)\n","src\u002Fruntime\u002Fserver\u002Fpolicies.ts",[597,2108,2109,2114],{"__ignoreMap":663},[667,2110,2111],{"class":669,"line":670},[667,2112,2113],{"class":758},"\u002F\u002F definePolicy is auto-imported in server code. Return false to deny, true or undefined to keep the result.\n",[667,2115,2116,2118,2120,2123,2125,2128,2131,2134,2136,2138,2141,2143,2145,2147,2149,2151,2153,2155,2157,2160,2162,2164,2166,2168,2171,2174,2176,2178],{"class":669,"line":700},[667,2117,710],{"class":673},[667,2119,714],{"class":713},[667,2121,2122],{"class":681}," ownTicket ",[667,2124,720],{"class":677},[667,2126,2127],{"class":723}," definePolicy",[667,2129,2130],{"class":677},"\u003C{",[667,2132,2133],{"class":746}," userId",[667,2135,750],{"class":677},[667,2137,1977],{"class":1976},[667,2139,2140],{"class":677}," }>",[667,2142,726],{"class":681},[667,2144,729],{"class":677},[667,2146,941],{"class":694},[667,2148,729],{"class":677},[667,2150,737],{"class":677},[667,2152,1948],{"class":677},[667,2154,1952],{"class":1951},[667,2156,737],{"class":677},[667,2158,2159],{"class":1951}," record",[667,2161,1995],{"class":677},[667,2163,1998],{"class":713},[667,2165,2159],{"class":681},[667,2167,612],{"class":677},[667,2169,2170],{"class":681},"userId ",[667,2172,2173],{"class":677},"===",[667,2175,1952],{"class":681},[667,2177,612],{"class":677},[667,2179,2180],{"class":681},"id)\n",[593,2182,2183,2184,2187,2188,635,2190,2193,2194,2197,2198,2201],{},"A policy runs only for a check that passes a record, unless you set ",[597,2185,2186],{},"{ withoutRecord: true }",". A shared policy\n(",[597,2189,1618],{},[597,2191,2192],{},"@frigear-nu\u002Fauth\u002Fpolicies",", registered with ",[597,2195,2196],{},"shared: true",") also runs in the browser's\n",[597,2199,2200],{},"can()",": it must stay synchronous and import nothing from Nuxt, h3 or the database.",[653,2203,2205],{"id":2204},"check-them-in-routes-and-tools","Check them in routes and tools",[593,2207,2208],{},"Authorize first, load second, authorize with the record third. Name the permission through the access declaration:",[658,2210,2213],{"className":660,"code":2211,"filename":2212,"language":662,"meta":663,"style":663},"import { eventsAccess } from '..\u002F..\u002F..\u002Faccess'\n\nexport default defineEventHandler(async (event) => {\n  await authorize(event, eventsAccess.permissions.manage)\n  const { id } = await getValidatedRouterParams(event, idParams().parse)\n  const record = await loadEvent(id)\n  await authorize(event, eventsAccess.permissions.manage, record)\n  \u002F\u002F ...\n})\n","src\u002Fruntime\u002Fserver\u002Froutes\u002Fadmin\u002Fevent.patch.ts",[597,2214,2215,2234,2238,2265,2291,2330,2349,2377,2382],{"__ignoreMap":663},[667,2216,2217,2219,2221,2223,2225,2227,2229,2232],{"class":669,"line":670},[667,2218,674],{"class":673},[667,2220,678],{"class":677},[667,2222,1209],{"class":681},[667,2224,685],{"class":677},[667,2226,688],{"class":673},[667,2228,691],{"class":677},[667,2230,2231],{"class":694},"..\u002F..\u002F..\u002Faccess",[667,2233,697],{"class":677},[667,2235,2236],{"class":669,"line":700},[667,2237,704],{"emptyLinePlaceholder":703},[667,2239,2240,2242,2244,2247,2249,2252,2255,2258,2261,2263],{"class":669,"line":707},[667,2241,710],{"class":673},[667,2243,1942],{"class":673},[667,2245,2246],{"class":723}," defineEventHandler",[667,2248,726],{"class":681},[667,2250,2251],{"class":713},"async",[667,2253,2254],{"class":677}," (",[667,2256,2257],{"class":1951},"event",[667,2259,2260],{"class":677},")",[667,2262,1998],{"class":713},[667,2264,740],{"class":677},[667,2266,2267,2270,2273,2275,2277,2279,2281,2283,2285,2287,2289],{"class":669,"line":743},[667,2268,2269],{"class":673},"  await",[667,2271,2272],{"class":723}," authorize",[667,2274,726],{"class":746},[667,2276,2257],{"class":681},[667,2278,737],{"class":677},[667,2280,1209],{"class":681},[667,2282,612],{"class":677},[667,2284,1416],{"class":681},[667,2286,612],{"class":677},[667,2288,768],{"class":681},[667,2290,1175],{"class":746},[667,2292,2293,2296,2298,2300,2302,2305,2308,2311,2313,2315,2317,2320,2323,2325,2328],{"class":669,"line":755},[667,2294,2295],{"class":713},"  const",[667,2297,678],{"class":677},[667,2299,1971],{"class":681},[667,2301,685],{"class":677},[667,2303,2304],{"class":677}," =",[667,2306,2307],{"class":673}," await",[667,2309,2310],{"class":723}," getValidatedRouterParams",[667,2312,726],{"class":746},[667,2314,2257],{"class":681},[667,2316,737],{"class":677},[667,2318,2319],{"class":723}," idParams",[667,2321,2322],{"class":746},"()",[667,2324,612],{"class":677},[667,2326,2327],{"class":681},"parse",[667,2329,1175],{"class":746},[667,2331,2332,2334,2336,2338,2340,2343,2345,2347],{"class":669,"line":762},[667,2333,2295],{"class":713},[667,2335,2159],{"class":681},[667,2337,2304],{"class":677},[667,2339,2307],{"class":673},[667,2341,2342],{"class":723}," loadEvent",[667,2344,726],{"class":746},[667,2346,2018],{"class":681},[667,2348,1175],{"class":746},[667,2350,2351,2353,2355,2357,2359,2361,2363,2365,2367,2369,2371,2373,2375],{"class":669,"line":824},[667,2352,2269],{"class":673},[667,2354,2272],{"class":723},[667,2356,726],{"class":746},[667,2358,2257],{"class":681},[667,2360,737],{"class":677},[667,2362,1209],{"class":681},[667,2364,612],{"class":677},[667,2366,1416],{"class":681},[667,2368,612],{"class":677},[667,2370,768],{"class":681},[667,2372,737],{"class":677},[667,2374,2159],{"class":681},[667,2376,1175],{"class":746},[667,2378,2379],{"class":669,"line":881},[667,2380,2381],{"class":758},"  \u002F\u002F ...\n",[667,2383,2384,2386],{"class":669,"line":936},[667,2385,1172],{"class":677},[667,2387,1175],{"class":681},[593,2389,2390,2391,2394,2395,2398,2399,1339,2401,2404,2405,2407],{},"An unregistered name is ",[597,2392,2393],{},"403 forbidden"," for everyone, so a typo is not a hole. MCP tools and resources pass\n",[597,2396,2397],{},"{ permission }"," and nothing else. Never check another user with ",[597,2400,1611],{},[597,2402,2403],{},"userCan"," is for assignment and target\nchecks only. ",[606,2406,121],{"href":122}," has the rest of the handler order.",[593,2409,2410,2411,2414],{},"Menus, widgets and tabs take ",[597,2412,2413],{},"permission"," to hide what a visitor cannot use. That is display only, so the route\nbehind them checks too.",[653,2416,2418],{"id":2417},"under-the-hood","Under the hood",[593,2420,2421,2423,2424,2426,2427,2429,2430,2433],{},[597,2422,634],{}," calls the low-level build functions of ",[597,2425,638],{},", which stay available for what\n",[597,2428,622],{}," does not cover, such as scopes read from module options (OAuth Server's ",[597,2431,2432],{},"oauthServer.scopes",") or a test\nfixture's one-off role:",[1391,2435,2436,2446],{},[1394,2437,2438],{},[1397,2439,2440,2443],{},[1400,2441,2442],{},"Function",[1400,2444,2445],{},"Registers",[1407,2447,2448,2461,2471,2486,2496],{},[1397,2449,2450,2455],{},[1412,2451,2452],{},[597,2453,2454],{},"addPermission(nuxt, ...definitions)",[1412,2456,2457,2458,612],{},"Permissions, each with its full ",[597,2459,2460],{},"name",[1397,2462,2463,2468],{},[1412,2464,2465],{},[597,2466,2467],{},"addScope(nuxt, ...scopes)",[1412,2469,2470],{},"Named scopes.",[1397,2472,2473,2478],{},[1412,2474,2475],{},[597,2476,2477],{},"addRole(nuxt, ...roles)",[1412,2479,2480,2481,1580,2483,2485],{},"Roles, with ",[597,2482,2460],{},[597,2484,1457],{}," (the provenance).",[1397,2487,2488,2493],{},[1412,2489,2490],{},[597,2491,2492],{},"addRoleRequirement(nuxt, { name, path })",[1412,2494,2495],{},"A role requirement.",[1397,2497,2498,2503],{},[1412,2499,2500],{},[597,2501,2502],{},"addPolicies(nuxt, { path, shared? })",[1412,2504,2505],{},"A file of policies.",[658,2507,2509],{"className":660,"code":2508,"language":662,"meta":663,"style":663},"addPermission(nuxt, { name: 'events:manage', label: 'Manage events', labelKey: 'events.permissions.manage', grantTo: ['admin'] })\naddRole(nuxt, { name: 'eventmanager', label: 'Event manager', grant: ['events:manage'], from: '@frigear-nu\u002Fevents' })\n",[597,2510,2511,2578],{"__ignoreMap":663},[667,2512,2513,2516,2518,2520,2522,2524,2526,2528,2530,2532,2534,2536,2538,2540,2542,2544,2546,2549,2551,2553,2556,2558,2560,2562,2564,2566,2568,2570,2572,2574,2576],{"class":669,"line":670},[667,2514,2515],{"class":723},"addPermission",[667,2517,1236],{"class":681},[667,2519,737],{"class":677},[667,2521,678],{"class":677},[667,2523,1889],{"class":746},[667,2525,750],{"class":677},[667,2527,691],{"class":677},[667,2529,1137],{"class":694},[667,2531,729],{"class":677},[667,2533,737],{"class":677},[667,2535,777],{"class":746},[667,2537,750],{"class":677},[667,2539,691],{"class":677},[667,2541,784],{"class":694},[667,2543,729],{"class":677},[667,2545,737],{"class":677},[667,2547,2548],{"class":746}," labelKey",[667,2550,750],{"class":677},[667,2552,691],{"class":677},[667,2554,2555],{"class":694},"events.permissions.manage",[667,2557,729],{"class":677},[667,2559,737],{"class":677},[667,2561,791],{"class":746},[667,2563,750],{"class":677},[667,2565,796],{"class":681},[667,2567,729],{"class":677},[667,2569,801],{"class":694},[667,2571,729],{"class":677},[667,2573,815],{"class":681},[667,2575,1172],{"class":677},[667,2577,1175],{"class":681},[667,2579,2580,2583,2585,2587,2589,2591,2593,2595,2597,2599,2601,2603,2605,2607,2609,2611,2613,2615,2617,2619,2621,2623,2625,2628,2630,2632,2634,2636,2638,2640,2642],{"class":669,"line":700},[667,2581,2582],{"class":723},"addRole",[667,2584,1236],{"class":681},[667,2586,737],{"class":677},[667,2588,678],{"class":677},[667,2590,1889],{"class":746},[667,2592,750],{"class":677},[667,2594,691],{"class":677},[667,2596,810],{"class":694},[667,2598,729],{"class":677},[667,2600,737],{"class":677},[667,2602,777],{"class":746},[667,2604,750],{"class":677},[667,2606,691],{"class":677},[667,2608,1121],{"class":694},[667,2610,729],{"class":677},[667,2612,737],{"class":677},[667,2614,1128],{"class":746},[667,2616,750],{"class":677},[667,2618,796],{"class":681},[667,2620,729],{"class":677},[667,2622,1137],{"class":694},[667,2624,729],{"class":677},[667,2626,2627],{"class":681},"]",[667,2629,737],{"class":677},[667,2631,688],{"class":746},[667,2633,750],{"class":677},[667,2635,691],{"class":677},[667,2637,1277],{"class":694},[667,2639,729],{"class":677},[667,2641,685],{"class":677},[667,2643,1175],{"class":681},[653,2645,2647],{"id":2646},"test-it","Test it",[593,2649,2650,2651,2653,2654,2656],{},"A fixture does not need ",[597,2652,1544],{}," to have roles. Register the roles your tests use with ",[597,2655,2582],{},",\nand give the fixture a role provider that reads a field of the test user. The events fixture decides by email domain,\nso a cookie session and an API token get the same answer:",[658,2658,2661],{"className":660,"code":2659,"filename":2660,"language":662,"meta":663,"style":663},"import { fileURLToPath } from 'node:url'\nimport { addRole, setRoleProvider } from '@frigear-nu\u002Fauth\u002Futils'\nimport MyModule from '..\u002F..\u002F..\u002Fsrc\u002Fmodule'\n\nexport default defineNuxtConfig({\n  modules: [\n    '@nuxthub\u002Fcore',\n    '@frigear-nu\u002Fusers',\n    MyModule,\n    (_options, nuxt) => {\n      addRole(nuxt, { name: 'wild', label: 'Wildcard staff', grant: ['events:*'] })\n      setRoleProvider(nuxt, { path: fileURLToPath(new URL('.\u002Fserver\u002Froles\u002Fprovider', import.meta.url)) })\n    },\n  ],\n})\n","test\u002Ffixtures\u002Fauthenticated\u002Fnuxt.config.ts",[597,2662,2663,2683,2707,2723,2727,2741,2751,2762,2773,2780,2799,2858,2916,2921,2928],{"__ignoreMap":663},[667,2664,2665,2667,2669,2672,2674,2676,2678,2681],{"class":669,"line":670},[667,2666,674],{"class":673},[667,2668,678],{"class":677},[667,2670,2671],{"class":681}," fileURLToPath",[667,2673,685],{"class":677},[667,2675,688],{"class":673},[667,2677,691],{"class":677},[667,2679,2680],{"class":694},"node:url",[667,2682,697],{"class":677},[667,2684,2685,2687,2689,2692,2694,2697,2699,2701,2703,2705],{"class":669,"line":700},[667,2686,674],{"class":673},[667,2688,678],{"class":677},[667,2690,2691],{"class":681}," addRole",[667,2693,737],{"class":677},[667,2695,2696],{"class":681}," setRoleProvider",[667,2698,685],{"class":677},[667,2700,688],{"class":673},[667,2702,691],{"class":677},[667,2704,638],{"class":694},[667,2706,697],{"class":677},[667,2708,2709,2711,2714,2716,2718,2721],{"class":669,"line":707},[667,2710,674],{"class":673},[667,2712,2713],{"class":681}," MyModule ",[667,2715,1457],{"class":673},[667,2717,691],{"class":677},[667,2719,2720],{"class":694},"..\u002F..\u002F..\u002Fsrc\u002Fmodule",[667,2722,697],{"class":677},[667,2724,2725],{"class":669,"line":743},[667,2726,704],{"emptyLinePlaceholder":703},[667,2728,2729,2731,2733,2736,2738],{"class":669,"line":755},[667,2730,710],{"class":673},[667,2732,1942],{"class":673},[667,2734,2735],{"class":723}," defineNuxtConfig",[667,2737,726],{"class":681},[667,2739,2740],{"class":677},"{\n",[667,2742,2743,2746,2748],{"class":669,"line":762},[667,2744,2745],{"class":746},"  modules",[667,2747,750],{"class":677},[667,2749,2750],{"class":681}," [\n",[667,2752,2753,2755,2758,2760],{"class":669,"line":824},[667,2754,765],{"class":677},[667,2756,2757],{"class":694},"@nuxthub\u002Fcore",[667,2759,729],{"class":677},[667,2761,1615],{"class":677},[667,2763,2764,2766,2769,2771],{"class":669,"line":881},[667,2765,765],{"class":677},[667,2767,2768],{"class":694},"@frigear-nu\u002Fusers",[667,2770,729],{"class":677},[667,2772,1615],{"class":677},[667,2774,2775,2778],{"class":669,"line":936},[667,2776,2777],{"class":681},"    MyModule",[667,2779,1615],{"class":677},[667,2781,2782,2785,2788,2790,2793,2795,2797],{"class":669,"line":976},[667,2783,2784],{"class":677},"    (",[667,2786,2787],{"class":1951},"_options",[667,2789,737],{"class":677},[667,2791,2792],{"class":1951}," nuxt",[667,2794,2260],{"class":677},[667,2796,1998],{"class":713},[667,2798,740],{"class":677},[667,2800,2801,2804,2806,2809,2811,2813,2815,2817,2819,2822,2824,2826,2828,2830,2832,2835,2837,2839,2841,2843,2845,2847,2850,2852,2854,2856],{"class":669,"line":1012},[667,2802,2803],{"class":723},"      addRole",[667,2805,726],{"class":746},[667,2807,2808],{"class":681},"nuxt",[667,2810,737],{"class":677},[667,2812,678],{"class":677},[667,2814,1889],{"class":746},[667,2816,750],{"class":677},[667,2818,691],{"class":677},[667,2820,2821],{"class":694},"wild",[667,2823,729],{"class":677},[667,2825,737],{"class":677},[667,2827,777],{"class":746},[667,2829,750],{"class":677},[667,2831,691],{"class":677},[667,2833,2834],{"class":694},"Wildcard staff",[667,2836,729],{"class":677},[667,2838,737],{"class":677},[667,2840,1128],{"class":746},[667,2842,750],{"class":677},[667,2844,796],{"class":746},[667,2846,729],{"class":677},[667,2848,2849],{"class":694},"events:*",[667,2851,729],{"class":677},[667,2853,815],{"class":746},[667,2855,1172],{"class":677},[667,2857,1175],{"class":746},[667,2859,2860,2863,2865,2867,2869,2871,2873,2875,2877,2879,2882,2885,2887,2889,2892,2894,2896,2899,2901,2904,2906,2909,2912,2914],{"class":669,"line":1018},[667,2861,2862],{"class":723},"      setRoleProvider",[667,2864,726],{"class":746},[667,2866,2808],{"class":681},[667,2868,737],{"class":677},[667,2870,678],{"class":677},[667,2872,1903],{"class":746},[667,2874,750],{"class":677},[667,2876,2671],{"class":723},[667,2878,726],{"class":746},[667,2880,2881],{"class":677},"new",[667,2883,2884],{"class":723}," URL",[667,2886,726],{"class":746},[667,2888,729],{"class":677},[667,2890,2891],{"class":694},".\u002Fserver\u002Froles\u002Fprovider",[667,2893,729],{"class":677},[667,2895,737],{"class":677},[667,2897,2898],{"class":673}," import",[667,2900,612],{"class":677},[667,2902,2903],{"class":681},"meta",[667,2905,612],{"class":677},[667,2907,2908],{"class":681},"url",[667,2910,2911],{"class":746},")) ",[667,2913,1172],{"class":677},[667,2915,1175],{"class":746},[667,2917,2918],{"class":669,"line":1028},[667,2919,2920],{"class":677},"    },\n",[667,2922,2923,2926],{"class":669,"line":1090},[667,2924,2925],{"class":681},"  ]",[667,2927,1615],{"class":677},[667,2929,2930,2932],{"class":669,"line":1095},[667,2931,1172],{"class":677},[667,2933,1175],{"class":681},[658,2935,2938],{"className":660,"code":2936,"filename":2937,"language":662,"meta":663,"style":663},"import { db } from '@nuxthub\u002Fdb'\nimport type { AuthRoleProvider } from '@frigear-nu\u002Fauth\u002Futils'\n\nconst provider: AuthRoleProvider = {\n  getAssignedRoles: async (_event, userId) => {\n    const actor = await db.query.users.findFirst({ where: (t, { eq }) => eq(t.id, Number(userId)) })\n    return actor?.email.endsWith('@staff.test') ? ['eventmanager'] : []\n  },\n  setRoles: async () => {},\n}\n\nexport default provider\n","test\u002Ffixtures\u002Fauthenticated\u002Fserver\u002Froles\u002Fprovider.ts",[597,2939,2940,2960,2982,2986,3002,3026,3107,3153,3157,3174,3178,3182],{"__ignoreMap":663},[667,2941,2942,2944,2946,2949,2951,2953,2955,2958],{"class":669,"line":670},[667,2943,674],{"class":673},[667,2945,678],{"class":677},[667,2947,2948],{"class":681}," db",[667,2950,685],{"class":677},[667,2952,688],{"class":673},[667,2954,691],{"class":677},[667,2956,2957],{"class":694},"@nuxthub\u002Fdb",[667,2959,697],{"class":677},[667,2961,2962,2964,2967,2969,2972,2974,2976,2978,2980],{"class":669,"line":700},[667,2963,674],{"class":673},[667,2965,2966],{"class":673}," type",[667,2968,678],{"class":677},[667,2970,2971],{"class":681}," AuthRoleProvider",[667,2973,685],{"class":677},[667,2975,688],{"class":673},[667,2977,691],{"class":677},[667,2979,638],{"class":694},[667,2981,697],{"class":677},[667,2983,2984],{"class":669,"line":707},[667,2985,704],{"emptyLinePlaceholder":703},[667,2987,2988,2991,2994,2996,2998,3000],{"class":669,"line":743},[667,2989,2990],{"class":713},"const",[667,2992,2993],{"class":681}," provider",[667,2995,750],{"class":677},[667,2997,2971],{"class":1976},[667,2999,2304],{"class":677},[667,3001,740],{"class":677},[667,3003,3004,3007,3009,3011,3013,3016,3018,3020,3022,3024],{"class":669,"line":755},[667,3005,3006],{"class":723},"  getAssignedRoles",[667,3008,750],{"class":677},[667,3010,1945],{"class":713},[667,3012,2254],{"class":677},[667,3014,3015],{"class":1951},"_event",[667,3017,737],{"class":677},[667,3019,2133],{"class":1951},[667,3021,2260],{"class":677},[667,3023,1998],{"class":713},[667,3025,740],{"class":677},[667,3027,3028,3031,3034,3036,3038,3040,3042,3045,3047,3050,3052,3055,3057,3060,3063,3065,3067,3070,3072,3074,3077,3079,3081,3083,3085,3087,3089,3091,3093,3096,3098,3101,3103,3105],{"class":669,"line":762},[667,3029,3030],{"class":713},"    const",[667,3032,3033],{"class":681}," actor",[667,3035,2304],{"class":677},[667,3037,2307],{"class":673},[667,3039,2948],{"class":681},[667,3041,612],{"class":677},[667,3043,3044],{"class":681},"query",[667,3046,612],{"class":677},[667,3048,3049],{"class":681},"users",[667,3051,612],{"class":677},[667,3053,3054],{"class":723},"findFirst",[667,3056,726],{"class":746},[667,3058,3059],{"class":677},"{",[667,3061,3062],{"class":723}," where",[667,3064,750],{"class":677},[667,3066,2254],{"class":677},[667,3068,3069],{"class":1951},"t",[667,3071,737],{"class":677},[667,3073,678],{"class":677},[667,3075,3076],{"class":1951}," eq",[667,3078,1995],{"class":677},[667,3080,1998],{"class":713},[667,3082,3076],{"class":723},[667,3084,726],{"class":746},[667,3086,3069],{"class":681},[667,3088,612],{"class":677},[667,3090,2018],{"class":681},[667,3092,737],{"class":677},[667,3094,3095],{"class":723}," Number",[667,3097,726],{"class":746},[667,3099,3100],{"class":681},"userId",[667,3102,2911],{"class":746},[667,3104,1172],{"class":677},[667,3106,1175],{"class":746},[667,3108,3109,3112,3114,3116,3119,3121,3124,3126,3128,3131,3133,3135,3138,3140,3142,3144,3146,3148,3150],{"class":669,"line":824},[667,3110,3111],{"class":673},"    return",[667,3113,3033],{"class":681},[667,3115,1263],{"class":677},[667,3117,3118],{"class":681},"email",[667,3120,612],{"class":677},[667,3122,3123],{"class":723},"endsWith",[667,3125,726],{"class":746},[667,3127,729],{"class":677},[667,3129,3130],{"class":694},"@staff.test",[667,3132,729],{"class":677},[667,3134,1925],{"class":746},[667,3136,3137],{"class":677},"?",[667,3139,796],{"class":746},[667,3141,729],{"class":677},[667,3143,810],{"class":694},[667,3145,729],{"class":677},[667,3147,815],{"class":746},[667,3149,750],{"class":677},[667,3151,3152],{"class":746}," []\n",[667,3154,3155],{"class":669,"line":881},[667,3156,1015],{"class":677},[667,3158,3159,3162,3164,3166,3169,3171],{"class":669,"line":936},[667,3160,3161],{"class":723},"  setRoles",[667,3163,750],{"class":677},[667,3165,1945],{"class":713},[667,3167,3168],{"class":677}," ()",[667,3170,1998],{"class":713},[667,3172,3173],{"class":677}," {},\n",[667,3175,3176],{"class":669,"line":976},[667,3177,2032],{"class":677},[667,3179,3180],{"class":669,"line":1012},[667,3181,704],{"emptyLinePlaceholder":703},[667,3183,3184,3186,3188],{"class":669,"line":1018},[667,3185,710],{"class":673},[667,3187,1942],{"class":673},[667,3189,3190],{"class":681}," provider\n",[593,3192,3193,3194,612],{},"The real example is ",[597,3195,3196],{},"packages\u002Fevents\u002Ftest\u002Ffixtures\u002Fauthenticated",[1285,3198,3199,3226,3238,3260,3272],{},[1288,3200,3201,3204,3205,3208,3209,3211,3212,3215,3216,1580,3219,635,3222,3225],{},[1291,3202,3203],{},"Cover the credentials."," For each route, test a cookie session, an API token that carries the permission, one that\ndoes not (",[597,3206,3207],{},"403 insufficient_scope","), and for a ",[597,3210,1528],{}," permission a token that does (",[597,3213,3214],{},"403 session_required",").\n",[597,3217,3218],{},"apiClient({ cookie?, bearer? })",[597,3220,3221],{},"expectApiError(promise, status, code?)",[597,3223,3224],{},"@frigear-nu\u002Fmodule-kit\u002Ftesting","\ndo the calls.",[1288,3227,3228,3231,3232,3234,3235,3237],{},[1291,3229,3230],{},"Cover the wildcard."," A role with ",[597,3233,1517],{}," must not reach the ",[597,3236,1521],{}," permission.",[1288,3239,3240,3243,3244,3247,3248,3250,3251,3254,3255,3259],{},[1291,3241,3242],{},"Migrations."," API tokens freeze their permissions in ",[597,3245,3246],{},"auth_user_tokens.permissions",". Every fixture that includes a\nmodule using ",[597,3249,1608],{}," needs the ",[597,3252,3253],{},"auth_user_tokens"," migration with that column, and regenerate it when auth adds\none: ",[606,3256,3258],{"href":3257},"\u002Fcontribute\u002Fwriting-a-module\u002Fdatabase-tables#generate-test-fixture-migrations","Generate test fixture migrations",".\nWithout it every token write fails, which looks like a missing session cookie.",[1288,3261,3262,3265,3266,3269,3270,612],{},[1291,3263,3264],{},"Name permissions through the access file"," in tests too (",[597,3267,3268],{},"eventsAccess.permissions.refund","), so a renamed\npermission breaks the test at compile time instead of silently testing a ",[597,3271,2393],{},[1288,3273,3274,3277,3278,3281,3282,612],{},[1291,3275,3276],{},"Unit tests."," The matcher and the decision order are pure (",[597,3279,3280],{},"packages\u002Fauth\u002Fsrc\u002Fruntime\u002Futils\u002Fpermissions.ts",").\nTest a rule of yours without a Nuxt server under ",[597,3283,3284],{},"test\u002Funit\u002F",[653,3286,3288],{"id":3287},"next-steps","Next steps",[1285,3290,3291,3296,3301],{},[1288,3292,3293,3295],{},[606,3294,537],{"href":538}," – boot the fixture and test API routes.",[1288,3297,3298,3300],{},[606,3299,151],{"href":152}," – what the removed APIs map to.",[1288,3302,3303,3305],{},[606,3304,67],{"href":68}," – the vocabulary and the fail-closed table.",[3307,3308,3309],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":663,"searchDepth":700,"depth":700,"links":3311},[3312,3317,3318,3319,3320,3321,3322,3323],{"id":655,"depth":700,"text":656,"children":3313},[3314,3315,3316],{"id":1464,"depth":707,"text":1465},{"id":1586,"depth":707,"text":1587},{"id":1639,"depth":707,"text":1640},{"id":1430,"depth":700,"text":1740},{"id":1266,"depth":700,"text":1775},{"id":1839,"depth":700,"text":1840},{"id":2204,"depth":700,"text":2205},{"id":2417,"depth":700,"text":2418},{"id":2646,"depth":700,"text":2647},{"id":3287,"depth":700,"text":3288},"Declare a module's permissions, scopes and roles with defineAccess, register them with registerAccess, add role requirements and policies, and test them with a role provider in a fixture.","md",null,{},{"icon":70},{"title":67,"description":3324},"lfWO-akpX16wjki0miDhv5JFnshGwSztOWx1jm3-3Q0",[3332,3334],{"title":537,"path":538,"stem":539,"description":3333,"icon":540,"children":-1},"Test a Frigear module with Vitest and test fixtures, including database and sign-in tests, then document and release it.",{"title":545,"path":546,"stem":547,"description":3335,"icon":548,"children":-1},"Release a package to npm from the Release workflow or with release-it, and resume an interrupted publish.",1794503153467]