@wordpress/abilities 0.5.1-next.v.202602091733.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.0 (2026-02-18)
6
+
5
7
  ## 0.5.0 (2026-01-29)
6
8
 
7
9
  ## 0.4.0 (2026-01-16)
@@ -46,7 +46,7 @@ function registerAbility(ability) {
46
46
  }
47
47
  if (!import_constants.ABILITY_NAME_PATTERN.test(ability.name)) {
48
48
  throw new Error(
49
- 'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
49
+ 'Ability name must be a string containing a namespace prefix with 2-4 segments, e.g. "my-plugin/my-ability" or "core/posts/find". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
50
50
  );
51
51
  }
52
52
  if (!ability.label) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/store/actions.ts"],
4
- "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { sprintf } from '@wordpress/i18n';\n\n/**\n * Internal dependencies\n */\nimport type { Ability, AbilityCategory, AbilityCategoryArgs } from '../types';\nimport {\n\tREGISTER_ABILITY,\n\tUNREGISTER_ABILITY,\n\tREGISTER_ABILITY_CATEGORY,\n\tUNREGISTER_ABILITY_CATEGORY,\n\tABILITY_NAME_PATTERN,\n\tCATEGORY_SLUG_PATTERN,\n} from './constants';\n\ntype AbilityAnnotations = NonNullable< Ability[ 'meta' ] >[ 'annotations' ];\n\n/**\n * Filters annotations to only include allowed keys with non-null values.\n *\n * @param sourceAnnotations The source annotations object to filter.\n * @param allowedKeys Array of annotation keys to include.\n * @return Filtered annotations object.\n */\nfunction filterAnnotations< K extends keyof NonNullable< AbilityAnnotations > >(\n\tsourceAnnotations: Record< string, boolean > | undefined,\n\tallowedKeys: readonly K[]\n): NonNullable< AbilityAnnotations > {\n\tconst annotations: NonNullable< AbilityAnnotations > = {};\n\n\tif ( sourceAnnotations ) {\n\t\tfor ( const key of allowedKeys ) {\n\t\t\tif ( sourceAnnotations[ key ] !== undefined ) {\n\t\t\t\tannotations[ key ] = sourceAnnotations[ key ];\n\t\t\t}\n\t\t}\n\t}\n\treturn annotations;\n}\n\n/**\n * Registers an ability in the store.\n *\n * This action validates the ability before registration. If validation fails,\n * an error will be thrown.\n *\n * @param ability The ability to register.\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbility( ability: Ability ) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! ability.name ) {\n\t\t\tthrow new Error( 'Ability name is required' );\n\t\t}\n\n\t\t// Validate name format matches server implementation\n\t\tif ( ! ABILITY_NAME_PATTERN.test( ability.name ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Ability name must be a string containing a namespace prefix, i.e. \"my-plugin/my-ability\". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.label ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a label', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.description ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a description', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.category ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a category', ability.name )\n\t\t\t);\n\t\t}\n\n\t\t// Validate category format\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( ability.category ) ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" has an invalid category. Category must be lowercase alphanumeric with dashes only. Got: \"%2$s\"',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check that the category exists\n\t\tconst categories = select.getAbilityCategories();\n\t\tconst existingCategory = categories.find(\n\t\t\t( cat: AbilityCategory ) => cat.slug === ability.category\n\t\t);\n\t\tif ( ! existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" references non-existent category \"%2$s\". Please register the category first.',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Client-side abilities must have a callback\n\t\tif ( ability.callback && typeof ability.callback !== 'function' ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%s\" has an invalid callback. Callback must be a function',\n\t\t\t\t\tability.name\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check if ability is already registered\n\t\tconst existingAbility = select.getAbility( ability.name );\n\t\tif ( existingAbility ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" is already registered', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( ability.meta?.annotations, [\n\t\t\t'readonly',\n\t\t\t'destructive',\n\t\t\t'idempotent',\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( ability.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\n\t\t// All validation passed, dispatch the registration action\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY,\n\t\t\tability: {\n\t\t\t\t...ability,\n\t\t\t\tmeta,\n\t\t\t},\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability.\n *\n * @param name The name of the ability to unregister.\n * @return Action object.\n */\nexport function unregisterAbility( name: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY,\n\t\tname,\n\t};\n}\n\n/**\n * Registers a client-side ability category in the store.\n *\n * This action validates the category before registration. If validation fails,\n * an error will be thrown.\n *\n * @param slug The unique category slug identifier.\n * @param args Category arguments (label, description, optional meta).\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbilityCategory(\n\tslug: string,\n\targs: AbilityCategoryArgs\n) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! slug ) {\n\t\t\tthrow new Error( 'Category slug is required' );\n\t\t}\n\n\t\t// Validate slug format matches server implementation\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( slug ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Category slug must contain only lowercase alphanumeric characters and dashes.'\n\t\t\t);\n\t\t}\n\n\t\t// Check for duplicates\n\t\tconst existingCategory = select.getAbilityCategory( slug );\n\t\tif ( existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Category \"%s\" is already registered.', slug )\n\t\t\t);\n\t\t}\n\n\t\t// Validate label presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.label || typeof args.label !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `label` string.'\n\t\t\t);\n\t\t}\n\n\t\t// Validate description presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.description || typeof args.description !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `description` string.'\n\t\t\t);\n\t\t}\n\n\t\tif (\n\t\t\targs.meta !== undefined &&\n\t\t\t( typeof args.meta !== 'object' || Array.isArray( args.meta ) )\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties should provide a valid `meta` object.'\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( args.meta?.annotations, [\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( args.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\t\tconst category: AbilityCategory = {\n\t\t\tslug,\n\t\t\tlabel: args.label,\n\t\t\tdescription: args.description,\n\t\t\tmeta,\n\t\t};\n\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY_CATEGORY,\n\t\t\tcategory,\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability category.\n *\n * @param slug The slug of the category to unregister.\n * @return Action object.\n */\nexport function unregisterAbilityCategory( slug: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY_CATEGORY,\n\t\tslug,\n\t};\n}\n"],
4
+ "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { sprintf } from '@wordpress/i18n';\n\n/**\n * Internal dependencies\n */\nimport type { Ability, AbilityCategory, AbilityCategoryArgs } from '../types';\nimport {\n\tREGISTER_ABILITY,\n\tUNREGISTER_ABILITY,\n\tREGISTER_ABILITY_CATEGORY,\n\tUNREGISTER_ABILITY_CATEGORY,\n\tABILITY_NAME_PATTERN,\n\tCATEGORY_SLUG_PATTERN,\n} from './constants';\n\ntype AbilityAnnotations = NonNullable< Ability[ 'meta' ] >[ 'annotations' ];\n\n/**\n * Filters annotations to only include allowed keys with non-null values.\n *\n * @param sourceAnnotations The source annotations object to filter.\n * @param allowedKeys Array of annotation keys to include.\n * @return Filtered annotations object.\n */\nfunction filterAnnotations< K extends keyof NonNullable< AbilityAnnotations > >(\n\tsourceAnnotations: Record< string, boolean > | undefined,\n\tallowedKeys: readonly K[]\n): NonNullable< AbilityAnnotations > {\n\tconst annotations: NonNullable< AbilityAnnotations > = {};\n\n\tif ( sourceAnnotations ) {\n\t\tfor ( const key of allowedKeys ) {\n\t\t\tif ( sourceAnnotations[ key ] !== undefined ) {\n\t\t\t\tannotations[ key ] = sourceAnnotations[ key ];\n\t\t\t}\n\t\t}\n\t}\n\treturn annotations;\n}\n\n/**\n * Registers an ability in the store.\n *\n * This action validates the ability before registration. If validation fails,\n * an error will be thrown.\n *\n * @param ability The ability to register.\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbility( ability: Ability ) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! ability.name ) {\n\t\t\tthrow new Error( 'Ability name is required' );\n\t\t}\n\n\t\t// Validate name format matches server implementation\n\t\tif ( ! ABILITY_NAME_PATTERN.test( ability.name ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Ability name must be a string containing a namespace prefix with 2-4 segments, e.g. \"my-plugin/my-ability\" or \"core/posts/find\". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.label ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a label', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.description ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a description', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.category ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a category', ability.name )\n\t\t\t);\n\t\t}\n\n\t\t// Validate category format\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( ability.category ) ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" has an invalid category. Category must be lowercase alphanumeric with dashes only. Got: \"%2$s\"',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check that the category exists\n\t\tconst categories = select.getAbilityCategories();\n\t\tconst existingCategory = categories.find(\n\t\t\t( cat: AbilityCategory ) => cat.slug === ability.category\n\t\t);\n\t\tif ( ! existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" references non-existent category \"%2$s\". Please register the category first.',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Client-side abilities must have a callback\n\t\tif ( ability.callback && typeof ability.callback !== 'function' ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%s\" has an invalid callback. Callback must be a function',\n\t\t\t\t\tability.name\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check if ability is already registered\n\t\tconst existingAbility = select.getAbility( ability.name );\n\t\tif ( existingAbility ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" is already registered', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( ability.meta?.annotations, [\n\t\t\t'readonly',\n\t\t\t'destructive',\n\t\t\t'idempotent',\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( ability.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\n\t\t// All validation passed, dispatch the registration action\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY,\n\t\t\tability: {\n\t\t\t\t...ability,\n\t\t\t\tmeta,\n\t\t\t},\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability.\n *\n * @param name The name of the ability to unregister.\n * @return Action object.\n */\nexport function unregisterAbility( name: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY,\n\t\tname,\n\t};\n}\n\n/**\n * Registers a client-side ability category in the store.\n *\n * This action validates the category before registration. If validation fails,\n * an error will be thrown.\n *\n * @param slug The unique category slug identifier.\n * @param args Category arguments (label, description, optional meta).\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbilityCategory(\n\tslug: string,\n\targs: AbilityCategoryArgs\n) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! slug ) {\n\t\t\tthrow new Error( 'Category slug is required' );\n\t\t}\n\n\t\t// Validate slug format matches server implementation\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( slug ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Category slug must contain only lowercase alphanumeric characters and dashes.'\n\t\t\t);\n\t\t}\n\n\t\t// Check for duplicates\n\t\tconst existingCategory = select.getAbilityCategory( slug );\n\t\tif ( existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Category \"%s\" is already registered.', slug )\n\t\t\t);\n\t\t}\n\n\t\t// Validate label presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.label || typeof args.label !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `label` string.'\n\t\t\t);\n\t\t}\n\n\t\t// Validate description presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.description || typeof args.description !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `description` string.'\n\t\t\t);\n\t\t}\n\n\t\tif (\n\t\t\targs.meta !== undefined &&\n\t\t\t( typeof args.meta !== 'object' || Array.isArray( args.meta ) )\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties should provide a valid `meta` object.'\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( args.meta?.annotations, [\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( args.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\t\tconst category: AbilityCategory = {\n\t\t\tslug,\n\t\t\tlabel: args.label,\n\t\t\tdescription: args.description,\n\t\t\tmeta,\n\t\t};\n\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY_CATEGORY,\n\t\t\tcategory,\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability category.\n *\n * @param slug The slug of the category to unregister.\n * @return Action object.\n */\nexport function unregisterAbilityCategory( slug: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY_CATEGORY,\n\t\tslug,\n\t};\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,kBAAwB;AAMxB,uBAOO;AAWP,SAAS,kBACR,mBACA,aACoC;AACpC,QAAM,cAAiD,CAAC;AAExD,MAAK,mBAAoB;AACxB,eAAY,OAAO,aAAc;AAChC,UAAK,kBAAmB,GAAI,MAAM,QAAY;AAC7C,oBAAa,GAAI,IAAI,kBAAmB,GAAI;AAAA,MAC7C;AAAA,IACD;AAAA,EACD;AACA,SAAO;AACR;AAYO,SAAS,gBAAiB,SAAmB;AAEnD,SAAO,CAAE,EAAE,QAAQ,SAAS,MAAO;AAClC,QAAK,CAAE,QAAQ,MAAO;AACrB,YAAM,IAAI,MAAO,0BAA2B;AAAA,IAC7C;AAGA,QAAK,CAAE,sCAAqB,KAAM,QAAQ,IAAK,GAAI;AAClD,YAAM,IAAI;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAEA,QAAK,CAAE,QAAQ,OAAQ;AACtB,YAAM,IAAI;AAAA,YACT,qBAAS,kCAAkC,QAAQ,IAAK;AAAA,MACzD;AAAA,IACD;AAEA,QAAK,CAAE,QAAQ,aAAc;AAC5B,YAAM,IAAI;AAAA,YACT,qBAAS,wCAAwC,QAAQ,IAAK;AAAA,MAC/D;AAAA,IACD;AAEA,QAAK,CAAE,QAAQ,UAAW;AACzB,YAAM,IAAI;AAAA,YACT,qBAAS,qCAAqC,QAAQ,IAAK;AAAA,MAC5D;AAAA,IACD;AAGA,QAAK,CAAE,uCAAsB,KAAM,QAAQ,QAAS,GAAI;AACvD,YAAM,IAAI;AAAA,YACT;AAAA,UACC;AAAA,UACA,QAAQ;AAAA,UACR,QAAQ;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAGA,UAAM,aAAa,OAAO,qBAAqB;AAC/C,UAAM,mBAAmB,WAAW;AAAA,MACnC,CAAE,QAA0B,IAAI,SAAS,QAAQ;AAAA,IAClD;AACA,QAAK,CAAE,kBAAmB;AACzB,YAAM,IAAI;AAAA,YACT;AAAA,UACC;AAAA,UACA,QAAQ;AAAA,UACR,QAAQ;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAGA,QAAK,QAAQ,YAAY,OAAO,QAAQ,aAAa,YAAa;AACjE,YAAM,IAAI;AAAA,YACT;AAAA,UACC;AAAA,UACA,QAAQ;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAGA,UAAM,kBAAkB,OAAO,WAAY,QAAQ,IAAK;AACxD,QAAK,iBAAkB;AACtB,YAAM,IAAI;AAAA,YACT,qBAAS,sCAAsC,QAAQ,IAAK;AAAA,MAC7D;AAAA,IACD;AAEA,UAAM,cAAc,kBAAmB,QAAQ,MAAM,aAAa;AAAA,MACjE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACD,CAAE;AAEF,QAAK,CAAE,YAAY,kBAAmB;AACrC,kBAAY,mBAAmB;AAAA,IAChC;AAEA,UAAM,OAAO;AAAA,MACZ,GAAK,QAAQ,QAAQ,CAAC;AAAA,MACtB;AAAA,IACD;AAGA,aAAU;AAAA,MACT,MAAM;AAAA,MACN,SAAS;AAAA,QACR,GAAG;AAAA,QACH;AAAA,MACD;AAAA,IACD,CAAE;AAAA,EACH;AACD;AAQO,SAAS,kBAAmB,MAAe;AACjD,SAAO;AAAA,IACN,MAAM;AAAA,IACN;AAAA,EACD;AACD;AAaO,SAAS,wBACf,MACA,MACC;AAED,SAAO,CAAE,EAAE,QAAQ,SAAS,MAAO;AAClC,QAAK,CAAE,MAAO;AACb,YAAM,IAAI,MAAO,2BAA4B;AAAA,IAC9C;AAGA,QAAK,CAAE,uCAAsB,KAAM,IAAK,GAAI;AAC3C,YAAM,IAAI;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAGA,UAAM,mBAAmB,OAAO,mBAAoB,IAAK;AACzD,QAAK,kBAAmB;AACvB,YAAM,IAAI;AAAA,YACT,qBAAS,wCAAwC,IAAK;AAAA,MACvD;AAAA,IACD;AAGA,QAAK,CAAE,KAAK,SAAS,OAAO,KAAK,UAAU,UAAW;AACrD,YAAM,IAAI;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAGA,QAAK,CAAE,KAAK,eAAe,OAAO,KAAK,gBAAgB,UAAW;AACjE,YAAM,IAAI;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAEA,QACC,KAAK,SAAS,WACZ,OAAO,KAAK,SAAS,YAAY,MAAM,QAAS,KAAK,IAAK,IAC3D;AACD,YAAM,IAAI;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAEA,UAAM,cAAc,kBAAmB,KAAK,MAAM,aAAa;AAAA,MAC9D;AAAA,MACA;AAAA,IACD,CAAE;AAEF,QAAK,CAAE,YAAY,kBAAmB;AACrC,kBAAY,mBAAmB;AAAA,IAChC;AAEA,UAAM,OAAO;AAAA,MACZ,GAAK,KAAK,QAAQ,CAAC;AAAA,MACnB;AAAA,IACD;AACA,UAAM,WAA4B;AAAA,MACjC;AAAA,MACA,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB;AAAA,IACD;AAEA,aAAU;AAAA,MACT,MAAM;AAAA,MACN;AAAA,IACD,CAAE;AAAA,EACH;AACD;AAQO,SAAS,0BAA2B,MAAe;AACzD,SAAO;AAAA,IACN,MAAM;AAAA,IACN;AAAA,EACD;AACD;",
6
6
  "names": []
