[{"data":1,"prerenderedAt":2920},["ShallowReactive",2],{"navigation":3,"/features/api-keys":283,"/features/api-keys-surround":2915},[4,16,20,68,128,168,179,259,263,267,271,275,279],{"title":5,"path":6,"stem":7,"children":8},"Getting Started","/getting-started","1.getting-started",[9,12],{"title":10,"path":6,"stem":11},"Introduction","1.getting-started/index",{"title":13,"path":14,"stem":15},"Installation","/getting-started/installation","1.getting-started/1.installation",{"title":17,"path":18,"stem":19},"Upgrading","/upgrading","10.upgrading",{"title":21,"path":22,"stem":23,"children":24},"Essentials","/essentials","2.essentials",[25,28,32,36,40,44,48,52,56,60,64],{"title":26,"path":22,"stem":27},"Core concepts","2.essentials/index",{"title":29,"path":30,"stem":31},"Routing","/essentials/routing","2.essentials/1.routing",{"title":33,"path":34,"stem":35},"Typed Requests & Responses","/essentials/typed-io","2.essentials/10.typed-io",{"title":37,"path":38,"stem":39},"Authentication","/essentials/authentication","2.essentials/2.authentication",{"title":41,"path":42,"stem":43},"Identity & User Store","/essentials/identity","2.essentials/3.identity",{"title":45,"path":46,"stem":47},"Requests & Responses","/essentials/requests-responses","2.essentials/4.requests-responses",{"title":49,"path":50,"stem":51},"Controllers","/essentials/controllers","2.essentials/5.controllers",{"title":53,"path":54,"stem":55},"Database","/essentials/database","2.essentials/6.database",{"title":57,"path":58,"stem":59},"Validation","/essentials/validation","2.essentials/7.validation",{"title":61,"path":62,"stem":63},"Migrations","/essentials/migrations","2.essentials/8.migrations",{"title":65,"path":66,"stem":67},"ORM Models","/essentials/orm","2.essentials/9.orm",{"title":69,"path":70,"stem":71,"children":72},"Features","/features","3.features",[73,76,80,84,88,92,96,100,104,108,112,116,120,124],{"title":74,"path":70,"stem":75},"Built‑in Capabilities","3.features/index",{"title":77,"path":78,"stem":79},"API Keys & Scopes","/features/api-keys","3.features/api-keys",{"title":81,"path":82,"stem":83},"Browser Sessions (Cookie Auth)","/features/browser-sessions","3.features/browser-sessions",{"title":85,"path":86,"stem":87},"Caching","/features/caching","3.features/caching",{"title":89,"path":90,"stem":91},"CORS & CSRF","/features/cors-csrf","3.features/cors-csrf",{"title":93,"path":94,"stem":95},"Distributed Locks","/features/distributed-locks","3.features/distributed-locks",{"title":97,"path":98,"stem":99},"Events & Listeners","/features/events","3.features/events",{"title":101,"path":102,"stem":103},"Field Selection","/features/field-selection","3.features/field-selection",{"title":105,"path":106,"stem":107},"File Uploads","/features/file-uploads","3.features/file-uploads",{"title":109,"path":110,"stem":111},"Notifications","/features/notifications","3.features/notifications",{"title":113,"path":114,"stem":115},"Queues & Jobs","/features/queues-jobs","3.features/queues-jobs",{"title":117,"path":118,"stem":119},"Rate Limiting","/features/rate-limiting","3.features/rate-limiting",{"title":121,"path":122,"stem":123},"Task Scheduling","/features/scheduling","3.features/scheduling",{"title":125,"path":126,"stem":127},"Webhooks","/features/webhooks","3.features/webhooks",{"title":129,"path":130,"stem":131,"children":132},"Advanced","/advanced","4.advanced",[133,136,140,144,148,152,156,160,164],{"title":134,"path":130,"stem":135},"Techniques and Patterns","4.advanced/index",{"title":137,"path":138,"stem":139},"Configuration","/advanced/configuration","4.advanced/configuration",{"title":141,"path":142,"stem":143},"Dependency Injection","/advanced/dependency-injection","4.advanced/dependency-injection",{"title":145,"path":146,"stem":147},"Middleware","/advanced/middleware","4.advanced/middleware",{"title":149,"path":150,"stem":151},"OpenAPI Generation","/advanced/openapi","4.advanced/openapi",{"title":153,"path":154,"stem":155},"Performance","/advanced/performance","4.advanced/performance",{"title":157,"path":158,"stem":159},"Repositories","/advanced/repositories","4.advanced/repositories",{"title":161,"path":162,"stem":163},"Service Providers","/advanced/service-providers","4.advanced/service-providers",{"title":165,"path":166,"stem":167},"Testing","/advanced/testing","4.advanced/testing",{"title":169,"path":170,"stem":171,"children":172},"Overview","/tooling","5.tooling",[173,175],{"title":169,"path":170,"stem":174},"5.tooling/index",{"title":176,"path":177,"stem":178},"AI Skills","/tooling/ai-skills","5.tooling/1.ai-skills",{"title":180,"path":181,"stem":182,"children":183},"Cookbook","/cookbook","6.cookbook",[184,187,191,195,199,203,207,211,215,219,223,227,231,235,239,243,247,251,255],{"title":185,"path":181,"stem":186},"Recipes & How‑Tos","6.cookbook/index",{"title":188,"path":189,"stem":190},"Routing Recipes","/cookbook/routing","6.cookbook/1.routing",{"title":192,"path":193,"stem":194},"Caching Recipes","/cookbook/caching","6.cookbook/10.caching",{"title":196,"path":197,"stem":198},"Queue Infrastructure","/cookbook/queues-and-jobs","6.cookbook/11.queues-and-jobs",{"title":200,"path":201,"stem":202},"Notification Channels & Templates","/cookbook/notifications","6.cookbook/13.notifications",{"title":204,"path":205,"stem":206},"Storage","/cookbook/storage","6.cookbook/15.storage",{"title":208,"path":209,"stem":210},"Image Processing Examples","/cookbook/image-processing","6.cookbook/16.image-processing",{"title":212,"path":213,"stem":214},"Permissions and Authorization","/cookbook/permissions-and-authorization","6.cookbook/18.permissions-and-authorization",{"title":216,"path":217,"stem":218},"Session Analytics","/cookbook/sessions-analytics","6.cookbook/19.sessions-analytics",{"title":220,"path":221,"stem":222},"Writing Middleware","/cookbook/middleware","6.cookbook/2.middleware",{"title":224,"path":225,"stem":226},"API Metrics","/cookbook/api-metrics","6.cookbook/20.api-metrics",{"title":228,"path":229,"stem":230},"Performance Recipes","/cookbook/performance","6.cookbook/21.performance",{"title":232,"path":233,"stem":234},"Memory Management","/cookbook/memory-management","6.cookbook/22.memory-management",{"title":236,"path":237,"stem":238},"Writing Console Commands","/cookbook/console-commands","6.cookbook/23.console-commands",{"title":240,"path":241,"stem":242},"Glueful Extensions","/cookbook/extensions","6.cookbook/25.extensions",{"title":244,"path":245,"stem":246},"Error Handling","/cookbook/error-handling","6.cookbook/4.error-handling",{"title":248,"path":249,"stem":250},"Security Guide","/cookbook/security","6.cookbook/5.security",{"title":252,"path":253,"stem":254},"Service Options Resolver","/cookbook/configuration","6.cookbook/6.configuration",{"title":256,"path":257,"stem":258},"Logging","/cookbook/logging","6.cookbook/7.logging",{"title":260,"path":261,"stem":262},"API Reference","/api-reference","7.api-reference",{"title":264,"path":265,"stem":266},"CLI Reference","/cli-reference","8.cli-reference",{"title":268,"path":269,"stem":270},"Extensions","/extensions","9.extensions",{"title":272,"path":273,"stem":274},"Contributing","/contributing","contributing",{"title":276,"path":277,"stem":278},"Release Notes","/releases","releases",{"title":280,"path":281,"stem":282},"Release Archive","/releases-archive","releases-archive",{"id":284,"title":77,"body":285,"description":2909,"extension":2910,"links":2911,"meta":2912,"navigation":389,"path":78,"seo":2913,"stem":79,"__hash__":2914},"docs/3.features/api-keys.md",{"type":286,"value":287,"toc":2886},"minimark",[288,301,306,417,574,578,588,655,665,682,693,700,707,809,815,819,824,896,972,975,979,985,1271,1285,1289,1312,1443,1447,1450,1481,1640,1659,1663,1685,1714,1718,1721,1753,1764,1826,1835,1844,1847,1906,1909,1913,1918,1953,1960,1964,1976,1979,2003,2083,2090,2102,2116,2368,2371,2448,2462,2485,2489,2505,2508,2549,2552,2810,2824,2827,2831,2855,2859,2882],[289,290,291,292,296,297,300],"p",{},"API keys are long-lived credentials for server-to-server clients: CI jobs, integrations, partner backends. Each key belongs to a user, is stored hashed in the core ",[293,294,295],"code",{},"api_keys"," table, and can be limited by scopes, an IP allowlist and an expiry date. The standard ",[293,298,299],{},"auth"," middleware accepts them alongside JWT bearer tokens.",[302,303,305],"h2",{"id":304},"quick-start","Quick Start",[307,308,313],"pre",{"className":309,"code":310,"language":311,"meta":312,"style":312},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark monokai","# 1. Create a key (the plaintext is printed once)\nphp glueful apikey:create --user=\u003Cuser-uuid> --name=\"CI deploy\" \\\n  --scopes=\"read:posts,write:posts\" --expires=+1year\n\n# 2. Call the API with it\ncurl -H \"X-API-Key: gf_live_...\" https://api.example.com/api/v1/posts\n","bash","",[293,314,315,324,368,384,391,397],{"__ignoreMap":312},[316,317,320],"span",{"class":318,"line":319},"line",1,[316,321,323],{"class":322},"ss7Ak","# 1. Create a key (the plaintext is printed once)\n",[316,325,327,331,335,338,342,346,349,352,355,359,362,364],{"class":318,"line":326},2,[316,328,330],{"class":329},"sR7ES","php",[316,332,334],{"class":333},"sLACW"," glueful",[316,336,337],{"class":333}," apikey:create",[316,339,341],{"class":340},"sFhLe"," --user=",[316,343,345],{"class":344},"sGXK2","\u003C",[316,347,348],{"class":340},"user-uuid",[316,350,351],{"class":344},">",[316,353,354],{"class":340}," --name=",[316,356,358],{"class":357},"siCPE","\"",[316,360,361],{"class":333},"CI deploy",[316,363,358],{"class":357},[316,365,367],{"class":366},"sQeA1"," \\\n",[316,369,371,374,376,379,381],{"class":318,"line":370},3,[316,372,373],{"class":340},"  --scopes=",[316,375,358],{"class":357},[316,377,378],{"class":333},"read:posts,write:posts",[316,380,358],{"class":357},[316,382,383],{"class":340}," --expires=+1year\n",[316,385,387],{"class":318,"line":386},4,[316,388,390],{"emptyLinePlaceholder":389},true,"\n",[316,392,394],{"class":318,"line":393},5,[316,395,396],{"class":322},"# 2. Call the API with it\n",[316,398,400,403,406,409,412,414],{"class":318,"line":399},6,[316,401,402],{"class":329},"curl",[316,404,405],{"class":340}," -H",[316,407,408],{"class":357}," \"",[316,410,411],{"class":333},"X-API-Key: gf_live_...",[316,413,358],{"class":357},[316,415,416],{"class":333}," https://api.example.com/api/v1/posts\n",[307,418,421],{"className":419,"code":420,"language":330,"meta":312,"style":312},"language-php shiki shiki-themes material-theme-lighter github-light github-dark monokai","// 3. Require a scope on the route\nuse Glueful\\Routing\\Attributes\\{Get, Middleware, RequireScope};\n\n#[Get('/posts')]\n#[Middleware('auth')]\n#[RequireScope('read:posts')]\npublic function index(): Response { /* ... */ }\n",[293,422,423,428,473,477,502,520,540],{"__ignoreMap":312},[316,424,425],{"class":318,"line":319},[316,426,427],{"class":322},"// 3. Require a scope on the route\n",[316,429,430,434,438,442,444,446,449,451,455,459,462,465,467,470],{"class":318,"line":326},[316,431,433],{"class":432},"shWJe","use",[316,435,437],{"class":436},"s91G_"," Glueful",[316,439,441],{"class":440},"sv8o3","\\",[316,443,29],{"class":436},[316,445,441],{"class":440},[316,447,448],{"class":436},"Attributes",[316,450,441],{"class":440},[316,452,454],{"class":453},"swvn1","{",[316,456,458],{"class":457},"seZir","Get",[316,460,461],{"class":453},",",[316,463,464],{"class":457}," Middleware",[316,466,461],{"class":453},[316,468,469],{"class":457}," RequireScope",[316,471,472],{"class":453},"};\n",[316,474,475],{"class":318,"line":370},[316,476,390],{"emptyLinePlaceholder":389},[316,478,479,483,485,488,491,494,496,499],{"class":318,"line":386},[316,480,482],{"class":481},"ss--_","#[",[316,484,458],{"class":436},[316,486,487],{"class":453},"(",[316,489,490],{"class":357},"'",[316,492,493],{"class":333},"/posts",[316,495,490],{"class":357},[316,497,498],{"class":453},")",[316,500,501],{"class":481},"]\n",[316,503,504,506,508,510,512,514,516,518],{"class":318,"line":393},[316,505,482],{"class":481},[316,507,145],{"class":436},[316,509,487],{"class":453},[316,511,490],{"class":357},[316,513,299],{"class":333},[316,515,490],{"class":357},[316,517,498],{"class":453},[316,519,501],{"class":481},[316,521,522,524,527,529,531,534,536,538],{"class":318,"line":399},[316,523,482],{"class":481},[316,525,526],{"class":436},"RequireScope",[316,528,487],{"class":453},[316,530,490],{"class":357},[316,532,533],{"class":333},"read:posts",[316,535,490],{"class":357},[316,537,498],{"class":453},[316,539,501],{"class":481},[316,541,543,547,551,555,558,561,565,568,571],{"class":318,"line":542},7,[316,544,546],{"class":545},"sTNss","public",[316,548,550],{"class":549},"srJo8"," function",[316,552,554],{"class":553},"sD0ED"," index",[316,556,557],{"class":453},"()",[316,559,560],{"class":344},":",[316,562,564],{"class":563},"s_MOj"," Response",[316,566,567],{"class":453}," {",[316,569,570],{"class":322}," /* ... */",[316,572,573],{"class":453}," }\n",[302,575,577],{"id":576},"key-format","Key Format",[289,579,580,581,584,585,560],{},"Keys look like ",[293,582,583],{},"gf_live_\u003C32 random characters>"," or ",[293,586,587],{},"gf_test_\u003C32 random characters>",[589,590,591,604],"table",{},[592,593,594],"thead",{},[595,596,597,601],"tr",{},[598,599,600],"th",{},"Segment",[598,602,603],{},"Value",[605,606,607,626,644],"tbody",{},[595,608,609,613],{},[610,611,612],"td",{},"Brand",[610,614,615,618,619,622,623,498],{},[293,616,617],{},"auth.api_keys.prefix"," (",[293,620,621],{},"API_KEY_PREFIX",", default ",[293,624,625],{},"gf",[595,627,628,631],{},[610,629,630],{},"Environment",[610,632,633,636,637,640,641],{},[293,634,635],{},"live"," when the application environment is ",[293,638,639],{},"production",", otherwise ",[293,642,643],{},"test",[595,645,646,649],{},[610,647,648],{},"Random",[610,650,651,652],{},"32 base62 characters from ",[293,653,654],{},"random_int()",[289,656,657,658,661,662,664],{},"The environment is fixed when the key is created, so a key minted on a staging box is always ",[293,659,660],{},"gf_test_…",". The segment is a label that helps people spot a production key in a log or a leaked config. It does not limit where the key works: any key in the ",[293,663,295],{}," table authenticates.",[307,666,670],{"className":667,"code":668,"language":669,"meta":312,"style":312},"language-env shiki shiki-themes material-theme-lighter github-light github-dark monokai","# Rebrand generated keys: lm_live_... / lm_test_...\nAPI_KEY_PREFIX=lm\n","env",[293,671,672,677],{"__ignoreMap":312},[316,673,674],{"class":318,"line":319},[316,675,676],{},"# Rebrand generated keys: lm_live_... / lm_test_...\n",[316,678,679],{"class":318,"line":326},[316,680,681],{},"API_KEY_PREFIX=lm\n",[289,683,684,685,688,689,692],{},"Keep the brand short. Only the first 16 characters of a key are stored as the indexed lookup prefix (",[293,686,687],{},"key_prefix","); the full key is SHA-256 hashed into ",[293,690,691],{},"key_hash",". The plaintext is never stored, so a lost key can't be recovered, only rotated or revoked.",[302,694,696,697,699],{"id":695},"the-api_keys-table","The ",[293,698,295],{}," Table",[289,701,702,703,706],{},"Core installs the table (",[293,704,705],{},"migrations/auth/003_CreateApiKeysTable.php",") with the other auth tables:",[589,708,709,719],{},[592,710,711],{},[595,712,713,716],{},[598,714,715],{},"Column",[598,717,718],{},"Notes",[605,720,721,731,744,754,769,786,799],{},[595,722,723,728],{},[610,724,725],{},[293,726,727],{},"uuid",[610,729,730],{},"Key id, used by the CLI and in audit events",[595,732,733,738],{},[610,734,735],{},[293,736,737],{},"user_uuid",[610,739,740,741],{},"The principal the key acts as. Indexed, no foreign key: it's resolved through your ",[293,742,743],{},"UserProviderInterface",[595,745,746,751],{},[610,747,748],{},[293,749,750],{},"name",[610,752,753],{},"Human label",[595,755,756,763],{},[610,757,758,760,761],{},[293,759,687],{},", ",[293,762,691],{},[610,764,765,766,768],{},"Lookup prefix and SHA-256 hash; ",[293,767,691],{}," is unique",[595,770,771,779],{},[610,772,773,760,776],{},[293,774,775],{},"scopes",[293,777,778],{},"allowed_ips",[610,780,781,782,785],{},"JSON arrays, ",[293,783,784],{},"NULL"," means no restriction",[595,787,788,796],{},[610,789,790,760,793],{},[293,791,792],{},"expires_at",[293,794,795],{},"revoked_at",[610,797,798],{},"Lifecycle timestamps",[595,800,801,806],{},[610,802,803],{},[293,804,805],{},"rotated_from_id",[610,807,808],{},"Set on the successor key after a rotation",[289,810,811,812,814],{},"If ",[293,813,737],{}," doesn't resolve to a user through the user provider, authentication fails even with a valid key.",[302,816,818],{"id":817},"creating-keys","Creating Keys",[820,821,823],"h3",{"id":822},"from-the-cli","From the CLI",[307,825,827],{"className":309,"code":826,"language":311,"meta":312,"style":312},"php glueful apikey:create --user=\u003Cuser-uuid> --name=\"Partner sync\" \\\n  --scopes=\"read:*,write:orders\" \\\n  --ips=\"203.0.113.10,198.51.100.0/24\" \\\n  --expires=\"+90 days\"\n",[293,828,829,856,869,883],{"__ignoreMap":312},[316,830,831,833,835,837,839,841,843,845,847,849,852,854],{"class":318,"line":319},[316,832,330],{"class":329},[316,834,334],{"class":333},[316,836,337],{"class":333},[316,838,341],{"class":340},[316,840,345],{"class":344},[316,842,348],{"class":340},[316,844,351],{"class":344},[316,846,354],{"class":340},[316,848,358],{"class":357},[316,850,851],{"class":333},"Partner sync",[316,853,358],{"class":357},[316,855,367],{"class":366},[316,857,858,860,862,865,867],{"class":318,"line":326},[316,859,373],{"class":340},[316,861,358],{"class":357},[316,863,864],{"class":333},"read:*,write:orders",[316,866,358],{"class":357},[316,868,367],{"class":366},[316,870,871,874,876,879,881],{"class":318,"line":370},[316,872,873],{"class":340},"  --ips=",[316,875,358],{"class":357},[316,877,878],{"class":333},"203.0.113.10,198.51.100.0/24",[316,880,358],{"class":357},[316,882,367],{"class":366},[316,884,885,888,890,893],{"class":318,"line":386},[316,886,887],{"class":340},"  --expires=",[316,889,358],{"class":357},[316,891,892],{"class":333},"+90 days",[316,894,895],{"class":357},"\"\n",[589,897,898,908],{},[592,899,900],{},[595,901,902,905],{},[598,903,904],{},"Option",[598,906,907],{},"Description",[605,909,910,920,930,940,950],{},[595,911,912,917],{},[610,913,914],{},[293,915,916],{},"--user",[610,918,919],{},"User UUID (required)",[595,921,922,927],{},[610,923,924],{},[293,925,926],{},"--name",[610,928,929],{},"Label (required)",[595,931,932,937],{},[610,933,934],{},[293,935,936],{},"--scopes",[610,938,939],{},"Comma-separated scopes; omit for an unrestricted key",[595,941,942,947],{},[610,943,944],{},[293,945,946],{},"--ips",[610,948,949],{},"Comma-separated IPs or IPv4 CIDR ranges",[595,951,952,957],{},[610,953,954],{},[293,955,956],{},"--expires",[610,958,959,960,963,964,760,967,760,970],{},"Anything ",[293,961,962],{},"strtotime()"," accepts: ",[293,965,966],{},"2027-01-01",[293,968,969],{},"+1year",[293,971,892],{},[289,973,974],{},"The command prints the plaintext key once, with the key's UUID. Store the plaintext immediately.",[820,976,978],{"id":977},"in-code","In code",[289,980,981,984],{},[293,982,983],{},"Glueful\\Auth\\ApiKey\\ApiKeyService"," is a static, context-first service:",[307,986,988],{"className":419,"code":987,"language":330,"meta":312,"style":312},"use Glueful\\Auth\\ApiKey\\ApiKeyService;\n\n$result = ApiKeyService::create($context, [\n    'user_uuid'   => $user->uuid(),\n    'name'        => 'Partner sync',\n    'scopes'      => ['read:*', 'write:orders'],     // null = unrestricted\n    'allowed_ips' => ['198.51.100.0/24'],             // null = any IP\n    'expires_at'  => date('Y-m-d H:i:s', strtotime('+90 days')), // null = never\n]);\n\n$plain = $result['plain']; // show to the user once\n$key   = $result['key'];   // Glueful\\Auth\\ApiKey\\ApiKey model (uuid, name, scopes, ...)\n",[293,989,990,1014,1018,1049,1075,1096,1132,1157,1201,1207,1212,1243],{"__ignoreMap":312},[316,991,992,994,996,998,1001,1003,1006,1008,1011],{"class":318,"line":319},[316,993,433],{"class":432},[316,995,437],{"class":436},[316,997,441],{"class":440},[316,999,1000],{"class":436},"Auth",[316,1002,441],{"class":440},[316,1004,1005],{"class":436},"ApiKey",[316,1007,441],{"class":440},[316,1009,1010],{"class":457},"ApiKeyService",[316,1012,1013],{"class":453},";\n",[316,1015,1016],{"class":318,"line":326},[316,1017,390],{"emptyLinePlaceholder":389},[316,1019,1020,1023,1026,1029,1032,1035,1038,1041,1044,1046],{"class":318,"line":370},[316,1021,1022],{"class":453},"$",[316,1024,1025],{"class":481},"result ",[316,1027,1028],{"class":344},"=",[316,1030,1031],{"class":563}," ApiKeyService",[316,1033,1034],{"class":344},"::",[316,1036,1037],{"class":553},"create",[316,1039,1040],{"class":453},"($",[316,1042,1043],{"class":481},"context",[316,1045,461],{"class":453},[316,1047,1048],{"class":453}," [\n",[316,1050,1051,1054,1056,1058,1061,1064,1067,1070,1072],{"class":318,"line":386},[316,1052,1053],{"class":357},"    '",[316,1055,737],{"class":333},[316,1057,490],{"class":357},[316,1059,1060],{"class":344},"   =>",[316,1062,1063],{"class":453}," $",[316,1065,1066],{"class":481},"user",[316,1068,1069],{"class":344},"->",[316,1071,727],{"class":553},[316,1073,1074],{"class":453},"(),\n",[316,1076,1077,1079,1081,1083,1086,1089,1091,1093],{"class":318,"line":393},[316,1078,1053],{"class":357},[316,1080,750],{"class":333},[316,1082,490],{"class":357},[316,1084,1085],{"class":344},"        =>",[316,1087,1088],{"class":357}," '",[316,1090,851],{"class":333},[316,1092,490],{"class":357},[316,1094,1095],{"class":453},",\n",[316,1097,1098,1100,1102,1104,1107,1110,1112,1115,1117,1119,1121,1124,1126,1129],{"class":318,"line":399},[316,1099,1053],{"class":357},[316,1101,775],{"class":333},[316,1103,490],{"class":357},[316,1105,1106],{"class":344},"      =>",[316,1108,1109],{"class":453}," [",[316,1111,490],{"class":357},[316,1113,1114],{"class":333},"read:*",[316,1116,490],{"class":357},[316,1118,461],{"class":453},[316,1120,1088],{"class":357},[316,1122,1123],{"class":333},"write:orders",[316,1125,490],{"class":357},[316,1127,1128],{"class":453},"],",[316,1130,1131],{"class":322},"     // null = unrestricted\n",[316,1133,1134,1136,1138,1140,1143,1145,1147,1150,1152,1154],{"class":318,"line":542},[316,1135,1053],{"class":357},[316,1137,778],{"class":333},[316,1139,490],{"class":357},[316,1141,1142],{"class":344}," =>",[316,1144,1109],{"class":453},[316,1146,490],{"class":357},[316,1148,1149],{"class":333},"198.51.100.0/24",[316,1151,490],{"class":357},[316,1153,1128],{"class":453},[316,1155,1156],{"class":322},"             // null = any IP\n",[316,1158,1160,1162,1164,1166,1169,1173,1175,1177,1180,1182,1184,1187,1189,1191,1193,1195,1198],{"class":318,"line":1159},8,[316,1161,1053],{"class":357},[316,1163,792],{"class":333},[316,1165,490],{"class":357},[316,1167,1168],{"class":344},"  =>",[316,1170,1172],{"class":1171},"sMLJd"," date",[316,1174,487],{"class":453},[316,1176,490],{"class":357},[316,1178,1179],{"class":333},"Y-m-d H:i:s",[316,1181,490],{"class":357},[316,1183,461],{"class":453},[316,1185,1186],{"class":1171}," strtotime",[316,1188,487],{"class":453},[316,1190,490],{"class":357},[316,1192,892],{"class":333},[316,1194,490],{"class":357},[316,1196,1197],{"class":453},")),",[316,1199,1200],{"class":322}," // null = never\n",[316,1202,1204],{"class":318,"line":1203},9,[316,1205,1206],{"class":453},"]);\n",[316,1208,1210],{"class":318,"line":1209},10,[316,1211,390],{"emptyLinePlaceholder":389},[316,1213,1215,1217,1220,1222,1224,1227,1230,1232,1235,1237,1240],{"class":318,"line":1214},11,[316,1216,1022],{"class":453},[316,1218,1219],{"class":481},"plain ",[316,1221,1028],{"class":344},[316,1223,1063],{"class":453},[316,1225,1226],{"class":481},"result",[316,1228,1229],{"class":453},"[",[316,1231,490],{"class":357},[316,1233,1234],{"class":333},"plain",[316,1236,490],{"class":357},[316,1238,1239],{"class":453},"];",[316,1241,1242],{"class":322}," // show to the user once\n",[316,1244,1246,1248,1251,1253,1255,1257,1259,1261,1264,1266,1268],{"class":318,"line":1245},12,[316,1247,1022],{"class":453},[316,1249,1250],{"class":481},"key   ",[316,1252,1028],{"class":344},[316,1254,1063],{"class":453},[316,1256,1226],{"class":481},[316,1258,1229],{"class":453},[316,1260,490],{"class":357},[316,1262,1263],{"class":333},"key",[316,1265,490],{"class":357},[316,1267,1239],{"class":453},[316,1269,1270],{"class":322},"   // Glueful\\Auth\\ApiKey\\ApiKey model (uuid, name, scopes, ...)\n",[289,1272,1273,1274,1277,1278,1281,1282,1284],{},"Creating, rotating and revoking a key each dispatch a generic ",[293,1275,1276],{},"EntityCreatedEvent"," / ",[293,1279,1280],{},"EntityUpdatedEvent"," for the ",[293,1283,295],{}," table, which an audit extension can record. The payload has the key's identity, scopes, IPs and timestamps, never the plaintext or the hash. If dispatching fails, the key operation still succeeds.",[302,1286,1288],{"id":1287},"listing-keys","Listing Keys",[307,1290,1292],{"className":309,"code":1291,"language":311,"meta":312,"style":312},"php glueful apikey:list --user=\u003Cuser-uuid>\n",[293,1293,1294],{"__ignoreMap":312},[316,1295,1296,1298,1300,1303,1305,1307,1309],{"class":318,"line":319},[316,1297,330],{"class":329},[316,1299,334],{"class":333},[316,1301,1302],{"class":333}," apikey:list",[316,1304,341],{"class":340},[316,1306,345],{"class":344},[316,1308,348],{"class":340},[316,1310,1311],{"class":344},">\n",[307,1313,1315],{"className":419,"code":1314,"language":330,"meta":312,"style":312},"$keys = ApiKeyService::forUser($context, $userUuid); // list\u003CApiKey>\n\nforeach ($keys as $key) {\n    $key->getScopes();     // list\u003Cstring>, [] = unrestricted\n    $key->getAllowedIps(); // list\u003Cstring>, [] = any IP\n    $key->isExpired();\n    $key->isRevoked();\n}\n",[293,1316,1317,1350,1354,1377,1395,1411,1425,1438],{"__ignoreMap":312},[316,1318,1319,1321,1324,1326,1328,1330,1333,1335,1337,1339,1341,1344,1347],{"class":318,"line":319},[316,1320,1022],{"class":453},[316,1322,1323],{"class":481},"keys ",[316,1325,1028],{"class":344},[316,1327,1031],{"class":563},[316,1329,1034],{"class":344},[316,1331,1332],{"class":553},"forUser",[316,1334,1040],{"class":453},[316,1336,1043],{"class":481},[316,1338,461],{"class":453},[316,1340,1063],{"class":453},[316,1342,1343],{"class":481},"userUuid",[316,1345,1346],{"class":453},");",[316,1348,1349],{"class":322}," // list\u003CApiKey>\n",[316,1351,1352],{"class":318,"line":326},[316,1353,390],{"emptyLinePlaceholder":389},[316,1355,1356,1360,1363,1365,1368,1370,1372,1374],{"class":318,"line":370},[316,1357,1359],{"class":1358},"sRxSC","foreach",[316,1361,1362],{"class":453}," ($",[316,1364,1323],{"class":481},[316,1366,1367],{"class":344},"as",[316,1369,1063],{"class":453},[316,1371,1263],{"class":481},[316,1373,498],{"class":453},[316,1375,1376],{"class":453}," {\n",[316,1378,1379,1382,1384,1386,1389,1392],{"class":318,"line":386},[316,1380,1381],{"class":453},"    $",[316,1383,1263],{"class":481},[316,1385,1069],{"class":344},[316,1387,1388],{"class":553},"getScopes",[316,1390,1391],{"class":453},"();",[316,1393,1394],{"class":322},"     // list\u003Cstring>, [] = unrestricted\n",[316,1396,1397,1399,1401,1403,1406,1408],{"class":318,"line":393},[316,1398,1381],{"class":453},[316,1400,1263],{"class":481},[316,1402,1069],{"class":344},[316,1404,1405],{"class":553},"getAllowedIps",[316,1407,1391],{"class":453},[316,1409,1410],{"class":322}," // list\u003Cstring>, [] = any IP\n",[316,1412,1413,1415,1417,1419,1422],{"class":318,"line":399},[316,1414,1381],{"class":453},[316,1416,1263],{"class":481},[316,1418,1069],{"class":344},[316,1420,1421],{"class":553},"isExpired",[316,1423,1424],{"class":453},"();\n",[316,1426,1427,1429,1431,1433,1436],{"class":318,"line":542},[316,1428,1381],{"class":453},[316,1430,1263],{"class":481},[316,1432,1069],{"class":344},[316,1434,1435],{"class":553},"isRevoked",[316,1437,1424],{"class":453},[316,1439,1440],{"class":318,"line":1159},[316,1441,1442],{"class":453},"}\n",[302,1444,1446],{"id":1445},"rotating-keys","Rotating Keys",[289,1448,1449],{},"Rotation mints a successor and keeps the old key working for a grace period so clients can switch without downtime:",[307,1451,1453],{"className":309,"code":1452,"language":311,"meta":312,"style":312},"php glueful apikey:rotate \u003Ckey-uuid> --grace=48   # hours, default 24\n",[293,1454,1455],{"__ignoreMap":312},[316,1456,1457,1459,1461,1464,1467,1470,1473,1475,1478],{"class":318,"line":319},[316,1458,330],{"class":329},[316,1460,334],{"class":333},[316,1462,1463],{"class":333}," apikey:rotate",[316,1465,1466],{"class":344}," \u003C",[316,1468,1469],{"class":333},"key-uui",[316,1471,1472],{"class":481},"d",[316,1474,351],{"class":344},[316,1476,1477],{"class":340}," --grace=48",[316,1479,1480],{"class":322},"   # hours, default 24\n",[307,1482,1484],{"className":419,"code":1483,"language":330,"meta":312,"style":312},"use Glueful\\Auth\\ApiKey\\ApiKey;\nuse Glueful\\Auth\\ApiKey\\ApiKeyService;\n\n$existing = ApiKey::query($context)->where('uuid', '=', $uuid)->first();\n\n$rotation = ApiKeyService::rotate($context, $existing, graceHours: 48);\n// ['old_uuid' => ..., 'new_uuid' => ..., 'new_plain' => 'gf_live_...', 'old_expires_at' => '...']\n",[293,1485,1486,1506,1526,1530,1589,1593,1635],{"__ignoreMap":312},[316,1487,1488,1490,1492,1494,1496,1498,1500,1502,1504],{"class":318,"line":319},[316,1489,433],{"class":432},[316,1491,437],{"class":436},[316,1493,441],{"class":440},[316,1495,1000],{"class":436},[316,1497,441],{"class":440},[316,1499,1005],{"class":436},[316,1501,441],{"class":440},[316,1503,1005],{"class":457},[316,1505,1013],{"class":453},[316,1507,1508,1510,1512,1514,1516,1518,1520,1522,1524],{"class":318,"line":326},[316,1509,433],{"class":432},[316,1511,437],{"class":436},[316,1513,441],{"class":440},[316,1515,1000],{"class":436},[316,1517,441],{"class":440},[316,1519,1005],{"class":436},[316,1521,441],{"class":440},[316,1523,1010],{"class":457},[316,1525,1013],{"class":453},[316,1527,1528],{"class":318,"line":370},[316,1529,390],{"emptyLinePlaceholder":389},[316,1531,1532,1534,1537,1539,1542,1544,1547,1549,1551,1553,1555,1558,1560,1562,1564,1566,1568,1570,1572,1574,1576,1578,1580,1582,1584,1587],{"class":318,"line":386},[316,1533,1022],{"class":453},[316,1535,1536],{"class":481},"existing ",[316,1538,1028],{"class":344},[316,1540,1541],{"class":563}," ApiKey",[316,1543,1034],{"class":344},[316,1545,1546],{"class":553},"query",[316,1548,1040],{"class":453},[316,1550,1043],{"class":481},[316,1552,498],{"class":453},[316,1554,1069],{"class":344},[316,1556,1557],{"class":553},"where",[316,1559,487],{"class":453},[316,1561,490],{"class":357},[316,1563,727],{"class":333},[316,1565,490],{"class":357},[316,1567,461],{"class":453},[316,1569,1088],{"class":357},[316,1571,1028],{"class":333},[316,1573,490],{"class":357},[316,1575,461],{"class":453},[316,1577,1063],{"class":453},[316,1579,727],{"class":481},[316,1581,498],{"class":453},[316,1583,1069],{"class":344},[316,1585,1586],{"class":553},"first",[316,1588,1424],{"class":453},[316,1590,1591],{"class":318,"line":393},[316,1592,390],{"emptyLinePlaceholder":389},[316,1594,1595,1597,1600,1602,1604,1606,1609,1611,1613,1615,1617,1620,1622,1626,1628,1632],{"class":318,"line":399},[316,1596,1022],{"class":453},[316,1598,1599],{"class":481},"rotation ",[316,1601,1028],{"class":344},[316,1603,1031],{"class":563},[316,1605,1034],{"class":344},[316,1607,1608],{"class":553},"rotate",[316,1610,1040],{"class":453},[316,1612,1043],{"class":481},[316,1614,461],{"class":453},[316,1616,1063],{"class":453},[316,1618,1619],{"class":481},"existing",[316,1621,461],{"class":453},[316,1623,1625],{"class":1624},"sQqfL"," graceHours",[316,1627,560],{"class":453},[316,1629,1631],{"class":1630},"sYThS"," 48",[316,1633,1634],{"class":453},");\n",[316,1636,1637],{"class":318,"line":542},[316,1638,1639],{"class":322},"// ['old_uuid' => ..., 'new_uuid' => ..., 'new_plain' => 'gf_live_...', 'old_expires_at' => '...']\n",[289,1641,1642,1643,1646,1647,1649,1650,1654,1655,1658],{},"The successor copies the scopes, IP allowlist and expiry of the old key, gets the name ",[293,1644,1645],{},"\u003Cname> (rotated)",", and records ",[293,1648,805],{},". The old key's expiry becomes the ",[1651,1652,1653],"strong",{},"earlier"," of its current expiry and ",[293,1656,1657],{},"now + grace",", so rotation never extends a key's life.",[302,1660,1662],{"id":1661},"revoking-keys","Revoking Keys",[307,1664,1666],{"className":309,"code":1665,"language":311,"meta":312,"style":312},"php glueful apikey:revoke \u003Ckey-uuid>\n",[293,1667,1668],{"__ignoreMap":312},[316,1669,1670,1672,1674,1677,1679,1681,1683],{"class":318,"line":319},[316,1671,330],{"class":329},[316,1673,334],{"class":333},[316,1675,1676],{"class":333}," apikey:revoke",[316,1678,1466],{"class":344},[316,1680,1469],{"class":333},[316,1682,1472],{"class":481},[316,1684,1311],{"class":344},[307,1686,1688],{"className":419,"code":1687,"language":330,"meta":312,"style":312},"ApiKeyService::revoke($context, $key); // sets revoked_at, effective immediately\n",[293,1689,1690],{"__ignoreMap":312},[316,1691,1692,1694,1696,1699,1701,1703,1705,1707,1709,1711],{"class":318,"line":319},[316,1693,1010],{"class":563},[316,1695,1034],{"class":344},[316,1697,1698],{"class":553},"revoke",[316,1700,1040],{"class":453},[316,1702,1043],{"class":481},[316,1704,461],{"class":453},[316,1706,1063],{"class":453},[316,1708,1263],{"class":481},[316,1710,1346],{"class":453},[316,1712,1713],{"class":322}," // sets revoked_at, effective immediately\n",[302,1715,1717],{"id":1716},"sending-the-key","Sending the Key",[289,1719,1720],{},"Clients send the key in one of these places, checked in order:",[1722,1723,1724,1730,1747],"ol",{},[1725,1726,1727,1729],"li",{},[293,1728,411],{}," header (preferred)",[1725,1731,1732,1735,1736,1739,1740,1743,1744],{},[293,1733,1734],{},"?api_key=gf_live_..."," query parameter, ",[1651,1737,1738],{},"only"," when ",[293,1741,1742],{},"security.api_keys.allow_query_param"," is ",[293,1745,1746],{},"true",[1725,1748,1749,1752],{},[293,1750,1751],{},"Authorization: ApiKey gf_live_..."," header",[289,1754,1755,1756,1759,1760,1763],{},"The query parameter is off by default because URLs end up in access logs, proxies and ",[293,1757,1758],{},"Referer"," headers. The shipped ",[293,1761,1762],{},"config/security.php"," carries the switch, driven by an env var:",[307,1765,1767],{"className":419,"code":1766,"language":330,"meta":312,"style":312},"// config/security.php\n'api_keys' => [\n    'allow_query_param' => env('API_KEY_ALLOW_QUERY_PARAM', false), // true only if a client truly can't send headers\n],\n",[293,1768,1769,1774,1786,1821],{"__ignoreMap":312},[316,1770,1771],{"class":318,"line":319},[316,1772,1773],{"class":322},"// config/security.php\n",[316,1775,1776,1778,1780,1782,1784],{"class":318,"line":326},[316,1777,490],{"class":357},[316,1779,295],{"class":333},[316,1781,490],{"class":357},[316,1783,1142],{"class":344},[316,1785,1048],{"class":453},[316,1787,1788,1790,1793,1795,1797,1800,1802,1804,1807,1809,1811,1815,1818],{"class":318,"line":370},[316,1789,1053],{"class":357},[316,1791,1792],{"class":333},"allow_query_param",[316,1794,490],{"class":357},[316,1796,1142],{"class":344},[316,1798,1799],{"class":553}," env",[316,1801,487],{"class":453},[316,1803,490],{"class":357},[316,1805,1806],{"class":333},"API_KEY_ALLOW_QUERY_PARAM",[316,1808,490],{"class":357},[316,1810,461],{"class":453},[316,1812,1814],{"class":1813},"sMTiH"," false",[316,1816,1817],{"class":453},"),",[316,1819,1820],{"class":322}," // true only if a client truly can't send headers\n",[316,1822,1823],{"class":318,"line":386},[316,1824,1825],{"class":453},"],\n",[307,1827,1829],{"className":667,"code":1828,"language":669,"meta":312,"style":312},"API_KEY_ALLOW_QUERY_PARAM=true\n",[293,1830,1831],{"__ignoreMap":312},[316,1832,1833],{"class":318,"line":319},[316,1834,1828],{},[289,1836,1837,1840,1841,1843],{},[1651,1838,1839],{},"Since 1.91",", the block is in the shipped config. An app that added its own ",[293,1842,295],{}," block keeps working; the env var is now the simpler switch.",[289,1845,1846],{},"On success the request carries these attributes:",[589,1848,1849,1858],{},[592,1850,1851],{},[595,1852,1853,1856],{},[598,1854,1855],{},"Attribute",[598,1857,603],{},[605,1859,1860,1872,1882,1892],{},[595,1861,1862,1867],{},[610,1863,1864],{},[293,1865,1866],{},"auth_method",[610,1868,1869],{},[293,1870,1871],{},"api_key",[595,1873,1874,1879],{},[610,1875,1876],{},[293,1877,1878],{},"user_id",[610,1880,1881],{},"The key owner's uuid",[595,1883,1884,1889],{},[610,1885,1886],{},[293,1887,1888],{},"api_key_uuid",[610,1890,1891],{},"The key's own uuid (for audit or rate-limit keying)",[595,1893,1894,1899],{},[610,1895,1896],{},[293,1897,1898],{},"api_key_scopes",[610,1900,1901,1902,1905],{},"The key's scopes (",[293,1903,1904],{},"[]"," = unrestricted)",[289,1907,1908],{},"A missing, unknown, revoked, expired or IP-blocked key fails authentication with a generic error. The response doesn't say which check failed.",[302,1910,1912],{"id":1911},"ip-allowlists","IP Allowlists",[289,1914,1915,1917],{},[293,1916,778],{}," takes exact addresses and IPv4 CIDR ranges:",[307,1919,1921],{"className":419,"code":1920,"language":330,"meta":312,"style":312},"'allowed_ips' => ['203.0.113.10', '10.0.0.0/8'],\n",[293,1922,1923],{"__ignoreMap":312},[316,1924,1925,1927,1929,1931,1933,1935,1937,1940,1942,1944,1946,1949,1951],{"class":318,"line":319},[316,1926,490],{"class":357},[316,1928,778],{"class":333},[316,1930,490],{"class":357},[316,1932,1142],{"class":344},[316,1934,1109],{"class":453},[316,1936,490],{"class":357},[316,1938,1939],{"class":333},"203.0.113.10",[316,1941,490],{"class":357},[316,1943,461],{"class":453},[316,1945,1088],{"class":357},[316,1947,1948],{"class":333},"10.0.0.0/8",[316,1950,490],{"class":357},[316,1952,1825],{"class":453},[289,1954,1955,1956,1959],{},"Matching is fail-closed: a malformed entry matches nothing. IPv6 works only as an exact address; IPv6 CIDR ranges never match. The client address comes from ",[293,1957,1958],{},"Request::getClientIp()",", so behind a load balancer configure trusted proxies, or every request will appear to come from the proxy.",[302,1961,1963],{"id":1962},"expiry","Expiry",[289,1965,1966,1967,1969,1970,1972,1973,1975],{},"A key past ",[293,1968,792],{}," is rejected. Keys with no ",[293,1971,792],{}," never expire. Use ",[293,1974,956],{}," on creation for keys handed to third parties, and rotate before the date arrives.",[302,1977,1978],{"id":775},"Scopes",[289,1980,1981,1982,1985,1986,584,1988,1990,1991,1994,1995,1998,1999,2002],{},"Scopes are free-form strings. The usual convention is ",[293,1983,1984],{},"action:resource",", for example ",[293,1987,533],{},[293,1989,1123],{},". A key's granted scopes are matched against a route's required scope with ",[293,1992,1993],{},"fnmatch()",", so ",[293,1996,1997],{},"*"," wildcards work on the ",[1651,2000,2001],{},"granted"," side:",[589,2004,2005,2018],{},[592,2006,2007],{},[595,2008,2009,2012,2015],{},[598,2010,2011],{},"Granted",[598,2013,2014],{},"Required",[598,2016,2017],{},"Passes?",[605,2019,2020,2033,2045,2056,2070],{},[595,2021,2022,2026,2030],{},[610,2023,2024],{},[293,2025,533],{},[610,2027,2028],{},[293,2029,533],{},[610,2031,2032],{},"Yes",[595,2034,2035,2039,2043],{},[610,2036,2037],{},[293,2038,1114],{},[610,2040,2041],{},[293,2042,533],{},[610,2044,2032],{},[595,2046,2047,2051,2054],{},[610,2048,2049],{},[293,2050,1997],{},[610,2052,2053],{},"anything",[610,2055,2032],{},[595,2057,2058,2062,2067],{},[610,2059,2060],{},[293,2061,533],{},[610,2063,2064],{},[293,2065,2066],{},"write:posts",[610,2068,2069],{},"No",[595,2071,2072,2078,2080],{},[610,2073,2074],{},[2075,2076,2077],"em",{},"(no scopes)",[610,2079,2053],{},[610,2081,2082],{},"Yes: an unrestricted key",[820,2084,2086,2087],{"id":2085},"requiring-scopes-with-requirescope","Requiring scopes with ",[293,2088,2089],{},"#[RequireScope]",[289,2091,2092,2094,2095,2098,2099,2101],{},[293,2093,2089],{}," declares the scopes a route needs and auto-attaches the ",[293,2096,2097],{},"require_scope"," middleware. It doesn't authenticate on its own, so the route also needs ",[293,2100,299],{},".",[2103,2104,2105,2110],"ul",{},[1725,2106,2107],{},[1651,2108,2109],{},"One attribute, several scopes = any of them.",[1725,2111,2112,2115],{},[1651,2113,2114],{},"Stacked attributes = all of them."," Each attribute must pass independently.",[307,2117,2119],{"className":419,"code":2118,"language":330,"meta":312,"style":312},"use Glueful\\Routing\\Attributes\\{Get, Post, Middleware, RequireScope};\n\n#[Middleware('auth')]\nfinal class PostController\n{\n    #[Get('/posts')]\n    #[RequireScope('read:posts')]\n    public function index(): Response { /* ... */ }\n\n    // Needs (write:posts OR admin:posts) AND publish:posts\n    #[Post('/posts/{id}/publish')]\n    #[RequireScope(['write:posts', 'admin:posts'])]\n    #[RequireScope('publish:posts')]\n    public function publish(string $id): Response { /* ... */ }\n}\n",[293,2120,2121,2156,2160,2178,2190,2195,2214,2232,2253,2257,2262,2282,2311,2331,2363],{"__ignoreMap":312},[316,2122,2123,2125,2127,2129,2131,2133,2135,2137,2139,2141,2143,2146,2148,2150,2152,2154],{"class":318,"line":319},[316,2124,433],{"class":432},[316,2126,437],{"class":436},[316,2128,441],{"class":440},[316,2130,29],{"class":436},[316,2132,441],{"class":440},[316,2134,448],{"class":436},[316,2136,441],{"class":440},[316,2138,454],{"class":453},[316,2140,458],{"class":457},[316,2142,461],{"class":453},[316,2144,2145],{"class":457}," Post",[316,2147,461],{"class":453},[316,2149,464],{"class":457},[316,2151,461],{"class":453},[316,2153,469],{"class":457},[316,2155,472],{"class":453},[316,2157,2158],{"class":318,"line":326},[316,2159,390],{"emptyLinePlaceholder":389},[316,2161,2162,2164,2166,2168,2170,2172,2174,2176],{"class":318,"line":370},[316,2163,482],{"class":481},[316,2165,145],{"class":436},[316,2167,487],{"class":453},[316,2169,490],{"class":357},[316,2171,299],{"class":333},[316,2173,490],{"class":357},[316,2175,498],{"class":453},[316,2177,501],{"class":481},[316,2179,2180,2183,2186],{"class":318,"line":386},[316,2181,2182],{"class":545},"final",[316,2184,2185],{"class":549}," class",[316,2187,2189],{"class":2188},"sKvfc"," PostController\n",[316,2191,2192],{"class":318,"line":393},[316,2193,2194],{"class":453},"{\n",[316,2196,2197,2200,2202,2204,2206,2208,2210,2212],{"class":318,"line":399},[316,2198,2199],{"class":481},"    #[",[316,2201,458],{"class":436},[316,2203,487],{"class":453},[316,2205,490],{"class":357},[316,2207,493],{"class":333},[316,2209,490],{"class":357},[316,2211,498],{"class":453},[316,2213,501],{"class":481},[316,2215,2216,2218,2220,2222,2224,2226,2228,2230],{"class":318,"line":542},[316,2217,2199],{"class":481},[316,2219,526],{"class":436},[316,2221,487],{"class":453},[316,2223,490],{"class":357},[316,2225,533],{"class":333},[316,2227,490],{"class":357},[316,2229,498],{"class":453},[316,2231,501],{"class":481},[316,2233,2234,2237,2239,2241,2243,2245,2247,2249,2251],{"class":318,"line":1159},[316,2235,2236],{"class":545},"    public",[316,2238,550],{"class":549},[316,2240,554],{"class":553},[316,2242,557],{"class":453},[316,2244,560],{"class":344},[316,2246,564],{"class":563},[316,2248,567],{"class":453},[316,2250,570],{"class":322},[316,2252,573],{"class":453},[316,2254,2255],{"class":318,"line":1203},[316,2256,390],{"emptyLinePlaceholder":389},[316,2258,2259],{"class":318,"line":1209},[316,2260,2261],{"class":322},"    // Needs (write:posts OR admin:posts) AND publish:posts\n",[316,2263,2264,2266,2269,2271,2273,2276,2278,2280],{"class":318,"line":1214},[316,2265,2199],{"class":481},[316,2267,2268],{"class":436},"Post",[316,2270,487],{"class":453},[316,2272,490],{"class":357},[316,2274,2275],{"class":333},"/posts/{id}/publish",[316,2277,490],{"class":357},[316,2279,498],{"class":453},[316,2281,501],{"class":481},[316,2283,2284,2286,2288,2291,2293,2295,2297,2299,2301,2304,2306,2309],{"class":318,"line":1245},[316,2285,2199],{"class":481},[316,2287,526],{"class":436},[316,2289,2290],{"class":453},"([",[316,2292,490],{"class":357},[316,2294,2066],{"class":333},[316,2296,490],{"class":357},[316,2298,461],{"class":453},[316,2300,1088],{"class":357},[316,2302,2303],{"class":333},"admin:posts",[316,2305,490],{"class":357},[316,2307,2308],{"class":453},"])",[316,2310,501],{"class":481},[316,2312,2314,2316,2318,2320,2322,2325,2327,2329],{"class":318,"line":2313},13,[316,2315,2199],{"class":481},[316,2317,526],{"class":436},[316,2319,487],{"class":453},[316,2321,490],{"class":357},[316,2323,2324],{"class":333},"publish:posts",[316,2326,490],{"class":357},[316,2328,498],{"class":453},[316,2330,501],{"class":481},[316,2332,2334,2336,2338,2341,2343,2346,2348,2351,2353,2355,2357,2359,2361],{"class":318,"line":2333},14,[316,2335,2236],{"class":545},[316,2337,550],{"class":549},[316,2339,2340],{"class":553}," publish",[316,2342,487],{"class":453},[316,2344,2345],{"class":432},"string",[316,2347,1063],{"class":453},[316,2349,2350],{"class":481},"id",[316,2352,498],{"class":453},[316,2354,560],{"class":344},[316,2356,564],{"class":563},[316,2358,567],{"class":453},[316,2360,570],{"class":322},[316,2362,573],{"class":453},[316,2364,2366],{"class":318,"line":2365},15,[316,2367,1442],{"class":453},[289,2369,2370],{},"For file-defined routes, pass the scopes as middleware parameters. They form a single any-of group:",[307,2372,2374],{"className":419,"code":2373,"language":330,"meta":312,"style":312},"$router->get('/reports', [ReportController::class, 'index'])\n    ->middleware(['auth', 'require_scope:read:reports,admin:reports']);\n",[293,2375,2376,2421],{"__ignoreMap":312},[316,2377,2378,2380,2383,2385,2388,2390,2392,2395,2397,2399,2401,2404,2406,2409,2411,2413,2416,2418],{"class":318,"line":319},[316,2379,1022],{"class":453},[316,2381,2382],{"class":481},"router",[316,2384,1069],{"class":344},[316,2386,2387],{"class":553},"get",[316,2389,487],{"class":453},[316,2391,490],{"class":357},[316,2393,2394],{"class":333},"/reports",[316,2396,490],{"class":357},[316,2398,461],{"class":453},[316,2400,1109],{"class":453},[316,2402,2403],{"class":563},"ReportController",[316,2405,1034],{"class":344},[316,2407,2408],{"class":432},"class",[316,2410,461],{"class":453},[316,2412,1088],{"class":357},[316,2414,2415],{"class":333},"index",[316,2417,490],{"class":357},[316,2419,2420],{"class":453},"])\n",[316,2422,2423,2426,2429,2431,2433,2435,2437,2439,2441,2444,2446],{"class":318,"line":326},[316,2424,2425],{"class":344},"    ->",[316,2427,2428],{"class":553},"middleware",[316,2430,2290],{"class":453},[316,2432,490],{"class":357},[316,2434,299],{"class":333},[316,2436,490],{"class":357},[316,2438,461],{"class":453},[316,2440,1088],{"class":357},[316,2442,2443],{"class":333},"require_scope:read:reports,admin:reports",[316,2445,490],{"class":357},[316,2447,1206],{"class":453},[289,2449,2450,2451,618,2454,2457,2458,2461],{},"A request that fails the check gets ",[293,2452,2453],{},"403",[293,2455,2456],{},"Glueful\\Auth\\ApiKey\\Exceptions\\InsufficientScopeException",") with the message ",[293,2459,2460],{},"Insufficient scope: required any of [...]",". An unauthenticated request is denied too: a missing credential never counts as full access.",[2463,2464,2468],"u-alert",{"color":2465,"icon":2466,"variant":2467},"warning","i-lucide-triangle-alert","subtle",[289,2469,2470,2471,2474,2475,2477,2478,2480,2481,2484],{},"Run ",[293,2472,2473],{},"php glueful route:cache:clear"," when you upgrade to 1.91. Before 1.91, routes loaded from the route cache lost their ",[293,2476,2089],{}," configuration, so ",[293,2479,2097],{}," saw no requirement and ",[1651,2482,2483],{},"let requests through",". The cache format is now v3, and older cache files are also discarded on the next boot.",[302,2486,2488],{"id":2487},"scopes-on-jwt-requests","Scopes on JWT Requests",[289,2490,2491,2494,2495,2497,2498,2494,2501,2504],{},[1651,2492,2493],{},"New in 1.91."," ",[293,2496,2089],{}," used to reject every JWT, so scoped routes in practice required an API key. JWT requests can now pass when the authenticated identity (the ",[293,2499,2500],{},"auth.user",[293,2502,2503],{},"UserIdentity",") carries the scopes. The same wildcard matcher and the same any-of/all-of rules apply.",[289,2506,2507],{},"The identity's scopes come from:",[2103,2509,2510,2532,2538],{},[1725,2511,2512,2513,2515,2516,2519,2520,2523,2524,2527,2528,2531],{},"the token's ",[293,2514,775],{}," claim, or its ",[293,2517,2518],{},"scope"," claim, as either an array or an OAuth-style space-delimited string (",[293,2521,2522],{},"\"read:posts write:posts\"",", RFC 6749 §3.3). Both forms are normalised (",[293,2525,2526],{},"UserIdentity::normalizeScopes()",") wherever the identity is built, ",[293,2529,2530],{},"AuthToRequestAttributesMiddleware"," included",[1725,2533,2534,2535,2537],{},"a ",[293,2536,775],{}," value on the user record",[1725,2539,2540,2541,2544,2545,498],{},"any ",[293,2542,2543],{},"identity.claims_provider",", which can add scopes after login (see ",[2546,2547,2548],"a",{"href":42},"Identity",[289,2550,2551],{},"Here is a claims provider that grants scopes by role:",[307,2553,2555],{"className":419,"code":2554,"language":330,"meta":312,"style":312},"use Glueful\\Auth\\Contracts\\IdentityClaimsProviderInterface;\nuse Glueful\\Auth\\UserIdentity;\n\nfinal class EditorScopes implements IdentityClaimsProviderInterface\n{\n    public function enrich(UserIdentity $identity): UserIdentity\n    {\n        if (!in_array('editor', $identity->roles(), true)) {\n            return $identity;\n        }\n\n        return $identity->withClaims([\n            'scopes' => [...$identity->scopes(), 'read:posts', 'write:posts'],\n        ]);\n    }\n}\n\n// In your provider's services():\n// EditorScopes::class => ['class' => EditorScopes::class, 'shared' => true,\n//                         'tags' => ['identity.claims_provider']],\n",[293,2556,2557,2579,2595,2599,2615,2619,2644,2649,2693,2704,2709,2713,2730,2772,2777,2782,2787,2792,2798,2804],{"__ignoreMap":312},[316,2558,2559,2561,2563,2565,2567,2569,2572,2574,2577],{"class":318,"line":319},[316,2560,433],{"class":432},[316,2562,437],{"class":436},[316,2564,441],{"class":440},[316,2566,1000],{"class":436},[316,2568,441],{"class":440},[316,2570,2571],{"class":436},"Contracts",[316,2573,441],{"class":440},[316,2575,2576],{"class":457},"IdentityClaimsProviderInterface",[316,2578,1013],{"class":453},[316,2580,2581,2583,2585,2587,2589,2591,2593],{"class":318,"line":326},[316,2582,433],{"class":432},[316,2584,437],{"class":436},[316,2586,441],{"class":440},[316,2588,1000],{"class":436},[316,2590,441],{"class":440},[316,2592,2503],{"class":457},[316,2594,1013],{"class":453},[316,2596,2597],{"class":318,"line":370},[316,2598,390],{"emptyLinePlaceholder":389},[316,2600,2601,2603,2605,2608,2611],{"class":318,"line":386},[316,2602,2182],{"class":545},[316,2604,2185],{"class":549},[316,2606,2607],{"class":2188}," EditorScopes",[316,2609,2610],{"class":545}," implements",[316,2612,2614],{"class":2613},"sW3Pz"," IdentityClaimsProviderInterface\n",[316,2616,2617],{"class":318,"line":393},[316,2618,2194],{"class":453},[316,2620,2621,2623,2625,2628,2630,2632,2634,2637,2639,2641],{"class":318,"line":399},[316,2622,2236],{"class":545},[316,2624,550],{"class":549},[316,2626,2627],{"class":553}," enrich",[316,2629,487],{"class":453},[316,2631,2503],{"class":563},[316,2633,1063],{"class":453},[316,2635,2636],{"class":481},"identity",[316,2638,498],{"class":453},[316,2640,560],{"class":344},[316,2642,2643],{"class":563}," UserIdentity\n",[316,2645,2646],{"class":318,"line":542},[316,2647,2648],{"class":453},"    {\n",[316,2650,2651,2654,2656,2659,2662,2664,2666,2669,2671,2673,2675,2677,2679,2682,2685,2688,2691],{"class":318,"line":1159},[316,2652,2653],{"class":1358},"        if",[316,2655,618],{"class":453},[316,2657,2658],{"class":344},"!",[316,2660,2661],{"class":1171},"in_array",[316,2663,487],{"class":453},[316,2665,490],{"class":357},[316,2667,2668],{"class":333},"editor",[316,2670,490],{"class":357},[316,2672,461],{"class":453},[316,2674,1063],{"class":453},[316,2676,2636],{"class":481},[316,2678,1069],{"class":344},[316,2680,2681],{"class":553},"roles",[316,2683,2684],{"class":453},"(),",[316,2686,2687],{"class":1813}," true",[316,2689,2690],{"class":453},"))",[316,2692,1376],{"class":453},[316,2694,2695,2698,2700,2702],{"class":318,"line":1203},[316,2696,2697],{"class":1358},"            return",[316,2699,1063],{"class":453},[316,2701,2636],{"class":481},[316,2703,1013],{"class":453},[316,2705,2706],{"class":318,"line":1209},[316,2707,2708],{"class":453},"        }\n",[316,2710,2711],{"class":318,"line":1214},[316,2712,390],{"emptyLinePlaceholder":389},[316,2714,2715,2718,2720,2722,2724,2727],{"class":318,"line":1245},[316,2716,2717],{"class":1358},"        return",[316,2719,1063],{"class":453},[316,2721,2636],{"class":481},[316,2723,1069],{"class":344},[316,2725,2726],{"class":553},"withClaims",[316,2728,2729],{"class":453},"([\n",[316,2731,2732,2735,2737,2739,2741,2743,2746,2748,2750,2752,2754,2756,2758,2760,2762,2764,2766,2768,2770],{"class":318,"line":2313},[316,2733,2734],{"class":357},"            '",[316,2736,775],{"class":333},[316,2738,490],{"class":357},[316,2740,1142],{"class":344},[316,2742,1109],{"class":453},[316,2744,2745],{"class":344},"...",[316,2747,1022],{"class":453},[316,2749,2636],{"class":481},[316,2751,1069],{"class":344},[316,2753,775],{"class":553},[316,2755,2684],{"class":453},[316,2757,1088],{"class":357},[316,2759,533],{"class":333},[316,2761,490],{"class":357},[316,2763,461],{"class":453},[316,2765,1088],{"class":357},[316,2767,2066],{"class":333},[316,2769,490],{"class":357},[316,2771,1825],{"class":453},[316,2773,2774],{"class":318,"line":2333},[316,2775,2776],{"class":453},"        ]);\n",[316,2778,2779],{"class":318,"line":2365},[316,2780,2781],{"class":453},"    }\n",[316,2783,2785],{"class":318,"line":2784},16,[316,2786,1442],{"class":453},[316,2788,2790],{"class":318,"line":2789},17,[316,2791,390],{"emptyLinePlaceholder":389},[316,2793,2795],{"class":318,"line":2794},18,[316,2796,2797],{"class":322},"// In your provider's services():\n",[316,2799,2801],{"class":318,"line":2800},19,[316,2802,2803],{"class":322},"// EditorScopes::class => ['class' => EditorScopes::class, 'shared' => true,\n",[316,2805,2807],{"class":318,"line":2806},20,[316,2808,2809],{"class":322},"//                         'tags' => ['identity.claims_provider']],\n",[2463,2811,2814],{"color":2812,"icon":2813,"variant":2467},"info","i-lucide-info",[289,2815,2816,2819,2820,2823],{},[1651,2817,2818],{},"An empty scope set means different things for the two credentials."," An API key with no scopes is unrestricted and passes every scoped route. A JWT whose identity has no scopes is ",[1651,2821,2822],{},"denied"," on every scoped route. A token never gets full access just because it lacks scopes.",[289,2825,2826],{},"When a request carries an API key, the key's scopes always decide. The identity that API-key authentication builds can't widen them.",[302,2828,2830],{"id":2829},"best-practices","Best Practices",[2103,2832,2833,2836,2842,2845,2852],{},[1725,2834,2835],{},"Give every key the narrowest scopes it needs; avoid unrestricted keys outside internal tooling.",[1725,2837,2838,2839,2841],{},"Set ",[293,2840,956],{}," on keys for third parties and rotate before expiry.",[1725,2843,2844],{},"Use IP allowlists for fixed-egress integrations.",[1725,2846,2847,2848,2851],{},"Prefer the ",[293,2849,2850],{},"X-API-Key"," header; leave the query parameter disabled.",[1725,2853,2854],{},"Revoke a key the moment it leaks. Rotation leaves the old key valid during the grace period, so it isn't enough for a leaked key.",[302,2856,2858],{"id":2857},"next-steps","Next Steps",[2103,2860,2861,2869,2877],{},[1725,2862,2863,2865,2866,2868],{},[2546,2864,37],{"href":38}," - JWT login, the ",[293,2867,299],{}," middleware and roles",[1725,2870,2871,2873,2874,2876],{},[2546,2872,2548],{"href":42}," - ",[293,2875,2503],{},", the user store and claims providers",[1725,2878,2879,2881],{},[2546,2880,117],{"href":118}," - Throttle API clients",[2883,2884,2885],"style",{},"html pre.shiki code .ss7Ak, html code.shiki .ss7Ak{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit;--shiki-sepia:#88846F;--shiki-sepia-font-style:inherit}html pre.shiki code .sR7ES, html code.shiki .sR7ES{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .sLACW, html code.shiki .sLACW{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sFhLe, html code.shiki .sFhLe{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sGXK2, html code.shiki .sGXK2{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .siCPE, html code.shiki .siCPE{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sQeA1, html code.shiki .sQeA1{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .shWJe, html code.shiki .shWJe{--shiki-light:#F76D47;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .s91G_, html code.shiki .s91G_{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#F8F8F2}html pre.shiki code .sv8o3, html code.shiki .sv8o3{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#F8F8F2}html pre.shiki code .swvn1, html code.shiki .swvn1{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .seZir, html code.shiki .seZir{--shiki-light:#90A4AE;--shiki-light-font-style:inherit;--shiki-default:#005CC5;--shiki-default-font-style:inherit;--shiki-dark:#79B8FF;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .ss--_, html code.shiki .ss--_{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sTNss, html code.shiki .sTNss{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .srJo8, html code.shiki .srJo8{--shiki-light:#9C3EDA;--shiki-light-font-style:inherit;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sD0ED, html code.shiki .sD0ED{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .s_MOj, html code.shiki .s_MOj{--shiki-light:#E2931D;--shiki-light-font-style:inherit;--shiki-default:#005CC5;--shiki-default-font-style:inherit;--shiki-dark:#79B8FF;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sMLJd, html code.shiki .sMLJd{--shiki-light:#6182B8;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#66D9EF}html pre.shiki code .sRxSC, html code.shiki .sRxSC{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#F92672;--shiki-sepia-font-style:inherit}html pre.shiki code .sQqfL, html code.shiki .sQqfL{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#F8F8F2}html pre.shiki code .sYThS, html code.shiki .sYThS{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sMTiH, html code.shiki .sMTiH{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sKvfc, html code.shiki .sKvfc{--shiki-light:#E2931D;--shiki-light-text-decoration:inherit;--shiki-default:#6F42C1;--shiki-default-text-decoration:inherit;--shiki-dark:#B392F0;--shiki-dark-text-decoration:inherit;--shiki-sepia:#A6E22E;--shiki-sepia-text-decoration:underline}html pre.shiki code .sW3Pz, html code.shiki .sW3Pz{--shiki-light:#E2931D;--shiki-light-font-style:inherit;--shiki-light-text-decoration:inherit;--shiki-default:#6F42C1;--shiki-default-font-style:inherit;--shiki-default-text-decoration:inherit;--shiki-dark:#B392F0;--shiki-dark-font-style:inherit;--shiki-dark-text-decoration:inherit;--shiki-sepia:#A6E22E;--shiki-sepia-font-style:italic;--shiki-sepia-text-decoration:underline}",{"title":312,"searchDepth":319,"depth":326,"links":2887},[2888,2889,2890,2892,2896,2897,2898,2899,2900,2901,2902,2906,2907,2908],{"id":304,"depth":326,"text":305},{"id":576,"depth":326,"text":577},{"id":695,"depth":326,"text":2891},"The api_keys Table",{"id":817,"depth":326,"text":818,"children":2893},[2894,2895],{"id":822,"depth":370,"text":823},{"id":977,"depth":370,"text":978},{"id":1287,"depth":326,"text":1288},{"id":1445,"depth":326,"text":1446},{"id":1661,"depth":326,"text":1662},{"id":1716,"depth":326,"text":1717},{"id":1911,"depth":326,"text":1912},{"id":1962,"depth":326,"text":1963},{"id":775,"depth":326,"text":1978,"children":2903},[2904],{"id":2085,"depth":370,"text":2905},"Requiring scopes with #[RequireScope]",{"id":2487,"depth":326,"text":2488},{"id":2829,"depth":326,"text":2830},{"id":2857,"depth":326,"text":2858},"Issue, rotate and revoke scoped API keys, and protect routes with","md",null,{},{"title":77,"description":2909},"Yg4dzI7ib1yIKGjafI9ymkjCB1R3BOZ3KujlN0TZNEE",[2916,2918],{"title":74,"path":70,"stem":75,"description":2917,"children":-1},"Powerful built-in features for modern applications",{"title":81,"path":82,"stem":83,"description":2919,"children":-1},"Authenticate browser clients with HttpOnly session cookies instead of tokens in JavaScript",1793881543799]