7
7
  }
@@ -30,7 +30,7 @@ __export(constants_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(constants_exports);
32
32
  var STORE_NAME = "core/abilities";
33
- var ABILITY_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/;
33
+ var ABILITY_NAME_PATTERN = /^[a-z0-9-]+(?:\/[a-z0-9-]+){1,3}$/;
34
34
  var CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
35
35
  var REGISTER_ABILITY = "REGISTER_ABILITY";
36
36
  var UNREGISTER_ABILITY = "UNREGISTER_ABILITY";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/store/constants.ts"],
4
- "sourcesContent": ["/**\n * Store constants\n */\nexport const STORE_NAME = 'core/abilities';\n\n// Validation patterns\nexport const ABILITY_NAME_PATTERN = /^[a-z0-9-]+\\/[a-z0-9-]+$/;\nexport const CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n// Action types\nexport const REGISTER_ABILITY = 'REGISTER_ABILITY';\nexport const UNREGISTER_ABILITY = 'UNREGISTER_ABILITY';\nexport const REGISTER_ABILITY_CATEGORY = 'REGISTER_ABILITY_CATEGORY';\nexport const UNREGISTER_ABILITY_CATEGORY = 'UNREGISTER_ABILITY_CATEGORY';\n"],
4
+ "sourcesContent": ["/**\n * Store constants\n */\nexport const STORE_NAME = 'core/abilities';\n\n// Validation patterns\nexport const ABILITY_NAME_PATTERN = /^[a-z0-9-]+(?:\\/[a-z0-9-]+){1,3}$/;\nexport const CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n// Action types\nexport const REGISTER_ABILITY = 'REGISTER_ABILITY';\nexport const UNREGISTER_ABILITY = 'UNREGISTER_ABILITY';\nexport const REGISTER_ABILITY_CATEGORY = 'REGISTER_ABILITY_CATEGORY';\nexport const UNREGISTER_ABILITY_CATEGORY = 'UNREGISTER_ABILITY_CATEGORY';\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,IAAM,aAAa;AAGnB,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAG9B,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAClC,IAAM,8BAA8B;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/types.ts"],
4
- "sourcesContent": ["/**\n * WordPress Abilities API Types\n */\n\n/**\n * Callback function for client-side abilities.\n */\nexport type AbilityCallback = (\n\tinput: AbilityInput\n) => AbilityOutput | Promise< AbilityOutput >;\n\n/**\n * Permission callback function for client-side abilities.\n * Returns true if the ability can be executed, false otherwise.\n */\nexport type PermissionCallback = (\n\tinput?: AbilityInput\n) => boolean | Promise< boolean >;\n\n/**\n * Represents an ability in the WordPress Abilities API.\n *\n * @see WP_Ability\n */\nexport interface Ability {\n\t/**\n\t * The unique name/identifier of the ability, with its namespace.\n\t * Example: 'my-plugin/my-ability'\n\t * @see WP_Ability::get_name()\n\t */\n\tname: string;\n\n\t/**\n\t * The human-readable label for the ability.\n\t * @see WP_Ability::get_label()\n\t */\n\tlabel: string;\n\n\t/**\n\t * The detailed description of the ability.\n\t * @see WP_Ability::get_description()\n\t */\n\tdescription: string;\n\n\t/**\n\t * The category this ability belongs to.\n\t * Must be a valid category slug (lowercase alphanumeric with dashes).\n\t * Example: 'data-retrieval', 'user-management'\n\t * @see WP_Ability::get_category()\n\t */\n\tcategory: string;\n\n\t/**\n\t * JSON Schema for the ability's input parameters.\n\t * @see WP_Ability::get_input_schema()\n\t */\n\tinput_schema?: Record< string, any >;\n\n\t/**\n\t * JSON Schema for the ability's output format.\n\t * @see WP_Ability::get_output_schema()\n\t */\n\toutput_schema?: Record< string, any >;\n\n\t/**\n\t * Callback function for ability execution.\n\t * This property is required for all abilities.\n\t */\n\tcallback?: AbilityCallback;\n\n\t/**\n\t * Permission callback for abilities.\n\t * Called before executing the ability to check if it's allowed.\n\t * If it returns false, the ability execution will be denied.\n\t */\n\tpermissionCallback?: PermissionCallback;\n\n\t/**\n\t * Metadata about the ability.\n\t */\n\tmeta?: {\n\t\tannotations?: {\n\t\t\tclientRegistered?: boolean;\n\t\t\tserverRegistered?: boolean;\n\t\t\treadonly?: boolean;\n\t\t\tdestructive?: boolean;\n\t\t\tidempotent?: boolean;\n\t\t};\n\t\t[ key: string ]: any;\n\t};\n}\n\n/**\n * The shape of the arguments for querying abilities.\n */\nexport interface AbilitiesQueryArgs {\n\t/**\n\t * Optional category slug to filter abilities.\n\t */\n\tcategory?: string;\n}\n\n/**\n * Represents an ability category in the WordPress Abilities API.\n *\n * @see WP_Ability_Category\n */\nexport interface AbilityCategory {\n\t/**\n\t * The unique slug identifier for the category.\n\t * Must be lowercase alphanumeric with dashes only.\n\t * Example: 'data-retrieval', 'user-management'\n\t * @see WP_Ability_Category::get_slug()\n\t */\n\tslug: string;\n\n\t/**\n\t * The human-readable label for the category.\n\t * @see WP_Ability_Category::get_label()\n\t */\n\tlabel: string;\n\n\t/**\n\t * The detailed description of the category.\n\t * @see WP_Ability_Category::get_description()\n\t */\n\tdescription: string;\n\n\t/**\n\t * Metadata about the category.\n\t */\n\tmeta?: {\n\t\tannotations?: {\n\t\t\tclientRegistered?: boolean;\n\t\t\tserverRegistered?: boolean;\n\t\t};\n\t\t[ key: string ]: any;\n\t};\n}\n\n/**\n * Arguments for registering an ability category.\n * Matches the server-side wp_register_ability_category() $args parameter.\n *\n * @see wp_register_ability_category()\n */\nexport interface AbilityCategoryArgs {\n\t/**\n\t * The human-readable label for the category.\n\t */\n\tlabel: string;\n\n\t/**\n\t * The detailed description of the category.\n\t */\n\tdescription: string;\n\n\t/**\n\t * Optional metadata about the category.\n\t */\n\tmeta?: Record< string, any >;\n}\n\n/**\n * Input parameters for ability execution.\n * Can be any JSON-serializable value: primitive, array, object, or null.\n */\nexport type AbilityInput = any;\n\n/**\n * Result from ability execution.\n * The actual shape depends on the ability's output schema.\n */\nexport type AbilityOutput = any;\n\n/**\n * Validation error - just a message string.\n * The Abilities API wraps this with the appropriate error code.\n */\nexport type ValidationError = string;\n"],
4
+ "sourcesContent": ["/**\n * WordPress Abilities API Types\n */\n\n/**\n * Callback function for client-side abilities.\n */\nexport type AbilityCallback = (\n\tinput: AbilityInput\n) => AbilityOutput | Promise< AbilityOutput >;\n\n/**\n * Permission callback function for client-side abilities.\n * Returns true if the ability can be executed, false otherwise.\n */\nexport type PermissionCallback = (\n\tinput?: AbilityInput\n) => boolean | Promise< boolean >;\n\n/**\n * Represents an ability in the WordPress Abilities API.\n *\n * @see WP_Ability\n */\nexport interface Ability {\n\t/**\n\t * The unique name/identifier of the ability, with its namespace.\n\t * Supports 2-4 segments (e.g. 'my-plugin/my-ability', 'core/posts/find', 'my-plugin/resource/sub/action').\n\t * @see WP_Ability::get_name()\n\t */\n\tname: string;\n\n\t/**\n\t * The human-readable label for the ability.\n\t * @see WP_Ability::get_label()\n\t */\n\tlabel: string;\n\n\t/**\n\t * The detailed description of the ability.\n\t * @see WP_Ability::get_description()\n\t */\n\tdescription: string;\n\n\t/**\n\t * The category this ability belongs to.\n\t * Must be a valid category slug (lowercase alphanumeric with dashes).\n\t * Example: 'data-retrieval', 'user-management'\n\t * @see WP_Ability::get_category()\n\t */\n\tcategory: string;\n\n\t/**\n\t * JSON Schema for the ability's input parameters.\n\t * @see WP_Ability::get_input_schema()\n\t */\n\tinput_schema?: Record< string, any >;\n\n\t/**\n\t * JSON Schema for the ability's output format.\n\t * @see WP_Ability::get_output_schema()\n\t */\n\toutput_schema?: Record< string, any >;\n\n\t/**\n\t * Callback function for ability execution.\n\t * This property is required for all abilities.\n\t */\n\tcallback?: AbilityCallback;\n\n\t/**\n\t * Permission callback for abilities.\n\t * Called before executing the ability to check if it's allowed.\n\t * If it returns false, the ability execution will be denied.\n\t */\n\tpermissionCallback?: PermissionCallback;\n\n\t/**\n\t * Metadata about the ability.\n\t */\n\tmeta?: {\n\t\tannotations?: {\n\t\t\tclientRegistered?: boolean;\n\t\t\tserverRegistered?: boolean;\n\t\t\treadonly?: boolean;\n\t\t\tdestructive?: boolean;\n\t\t\tidempotent?: boolean;\n\t\t};\n\t\t[ key: string ]: any;\n\t};\n}\n\n/**\n * The shape of the arguments for querying abilities.\n */\nexport interface AbilitiesQueryArgs {\n\t/**\n\t * Optional category slug to filter abilities.\n\t */\n\tcategory?: string;\n}\n\n/**\n * Represents an ability category in the WordPress Abilities API.\n *\n * @see WP_Ability_Category\n */\nexport interface AbilityCategory {\n\t/**\n\t * The unique slug identifier for the category.\n\t * Must be lowercase alphanumeric with dashes only.\n\t * Example: 'data-retrieval', 'user-management'\n\t * @see WP_Ability_Category::get_slug()\n\t */\n\tslug: string;\n\n\t/**\n\t * The human-readable label for the category.\n\t * @see WP_Ability_Category::get_label()\n\t */\n\tlabel: string;\n\n\t/**\n\t * The detailed description of the category.\n\t * @see WP_Ability_Category::get_description()\n\t */\n\tdescription: string;\n\n\t/**\n\t * Metadata about the category.\n\t */\n\tmeta?: {\n\t\tannotations?: {\n\t\t\tclientRegistered?: boolean;\n\t\t\tserverRegistered?: boolean;\n\t\t};\n\t\t[ key: string ]: any;\n\t};\n}\n\n/**\n * Arguments for registering an ability category.\n * Matches the server-side wp_register_ability_category() $args parameter.\n *\n * @see wp_register_ability_category()\n */\nexport interface AbilityCategoryArgs {\n\t/**\n\t * The human-readable label for the category.\n\t */\n\tlabel: string;\n\n\t/**\n\t * The detailed description of the category.\n\t */\n\tdescription: string;\n\n\t/**\n\t * Optional metadata about the category.\n\t */\n\tmeta?: Record< string, any >;\n}\n\n/**\n * Input parameters for ability execution.\n * Can be any JSON-serializable value: primitive, array, object, or null.\n */\nexport type AbilityInput = any;\n\n/**\n * Result from ability execution.\n * The actual shape depends on the ability's output schema.\n */\nexport type AbilityOutput = any;\n\n/**\n * Validation error - just a message string.\n * The Abilities API wraps this with the appropriate error code.\n */\nexport type ValidationError = string;\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
@@ -26,7 +26,7 @@ function registerAbility(ability) {
26
26
  }
27
27
  if (!ABILITY_NAME_PATTERN.test(ability.name)) {
28
28
  throw new Error(
29
- 'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
29
+ 'Ability name must be a string containing a namespace prefix with 2-4 segments, e.g. "my-plugin/my-ability" or "core/posts/find". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
30
30
  );
31
31
  }
32
32
  if (!ability.label) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/store/actions.ts"],
4
- "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { sprintf } from '@wordpress/i18n';\n\n/**\n * Internal dependencies\n */\nimport type { Ability, AbilityCategory, AbilityCategoryArgs } from '../types';\nimport {\n\tREGISTER_ABILITY,\n\tUNREGISTER_ABILITY,\n\tREGISTER_ABILITY_CATEGORY,\n\tUNREGISTER_ABILITY_CATEGORY,\n\tABILITY_NAME_PATTERN,\n\tCATEGORY_SLUG_PATTERN,\n} from './constants';\n\ntype AbilityAnnotations = NonNullable< Ability[ 'meta' ] >[ 'annotations' ];\n\n/**\n * Filters annotations to only include allowed keys with non-null values.\n *\n * @param sourceAnnotations The source annotations object to filter.\n * @param allowedKeys Array of annotation keys to include.\n * @return Filtered annotations object.\n */\nfunction filterAnnotations< K extends keyof NonNullable< AbilityAnnotations > >(\n\tsourceAnnotations: Record< string, boolean > | undefined,\n\tallowedKeys: readonly K[]\n): NonNullable< AbilityAnnotations > {\n\tconst annotations: NonNullable< AbilityAnnotations > = {};\n\n\tif ( sourceAnnotations ) {\n\t\tfor ( const key of allowedKeys ) {\n\t\t\tif ( sourceAnnotations[ key ] !== undefined ) {\n\t\t\t\tannotations[ key ] = sourceAnnotations[ key ];\n\t\t\t}\n\t\t}\n\t}\n\treturn annotations;\n}\n\n/**\n * Registers an ability in the store.\n *\n * This action validates the ability before registration. If validation fails,\n * an error will be thrown.\n *\n * @param ability The ability to register.\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbility( ability: Ability ) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! ability.name ) {\n\t\t\tthrow new Error( 'Ability name is required' );\n\t\t}\n\n\t\t// Validate name format matches server implementation\n\t\tif ( ! ABILITY_NAME_PATTERN.test( ability.name ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Ability name must be a string containing a namespace prefix, i.e. \"my-plugin/my-ability\". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.label ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a label', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.description ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a description', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.category ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a category', ability.name )\n\t\t\t);\n\t\t}\n\n\t\t// Validate category format\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( ability.category ) ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" has an invalid category. Category must be lowercase alphanumeric with dashes only. Got: \"%2$s\"',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check that the category exists\n\t\tconst categories = select.getAbilityCategories();\n\t\tconst existingCategory = categories.find(\n\t\t\t( cat: AbilityCategory ) => cat.slug === ability.category\n\t\t);\n\t\tif ( ! existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" references non-existent category \"%2$s\". Please register the category first.',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Client-side abilities must have a callback\n\t\tif ( ability.callback && typeof ability.callback !== 'function' ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%s\" has an invalid callback. Callback must be a function',\n\t\t\t\t\tability.name\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check if ability is already registered\n\t\tconst existingAbility = select.getAbility( ability.name );\n\t\tif ( existingAbility ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" is already registered', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( ability.meta?.annotations, [\n\t\t\t'readonly',\n\t\t\t'destructive',\n\t\t\t'idempotent',\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( ability.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\n\t\t// All validation passed, dispatch the registration action\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY,\n\t\t\tability: {\n\t\t\t\t...ability,\n\t\t\t\tmeta,\n\t\t\t},\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability.\n *\n * @param name The name of the ability to unregister.\n * @return Action object.\n */\nexport function unregisterAbility( name: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY,\n\t\tname,\n\t};\n}\n\n/**\n * Registers a client-side ability category in the store.\n *\n * This action validates the category before registration. If validation fails,\n * an error will be thrown.\n *\n * @param slug The unique category slug identifier.\n * @param args Category arguments (label, description, optional meta).\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbilityCategory(\n\tslug: string,\n\targs: AbilityCategoryArgs\n) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! slug ) {\n\t\t\tthrow new Error( 'Category slug is required' );\n\t\t}\n\n\t\t// Validate slug format matches server implementation\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( slug ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Category slug must contain only lowercase alphanumeric characters and dashes.'\n\t\t\t);\n\t\t}\n\n\t\t// Check for duplicates\n\t\tconst existingCategory = select.getAbilityCategory( slug );\n\t\tif ( existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Category \"%s\" is already registered.', slug )\n\t\t\t);\n\t\t}\n\n\t\t// Validate label presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.label || typeof args.label !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `label` string.'\n\t\t\t);\n\t\t}\n\n\t\t// Validate description presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.description || typeof args.description !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `description` string.'\n\t\t\t);\n\t\t}\n\n\t\tif (\n\t\t\targs.meta !== undefined &&\n\t\t\t( typeof args.meta !== 'object' || Array.isArray( args.meta ) )\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties should provide a valid `meta` object.'\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( args.meta?.annotations, [\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( args.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\t\tconst category: AbilityCategory = {\n\t\t\tslug,\n\t\t\tlabel: args.label,\n\t\t\tdescription: args.description,\n\t\t\tmeta,\n\t\t};\n\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY_CATEGORY,\n\t\t\tcategory,\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability category.\n *\n * @param slug The slug of the category to unregister.\n * @return Action object.\n */\nexport function unregisterAbilityCategory( slug: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY_CATEGORY,\n\t\tslug,\n\t};\n}\n"],
4
+ "sourcesContent": ["/**\n * WordPress dependencies\n */\nimport { sprintf } from '@wordpress/i18n';\n\n/**\n * Internal dependencies\n */\nimport type { Ability, AbilityCategory, AbilityCategoryArgs } from '../types';\nimport {\n\tREGISTER_ABILITY,\n\tUNREGISTER_ABILITY,\n\tREGISTER_ABILITY_CATEGORY,\n\tUNREGISTER_ABILITY_CATEGORY,\n\tABILITY_NAME_PATTERN,\n\tCATEGORY_SLUG_PATTERN,\n} from './constants';\n\ntype AbilityAnnotations = NonNullable< Ability[ 'meta' ] >[ 'annotations' ];\n\n/**\n * Filters annotations to only include allowed keys with non-null values.\n *\n * @param sourceAnnotations The source annotations object to filter.\n * @param allowedKeys Array of annotation keys to include.\n * @return Filtered annotations object.\n */\nfunction filterAnnotations< K extends keyof NonNullable< AbilityAnnotations > >(\n\tsourceAnnotations: Record< string, boolean > | undefined,\n\tallowedKeys: readonly K[]\n): NonNullable< AbilityAnnotations > {\n\tconst annotations: NonNullable< AbilityAnnotations > = {};\n\n\tif ( sourceAnnotations ) {\n\t\tfor ( const key of allowedKeys ) {\n\t\t\tif ( sourceAnnotations[ key ] !== undefined ) {\n\t\t\t\tannotations[ key ] = sourceAnnotations[ key ];\n\t\t\t}\n\t\t}\n\t}\n\treturn annotations;\n}\n\n/**\n * Registers an ability in the store.\n *\n * This action validates the ability before registration. If validation fails,\n * an error will be thrown.\n *\n * @param ability The ability to register.\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbility( ability: Ability ) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! ability.name ) {\n\t\t\tthrow new Error( 'Ability name is required' );\n\t\t}\n\n\t\t// Validate name format matches server implementation\n\t\tif ( ! ABILITY_NAME_PATTERN.test( ability.name ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Ability name must be a string containing a namespace prefix with 2-4 segments, e.g. \"my-plugin/my-ability\" or \"core/posts/find\". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.label ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a label', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.description ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a description', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tif ( ! ability.category ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" must have a category', ability.name )\n\t\t\t);\n\t\t}\n\n\t\t// Validate category format\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( ability.category ) ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" has an invalid category. Category must be lowercase alphanumeric with dashes only. Got: \"%2$s\"',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check that the category exists\n\t\tconst categories = select.getAbilityCategories();\n\t\tconst existingCategory = categories.find(\n\t\t\t( cat: AbilityCategory ) => cat.slug === ability.category\n\t\t);\n\t\tif ( ! existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%1$s\" references non-existent category \"%2$s\". Please register the category first.',\n\t\t\t\t\tability.name,\n\t\t\t\t\tability.category\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Client-side abilities must have a callback\n\t\tif ( ability.callback && typeof ability.callback !== 'function' ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf(\n\t\t\t\t\t'Ability \"%s\" has an invalid callback. Callback must be a function',\n\t\t\t\t\tability.name\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t// Check if ability is already registered\n\t\tconst existingAbility = select.getAbility( ability.name );\n\t\tif ( existingAbility ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Ability \"%s\" is already registered', ability.name )\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( ability.meta?.annotations, [\n\t\t\t'readonly',\n\t\t\t'destructive',\n\t\t\t'idempotent',\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( ability.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\n\t\t// All validation passed, dispatch the registration action\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY,\n\t\t\tability: {\n\t\t\t\t...ability,\n\t\t\t\tmeta,\n\t\t\t},\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability.\n *\n * @param name The name of the ability to unregister.\n * @return Action object.\n */\nexport function unregisterAbility( name: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY,\n\t\tname,\n\t};\n}\n\n/**\n * Registers a client-side ability category in the store.\n *\n * This action validates the category before registration. If validation fails,\n * an error will be thrown.\n *\n * @param slug The unique category slug identifier.\n * @param args Category arguments (label, description, optional meta).\n * @return Action object or function.\n * @throws {Error} If validation fails.\n */\nexport function registerAbilityCategory(\n\tslug: string,\n\targs: AbilityCategoryArgs\n) {\n\t// @ts-expect-error - registry types are not yet available\n\treturn ( { select, dispatch } ) => {\n\t\tif ( ! slug ) {\n\t\t\tthrow new Error( 'Category slug is required' );\n\t\t}\n\n\t\t// Validate slug format matches server implementation\n\t\tif ( ! CATEGORY_SLUG_PATTERN.test( slug ) ) {\n\t\t\tthrow new Error(\n\t\t\t\t'Category slug must contain only lowercase alphanumeric characters and dashes.'\n\t\t\t);\n\t\t}\n\n\t\t// Check for duplicates\n\t\tconst existingCategory = select.getAbilityCategory( slug );\n\t\tif ( existingCategory ) {\n\t\t\tthrow new Error(\n\t\t\t\tsprintf( 'Category \"%s\" is already registered.', slug )\n\t\t\t);\n\t\t}\n\n\t\t// Validate label presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.label || typeof args.label !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `label` string.'\n\t\t\t);\n\t\t}\n\n\t\t// Validate description presence and type (matches PHP empty() + is_string())\n\t\tif ( ! args.description || typeof args.description !== 'string' ) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties must contain a `description` string.'\n\t\t\t);\n\t\t}\n\n\t\tif (\n\t\t\targs.meta !== undefined &&\n\t\t\t( typeof args.meta !== 'object' || Array.isArray( args.meta ) )\n\t\t) {\n\t\t\tthrow new Error(\n\t\t\t\t'The category properties should provide a valid `meta` object.'\n\t\t\t);\n\t\t}\n\n\t\tconst annotations = filterAnnotations( args.meta?.annotations, [\n\t\t\t'serverRegistered',\n\t\t\t'clientRegistered',\n\t\t] );\n\n\t\tif ( ! annotations.serverRegistered ) {\n\t\t\tannotations.clientRegistered = true;\n\t\t}\n\n\t\tconst meta = {\n\t\t\t...( args.meta || {} ),\n\t\t\tannotations,\n\t\t};\n\t\tconst category: AbilityCategory = {\n\t\t\tslug,\n\t\t\tlabel: args.label,\n\t\t\tdescription: args.description,\n\t\t\tmeta,\n\t\t};\n\n\t\tdispatch( {\n\t\t\ttype: REGISTER_ABILITY_CATEGORY,\n\t\t\tcategory,\n\t\t} );\n\t};\n}\n\n/**\n * Returns an action object used to unregister a client-side ability category.\n *\n * @param slug The slug of the category to unregister.\n * @return Action object.\n */\nexport function unregisterAbilityCategory( slug: string ) {\n\treturn {\n\t\ttype: UNREGISTER_ABILITY_CATEGORY,\n\t\tslug,\n\t};\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  // packages/abilities/src/store/constants.ts
2
2
  var STORE_NAME = "core/abilities";
3
- var ABILITY_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/;
3
+ var ABILITY_NAME_PATTERN = /^[a-z0-9-]+(?:\/[a-z0-9-]+){1,3}$/;
4
4
  var CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
5
  var REGISTER_ABILITY = "REGISTER_ABILITY";
6
6
  var UNREGISTER_ABILITY = "UNREGISTER_ABILITY";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/store/constants.ts"],
4
- "sourcesContent": ["/**\n * Store constants\n */\nexport const STORE_NAME = 'core/abilities';\n\n// Validation patterns\nexport const ABILITY_NAME_PATTERN = /^[a-z0-9-]+\\/[a-z0-9-]+$/;\nexport const CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n// Action types\nexport const REGISTER_ABILITY = 'REGISTER_ABILITY';\nexport const UNREGISTER_ABILITY = 'UNREGISTER_ABILITY';\nexport const REGISTER_ABILITY_CATEGORY = 'REGISTER_ABILITY_CATEGORY';\nexport const UNREGISTER_ABILITY_CATEGORY = 'UNREGISTER_ABILITY_CATEGORY';\n"],
4
+ "sourcesContent": ["/**\n * Store constants\n */\nexport const STORE_NAME = 'core/abilities';\n\n// Validation patterns\nexport const ABILITY_NAME_PATTERN = /^[a-z0-9-]+(?:\\/[a-z0-9-]+){1,3}$/;\nexport const CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n// Action types\nexport const REGISTER_ABILITY = 'REGISTER_ABILITY';\nexport const UNREGISTER_ABILITY = 'UNREGISTER_ABILITY';\nexport const REGISTER_ABILITY_CATEGORY = 'REGISTER_ABILITY_CATEGORY';\nexport const UNREGISTER_ABILITY_CATEGORY = 'UNREGISTER_ABILITY_CATEGORY';\n"],
5
5
  "mappings": ";AAGO,IAAM,aAAa;AAGnB,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAG9B,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAClC,IAAM,8BAA8B;",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/store/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,UAAU,mBAAmB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAC/D,eAAO,MAAM,qBAAqB,QAA+B,CAAC;AAGlE,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AACnD,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AACrE,eAAO,MAAM,2BAA2B,gCAAgC,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/store/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,UAAU,mBAAmB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,QAAsC,CAAC;AACxE,eAAO,MAAM,qBAAqB,QAA+B,CAAC;AAGlE,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AACnD,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AACrE,eAAO,MAAM,2BAA2B,gCAAgC,CAAC"}
@@ -18,7 +18,7 @@ export type PermissionCallback = (input?: AbilityInput) => boolean | Promise<boo
18
18
  export interface Ability {
19
19
  /**
20
20
  * The unique name/identifier of the ability, with its namespace.
21
- * Example: 'my-plugin/my-ability'
21
+ * Supports 2-4 segments (e.g. 'my-plugin/my-ability', 'core/posts/find', 'my-plugin/resource/sub/action').
22
22
  * @see WP_Ability::get_name()
23
23
  */
24
24
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/abilities",
3
- "version": "0.5.1-next.v.202602091733.0+daa0b19c4",
3
+ "version": "0.6.0",
4
4
  "description": "JavaScript client for WordPress Abilities API.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "types": "build-types",
47
47
  "dependencies": {
48
- "@wordpress/data": "^10.39.1-next.v.202602091733.0+daa0b19c4",
49
- "@wordpress/i18n": "^6.12.1-next.v.202602091733.0+daa0b19c4",
48
+ "@wordpress/data": "^10.40.0",
49
+ "@wordpress/i18n": "^6.13.0",
50
50
  "ajv": "^8.17.1",
51
51
  "ajv-draft-04": "^1.0.0",
52
52
  "ajv-formats": "^3.0.1"
@@ -54,5 +54,5 @@
54
54
  "publishConfig": {
55
55
  "access": "public"
56
56
  },
57
- "gitHead": "74f59922b25e30904319373dda91bf8e81f3544e"
57
+ "gitHead": "376124aa10dbc2cc0c81c964ec00b99fcfee5382"
58
58
  }
@@ -61,7 +61,7 @@ export function registerAbility( ability: Ability ) {
61
61
  // Validate name format matches server implementation
62
62
  if ( ! ABILITY_NAME_PATTERN.test( ability.name ) ) {
63
63
  throw new Error(
64
- 'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
64
+ 'Ability name must be a string containing a namespace prefix with 2-4 segments, e.g. "my-plugin/my-ability" or "core/posts/find". It can only contain lowercase alphanumeric characters, dashes and the forward slash.'
65
65
  );
66
66
  }
67
67
 
@@ -4,7 +4,7 @@
4
4
  export const STORE_NAME = 'core/abilities';
5
5
 
6
6
  // Validation patterns
7
- export const ABILITY_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/;
7
+ export const ABILITY_NAME_PATTERN = /^[a-z0-9-]+(?:\/[a-z0-9-]+){1,3}$/;
8
8
  export const CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
9
 
10
10
  // Action types
@@ -141,8 +141,8 @@ describe( 'Store Actions', () => {
141
141
 
142
142
  it( 'should validate and reject ability with invalid name format', () => {
143
143
  const testCases = [
144
- 'invalid', // No namespace
145
- 'my-plugin/feature/action', // Multiple slashes
144
+ 'invalid', // No namespace (only 1 segment)
145
+ 'my-plugin/a/b/c/d', // Too many slashes (5 segments)
146
146
  'My-Plugin/feature', // Uppercase letters
147
147
  'my_plugin/feature', // Underscores not allowed
148
148
  'my-plugin/feature!', // Special characters not allowed
@@ -170,6 +170,39 @@ describe( 'Store Actions', () => {
170
170
  }
171
171
  } );
172
172
 
173
+ it( 'should accept valid nested namespace ability names (2-4 segments)', () => {
174
+ const validNames = [
175
+ 'test/ability', // 2 segments
176
+ 'core/posts/find', // 3 segments
177
+ 'my-plugin/resource/action', // 3 segments
178
+ 'my-plugin/resource/sub/action', // 4 segments
179
+ ];
180
+
181
+ for ( const validName of validNames ) {
182
+ const ability: Ability = {
183
+ name: validName,
184
+ label: 'Test Ability',
185
+ description: 'Test description',
186
+ category: 'test-category',
187
+ callback: jest.fn(),
188
+ };
189
+
190
+ mockSelect.getAbility.mockReturnValue( null );
191
+ mockDispatch.mockClear();
192
+
193
+ const action = registerAbility( ability );
194
+ action( { select: mockSelect, dispatch: mockDispatch } );
195
+
196
+ expect( mockDispatch ).toHaveBeenCalledWith( {
197
+ type: REGISTER_ABILITY,
198
+ ability: {
199
+ ...ability,
200
+ meta: { annotations: { clientRegistered: true } },
201
+ },
202
+ } );
203
+ }
204
+ } );
205
+
173
206
  it( 'should validate and reject ability without label', () => {
174
207
  const ability: Ability = {
175
208
  name: 'test/ability',
package/src/types.ts CHANGED
@@ -25,7 +25,7 @@ export type PermissionCallback = (
25
25
  export interface Ability {
26
26
  /**
27
27
  * The unique name/identifier of the ability, with its namespace.
28
- * Example: 'my-plugin/my-ability'
28
+ * Supports 2-4 segments (e.g. 'my-plugin/my-ability', 'core/posts/find', 'my-plugin/resource/sub/action').
29
29
  * @see WP_Ability::get_name()
30
30
  */
31
31
  name: string;