# TrustedLogin Documentation (full) Concatenated source for every documentation page. Sections separated by `---`. Each section names its source URL. --- # Client SDK Intro Source: https://docs.trustedlogin.com/Client/intro Markdown: https://docs.trustedlogin.com/Client/01-intro.md # TrustedLogin SDK Easily and securely log in to your customers sites when providing support. ## Our priority: Be secure and [don't crash sites](https://www.bugsnag.com/blog/sdks-should-not-crash-apps) {#our-priority-sdks-should-not-crash-sites} When you integrate TrustedLogin into your project (theme, plugin, or custom code), you are counting on us not to mess up your customer or clients' sites. We take that extremely seriously. ## What is TrustedLogin? TrustedLogin is a secure and easy-to-use SDK that allows you to log in to your customers' WordPress sites without them sharing passwords. It is designed to be easy to integrate into your plugin, theme, or custom code. ## Why use TrustedLogin? Customer support is a critical part of any business. TrustedLogin makes it easy to provide support to your customers without the need for them to share their passwords. This is a huge win for both you and your customers. ## How to integrate Two paths, same destination: - **Step-by-step:** Start with the [Installation guide](./02-installation) and walk through each piece (Composer, namespacing, configuration, bootstrap). Best when you want to learn the system as you go. - **AI-assisted (or comprehensive checklist):** Paste the [AI Integration Prompt](./integration-prompt) into your AI coding assistant (Claude Code, Cursor, GitHub Copilot Chat, etc.) — it bundles every step into a single self-contained recipe with input collection, host-side conflict detection, and a verification checklist. Distilled from end-to-end testing across real plugins. Also works as the most thorough walkthrough for humans who want the deepest version of the docs. --- # Installation Source: https://docs.trustedlogin.com/Client/installation Markdown: https://docs.trustedlogin.com/Client/02-installation.md :::tip Using an AI coding assistant? Paste the [AI Integration Prompt](./integration-prompt) into Claude Code, Cursor, GitHub Copilot Chat, etc. It bundles every step on this page (plus host-side conflict detection for plugins with an existing `composer.json`, and a verification checklist) into a single self-contained recipe. Distilled from end-to-end testing across real plugins. Also reads as a thorough manual checklist if you want the deepest walkthrough. ::: ## Including in your plugin or theme {#including-in-your-plugin-or-theme} ### 1. Install the TrustedLogin SDK using Composer Run `composer require trustedlogin/client:dev-main` to install the TrustedLogin Client SDK as a dependency. ### 2. Add SCSS as a dev dependency Run `composer require scssphp/scssphp --dev` to install `scssphp` as a dev dependency. This is used to generate and namespace the CSS used by TrustedLogin. If you already have `scssphp` installed, or are [using an alternative way to namespace the CSS](/Client/namespacing/css-namespacing), skip this step. ### 3. Namespace the SDK using [Strauss](/Client/namespacing/strauss) or [PHP-Scoper](/Client/namespacing/php-scoper) In order to prevent conflicts with other plugins or themes that are using TrustedLogin, you must namespace the TrustedLogin Client SDK. We support two namespacing tools: - **[Strauss](/Client/namespacing/strauss) (recommended)** — Composer-installable, uses `delete_vendor_packages: true` to keep the un-namespaced source out of your dev tree. - [PHP-Scoper](/Client/namespacing/php-scoper) — alternative; rewrites the SDK into `build/`. If you're integrating into a plugin that already has its own `composer.json`, also see [Merging into an existing composer.json](/Client/namespacing/merging-into-existing-composer) for common host-side gotchas. If you're using an AI coding assistant (Claude Code, Cursor, etc.) for the integration, paste the [AI Integration Prompt](/Client/integration-prompt) into your assistant — it encodes the full workflow as a single self-contained recipe. ### 4. [Namespace the CSS files](/Client/namespacing/css-namespacing) TrustedLogin CSS files are namespaced so that they don't conflict with other plugins or themes that are using TrustedLogin. Follow the [CSS Namespacing](/Client/namespacing/css-namespacing) guide. ### 5. Include the namespaced autoloader Load the namespaced autoloader on **every** page load (admin and front-end). The exact path depends on which namespacing tool you chose: - **Strauss:** `vendor-namespaced/autoload.php` - **PHP-Scoper:** `vendor/autoload.php` after the host classmap is configured (see the PHP-Scoper guide). ```php // For a plugin or theme using Strauss: require_once trailingslashit( dirname( __FILE__ ) ) . 'vendor-namespaced/autoload.php'; ``` :::warning **Don't load `vendor/autoload.php` for the SDK when using Strauss** — that resolves to the un-namespaced original (`\TrustedLogin\Client`) via Composer's classmap, which would defeat the namespacing. Strauss writes its own self-contained autoloader to `vendor-namespaced/`. If your plugin already loads `vendor/autoload.php` for its own dependencies, that's fine — both autoloaders can coexist. ::: ### 6. Customize the [TrustedLogin configuration](/Client/configuration) options The configuration array is where you set up the TrustedLogin Client SDK to work with your plugin or theme. You can customize the configuration to match your needs. ### 7. Instantiate the TrustedLogin Client :::info The TrustedLogin client must be initialized on all page loads, both the front-end and the dashboard. ::: When instantiating the TrustedLogin `Client` class, you need to pass a valid `Config` object. The following is a minimal configuration. It has all the _required_ settings, but not all **recommended** settings! ```php /** * This is a basic example of how to instantiate the TrustedLogin Client, using the minimum required configuration * settings and hooked into the `plugins_loaded` action. Adjust the configuration to match your needs. */ add_action( 'plugins_loaded', function() { $config = [ 'auth' => [ 'api_key' => '1234567890', ], 'vendor' => [ 'namespace' => 'pro-block-builder', 'title' => 'Pro Block Builder', 'email' => 'support@example.com', 'website' => 'https://example.com', 'support_url' => 'https://help.example.com', ], 'role' => 'editor', ]; try { new \ProBlockBuilder\TrustedLogin\Client( new \ProBlockBuilder\TrustedLogin\Config( $config ) ); } catch ( \Exception $exception ) { error_log( $exception->getMessage() ); } } ); ``` #### Hooking the TrustedLogin Client instantiation We recommend instantiating the TrustedLogin Client on the `plugins_loaded` action. This ensures that the TrustedLogin Client is available on all page loads. TrustedLogin calls the following hooks: - `init` priority `100` - `admin_init` priority `100` - `template_redirect` priority `99` Instantiating the Client after any of these hooks are called will cause TrustedLogin to not function properly. :::warning **Always wrap TrustedLogin Client instantiation in a try/catch block!** ::: TrustedLogin Client instantiation must be wrapped in a try/catch block. The TrustedLogin Client throws Exceptions when: - The configuration is invalid. - TrustedLogin is globally disabled. - TrustedLogin is disabled for the namespace. - The current website lacks expected encryption functions (these _should_ be included in WordPress 5.2+ and PHP 7.2+). Wrapping the instantiation in a try/catch block ensures that the site won't crash if TrustedLogin fails to initialize. ------ ## Advanced ### Testing on local environments {#testing-on-local-environments} TrustedLogin won't work in local environments unless using a tunnel such as ngrok. Thus, TrustedLogin will display a warning when attempting to generate a login when in a local environment. To disable the warning, define `TRUSTEDLOGIN_DISABLE_LOCAL_NOTICE` and set it to true: ```php define( 'TRUSTEDLOGIN_DISABLE_LOCAL_NOTICE', true ); ``` --- # Configuring the TrustedLogin Client SDK Source: https://docs.trustedlogin.com/Client/configuration Markdown: https://docs.trustedlogin.com/Client/configuration.md # Client Configuration ## Minimal configuration {#minimal-configuration} When instantiating the TrustedLogin `Client` class, you need to pass a valid `Config` object. The following is a minimal configuration. It has all the _required_ settings, but not all **recommended** settings! ```php $config = [ 'auth' => [ 'api_key' => '1234567890', ], 'vendor' => [ 'namespace' => 'pro-block-builder', 'title' => 'Pro Block Builder', 'email' => 'support@example.com', 'website' => 'https://example.com', 'support_url' => 'https://help.example.com', ], 'role' => 'editor', ]; ``` :::note ### When you see `ProBlockBuilder`, this is a placeholder. Make sure to replace with your own namespace! {#when-you-see-problockbuilder-make-sure-to-replace-with-your-own-namespace} In the examples on this page, we're going to pretend your plugin or theme is named "Pro Block Builder" and your business is named Widgets, Co. These should not be the names you use—make sure to update the sample code below to match your business and plugin/theme name! ::: ## A more complete configuration {#a-more-complete-configuration} The following is a more complete configuration. It includes all the settings that can be configured. ```php $config = [ 'auth' => [ 'api_key' => '12345678', ], 'vendor' => [ 'namespace' => 'pro-block-builder', 'title' => 'Pro Block Builder', 'email' => 'support@example.com', 'website' => 'https://example.com', 'support_url' => 'https://help.example.com', 'logo_url' => plugins_url( 'path/to/logo/image/logo.svg', EXAMPLE_PBB_PLUGIN_FILE_PATH ), ], 'role' => 'editor', 'clone_role' => false, 'menu' => [ 'slug' => 'example', // This is the `page` attribute of top-level menu item (eg: `admin.php?page=example`). 'title' => esc_html__( 'Grant Site Access', 'pro-block-builder' ), ], 'webhook' => [ 'url' => 'https://hooks.zapier.com/hooks/catch/12345/example/silent/', // Zapier webhook URL. 'debug_data' => true, 'create_ticket' => true, 'format' => 'json', // Use 'json' to send JSON-formatted requests instead of form-encoded. ], ]; ``` ## All configuration options {#all-options} | Key | Type | Description | Default | Required? | |-------------------------|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|:---------:| | `auth/api_key` | `string` | The TrustedLogin key for the vendor, found in "API Keys" on https://app.trustedlogin.com. | `null` | ✅ | | `auth/license_key` | `string`, `null` | If enabled, the license key for the current client. This is used as a lookup value when integrating with help desk support widgets. If not defined, a cryptographic hash will be generated to use as the Access Key. | `null` | | | `role` | `string` | The role to clone when creating a new Support User. | `editor` | ✅ | | `clone_role` | `bool` | If true, create a new role with the same capabilites as the `role` setting. If false, use the defined `role` setting. | `true` | | | `vendor/namespace` | `string` | Slug for vendor. Must be unique. Must be shorter than 96 characters. Cannot be a reserved namespace. ([learn more about the vendor namespace setting below](#vendor-namespace)) | `null` | ✅ | | `vendor/title` | `string` | Name of the vendor company. Used in text such as `Visit the %s website` | `null` | ✅ | | `vendor/email` | `string` | Email address for support. Used when creating usernames. Recommended: use `{hash}` dynamic replacement ([see below](#task-specific-email-addresses)). | `null` | ✅ | | `vendor/website` | `string` | URL to the vendor website. Must be a valid URL. | `null` | ✅ | | `vendor/support_url` | `string` | URL to the vendor support page. Shown to users in the Grant Access form and also serves as a backup to redirect users if the TrustedLogin server is unreachable. Must be a valid URL. | `null` | ✅ | | `vendor/display_name` | `string` | Optional. Display name for the support team. See "Display Name vs Title" below. | `null` | | | `vendor/logo_url` | `string` | Optional. URL to the vendor logo. Displayed in the Grant Access form. May be inline SVG. Must be local to comply with WordPress.org. | `null` | | | `caps/add` | `array` | An array of additional capabilities to be granted to the Support User after their user role is cloned based on the `role` setting.

The key is the capability slug and the value is the reason why it is needed. Example: `[ 'gf_full_access' => 'Support will need to see and edit the forms, entries, and Gravity Forms settings on your site.' ]` | `[]` | | | `caps/remove` | `array` | An array of capabilities you want to _remove_ from Support User. If you want to remove access to WooCommerce, for example, you could remove the `manage_woocommerce` cap by using this setting: `[ 'manage_woocommerce' => 'We don\'t need to manage your shop!' ]`. | `[]` | | | `decay` | `int` | If defined, how long should support be granted access to the site? Defaults to a week in seconds (`604800`). Minimum: 1 day (`86400`). Maximum: 30 days (`2592000`). If `decay` is not defined, support access will not expire. | `604800` | | | `menu/slug` | `string`,`null`,`false` | TrustedLogin adds a submenu item to the sidebar in the Dashboard. The `menu/slug` setting is the slug name for the parent menu (or the file name of a standard WordPress admin page). If `null`, the a top-level menu will be added. If `false`, a menu item will not be added. If a string, it will be used as the `$parent_slug` argument passed to the [`add_submenu_page()` function](https://developer.wordpress.org/reference/functions/add_submenu_page/). | `null` | | | `menu/title` | `string` | The title of the submenu in the sidebar menu. | `Grant Support Access` | | | `menu/icon_url` | `string` | The URL to the icon to be used for this menu. The value is passed as `$icon_url` to the [`add_menu_page()` function](https://developer.wordpress.org/reference/functions/add_menu_page/). | `''` (empty string) | | | `menu/priority` | `int` | The priority of the `admin_menu` action used by TrustedLogin. | `100` | | | `menu/position` | `int`, `float`, `null` | The `$position` argument passed to the [`add_submenu_page()` function](https://developer.wordpress.org/reference/functions/add_submenu_page/) function. | `null` | | | `logging/enabled` | `bool` | If enabled, logs are stored in `wp-content/uploads/trustedlogin-logs` | `false` | | | `logging/directory` | `null`,`string` | Override the directory where logs are stored. See [Logging](logging/) for more information. | `null` | | | `logging/threshold` | `string` | Define what [PSR log level](https://www.php-fig.org/psr/psr-3/#5-psrlogloglevel) should be logged. To log everything, set the threshold to `debug`. | `notice` | | | `logging/options` | `array` | Array of [KLogger Additional Options](https://github.com/katzgrau/klogger#additional-options) | `['extension' => 'log', 'dateFormat' => 'Y-m-d G:i:s.u', 'filename' => null, 'flushFrequency' => false, 'logFormat' => false, 'appendContext' => true ]` | | | `paths/css` | `string` | Where to load CSS assets from. By default, the bundled TrustedLogin CSS file will be used. Must be local to comply with WordPress.org. | `{plugin_dir_url() to Config.php}/assets/trustedlogin.css` | | | `paths/js` | `string` | Where to load JS assets from. By default, the bundled TrustedLogin JS file will be used. Must be local to comply with WordPress.org. | `{plugin_dir_url() to Config.php}/assets/trustedlogin.js` | | | `reassign_posts` | `bool` | When the Support User is revoked, should posts & pages be re-assigned to a site administrator? If `false`, posts and pages created by the user will be deleted. Passed as the second argument to [the `wp_delete_user()` function](https://developer.wordpress.org/reference/functions/wp_delete_user/).

When `reassign_posts` setting is enabled, TrustedLogin will attempt to assign posts created by the user to the best-guess administrator: the user with the longest-active `administrator` role. | `true` | | | `require_ssl` | `bool` | Whether to use TrustedLogin when the site isn't served over HTTPS. TrustedLogin will still work, but the requests may not be secure. If `false`, the TrustedLogin "Grant Access" button will take users to the `vendor/support_url` URL directly. | `true` | | | `terms_of_service/url` | `null`,`string` | The URL to the vendor's Terms of Service. If defined, a message "By granting access, you agree to the Terms of Service." will be added below the Grant Access button. Added in 1.6.0. | `null` | | | `webhook/url` | `string` | If defined, TrustedLogin will send a `POST` request to the defined URL. Must be a valid URL if defined. See the Webhooks section below. | `null` | | | `webhook/debug_data` | `bool` | Whether to show the user an opt-in consent checkbox to send debugging data (the Site Health report) to the Webhook. Only relevant if `webhook/url` is defined and a valid URL. | `false` | | | `webhook/create_ticket` | `bool` | Whether to show the user a form to send a message to support via the Webhook. Only relevant if `webhook/url` is defined and a valid URL. | `false` | | | `webhook/format` | `string` | The format to use when sending webhook data: `'form'` (default) for form-encoded data, or `'json'` for JSON-encoded data with `Content-Type: application/json` header. JSON format is useful for endpoints expecting JSON payloads and also helps avoid false positive XSS blocks from security plugins. Added in 1.9.1. | `'form'` | | ## Logging {#logging} **We recommend disabling logging.** When logging is enabled, TrustedLogin logs to the `wp-content/uploads/trustedlogin-logs/` directory by default. 1. TrustedLogin creates a `trustedlogin-logs` directory inside the `wp-content/uploads/` directory. 2. An empty `index.html` file is placed inside the directory to prevent browsing. 3. New log files are created daily for each TrustedLogin namespace. The default log `filename` format is `client-debug-{Y-m-d}-{hash}.log` - `{namespace}` is the namespace of your business, plugin, or theme name - `{date}` is `YYYY-MM-DD` format - The hash is generated using `wp_hash()` using on the `vendor/namespace`, site `home_url()`, and the day of the year (`date('z')`). The point of the hash is to make log names harder to guess (security by obscurity). ### Using your own logging library {#using-your-own-logging-library} If you add an action for `trustedlogin/{namespace}/logging/log`, TrustedLogin will let you handle logging. The `trustedlogin-logs` directory and log files will not be created. ### Default settings: {#default-settings} ```php 'logging' => [ 'enabled' => false, 'directory' => null, 'threshold' => 'debug', 'options' => [], ], ``` ### logging/enabled {#loggingenabled} _Optional._ Default: `false` Whether to enable logging TrustedLogin activity to a file. Helpful for debugging. To enable logging in TrustedLogin, the minimum configuration necessary is: ```php 'logging' => [ 'enabled' => true, ], ``` ### `logging/directory` {#loggingdirectory} _Optional._ Default: `null` If `null`, TrustedLogin will generate its own directory inside the `wp-content/uploads/` directory. The path for logs is `/wp-content/uploads/trustedlogin-logs/`. Created directories are protected by an `index.html` file to prevent browsing. ### `logging/threshold` {#loggingthreshold} _Optional._ Default: `debug` This setting defines the level of logging that should be recorded. The default setting if logging is to record all logs (`debug`). The available options include the logging levels defined in [PSR-3 `Psr\Log\LogLevel`](https://www.php-fig.org/psr/psr-3/#5-psrlogloglevel): - `emergency` - `alert` - `critical` - `error` - `warning` - `notice` - `info` - `debug` Setting `logging/threshold` to `error` will record logs with the level of `error` and above (`error`, `critical`, `alert`, and `emergency`). ### `logging/options` {#loggingoptions} _Optional._ Default: `[]` This setting can be used to pass additional options to the `Logger` class. The TrustedLogin Logger class is based on KLogger. See [the KLogger docs for more information](https://github.com/katzgrau/KLogger#additional-options). ### Log file names {#log-file-names} There is one log file generated per day. Log file names use a hash to make them more secure by obscurity, in this format: `trustedlogin-debug-{{Date in Y-m-d format}}-{{hash}}.log` Example: `trustedlogin-debug-2020-07-27-39dabe12636f200938bbe8790c0aef94.log` ## Vendor namespaces {#vendor-namespace} Namespaces are used to isolate your TrustedLogin configuration from other plugins or themes that may also use TrustedLogin. They must be unique. Some of the places the namespace is used include: - The default dashboard URL for the Grant Access page: `/wp-admin/admin.php?page=grant-{namespace}-access` - The WP Login Grant access URL: `wp-login.php?action=trustedlogin&ns={namespace}` - The user role created for support access: `{namespace}-support` - The CSS classes used in the Grant Access form - Actions and filters (eg: `trustedlogin/{namespace}/access/created`) - The name of the constant to disable TrustedLogin (eg: `TRUSTEDLOGIN_DISABLE_{NAMESPACE}`) - As part of a hash for the log file name ### Reserved namespaces {#reserved-namespaces} The following namespaces are reserved and cannot be used: - `trustedlogin` - `trusted-login` - `client` - `vendor` - `admin` - `administrator` - `wordpress` - `support` ## Display Name vs Title {#display-name-vs-title} If `vendor/title` is set to `GravityView`, the default confirmation screen will say `Grant GravityView access to your site.` When `vendor/display_name` is also defined, the text will read `GravityView Support`, the default confirmation screen will say `Grant GravityView Support access to your site.` ## Task-specific email addresses {#task-specific-email-addresses} In order to prevent email address collision, we recommend using "plus addresses" (also called "task-specific email addresses") for your `vendor/email` setting. Rather than `support@example.com`, use `support+{hash}@example.com`. `{hash}` will be dynamically replaced when used in the email address. This is supported by many email providers, including [Gmail](https://docs.microsoft.com/en-us/exchange/recipients-in-exchange-online/plus-addressing-in-exchange-online), [Microsoft](https://docs.microsoft.com/en-us/exchange/recipients-in-exchange-online/plus-addressing-in-exchange-online), [Fastmail](https://www.fastmail.com/help/receive/addressing.html), and [ProtonMail](https://protonmail.com/support/knowledge-base/creating-aliases/). ## Invalid capabilities {#invalid-capabilities} The Support User will be created based on the role defined in the configuration (see configuration above). The following capabilities are never allowed when creating users through TrustedLogin, regardless of the role: - `create_users` - `delete_users` - `edit_users` - `promote_users` - `delete_site` - `remove_users` A goal for TrustedLogin is to instill confidence in the end user that they are not creating security holes when granting support access to their site. ## Webhooks {#webhooks} If the `webhook_url` setting is set and is a valid URL, the URL will be sent a `POST` request when creating a Support User, extending access, or revoking access. | Key | Type | Description | |--------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `url` | `string` | The site URL from where the webhook was triggered, as returned by `get_site_url()` | | `action` | `string` | The type of trigger: `created`, `extended`, or `logged_in`, or `revoked` | | `ref` | `string`,`null` | A sanitized reference ID, if passed. Otherwise, null. | | `debug_data` | `string` | The Site Health report in Markdown formatting. This key is only set for the `trustedlogin/{namespace}/access/created` action, and only if the user opted-in. Added in 1.4.0. | | `ticket` | `array` | The unmodified message created by the user. This key is only set for the `trustedlogin/{namespace}/access/created` action, and only if the message is not empty. Added in 1.5.0. | The default actions that trigger webhooks to run are: - `trustedlogin/{namespace}/access/created` - `trustedlogin/{namespace}/access/extended` - `trustedlogin/{namespace}/access/revoked` - `trustedlogin/{namespace}/logged_in` See [hook documentation](/Client/hooks). --- # Template Customization Source: https://docs.trustedlogin.com/Client/customization Markdown: https://docs.trustedlogin.com/Client/customization.md # Customizing the TrustedLogin Template The TrustedLogin template is designed to be easily customized to match your brand. This guide will walk you through the steps to customize the template. [Reference the hooks](hooks#trustedloginnamespacetemplateauth) doc for more information on how to customize the template using hooks. :::tip By removing placeholders you don't need, or replacing the placeholders with your preferred HTML, you can customize all output generated by the TrustedLogin Client. ::: ## The Grant Support Access Template You can modify the Grant Support Access auth form by using the [`trustedlogin/{namespace}/template/auth` filter](hooks#trustedloginnamespacetemplateauth). This is the default HTML structure of the Grant Support Access form: ```html
{{header}}

{{intro}}

{{auth_header}}
{{details}}
{{notices}}
{{button}}
{{terms_of_service}}
{{secured_by_trustedlogin}}
{{admin_debug}}
``` ### `{{header}}` placeholder The header of the auth form outputs the logo of the vendor. It is only displayed on the Grant Auth screen, not on the `wp-login.php` screen. ```html
``` #### Screenshots ![A screenshot of the Grant Support Access form with the header highlighted.](/img/client/form/header.png) ### `{{intro}}` placeholder The `{{intro}}` placeholder is the introductory text displayed at the top of the Grant Support Access form. Based on the context, the `{{intro}}` placeholder will be replaced with one of the following: - **Access has been granted:**
`Vendor Display Name has site access that expires in [expiration time].` - **On the login screen:**
`Vendor Display Name would like support access to this site.` - **On the Grant Support Access screen:**
`Grant Vendor Display Name access to this site.` #### Screenshots ![A screenshot of the Grant Support Access form with the intro highlighted.](/img/client/form/intro.png) ### `{{auth_header}}` placeholder If there are no active Support Users, this placeholder will not be rendered. If there are, the auth header shows the display name of the Support User, information about who granted access, and the Revoke Access button. #### Screenshot ![A screenshot of the Grant Support Access form with the placeholder parts highlighted.](/img/client/form/auth-header.png) ### `{{details}}` placeholder The `{{details}}` placeholder contains the bulk of the content of the Grant Support Access form. #### Screenshot ![A screenshot of the Grant Support Access form with the placeholder parts highlighted.](/img/client/form/details.png) ```html

This will allow {{name}} to:

{{roles_summary}}

{{caps}}

{{expire_summary}}{{expire_desc}}

``` #### `{{roles_summary}}` placeholder If cloning roles is disabled (using the `clone_role` configuration setting), the `{{roles_summary}}` placeholder is replaced by `Create a user with a role of {{role}}.`. ##### If cloning roles is enabled When cloning roles is enabled, `{{roles_summary}}` is replaced by `Create a user with a role based on {{cloned_role}}.`. Further, if custom capabilities are defined (using the `caps/remove` or `caps/add` configuration settings) are set, the `{{caps}}` placeholder will be rendered. #### Screenshot ![The capabilities display, showing additional capabilities that have been added or removed from the cloned role.](/img/client/form/caps.png) ### `{{notices}}` placeholder The `{{notices}}` placeholder is displayed when TrustedLogin is running on a local website that will not be accessible to support. It is disabled when `wp_get_environment_type()` is "staging" or "production", so it will not be displayed on a live site. #### Screenshot ![A screenshot of the Grant Support Access form with the notice circled with a green border.](/img/client/form/notices.png) #### Settings available - `vendor/about_live_access_url` - The URL to the vendor's documentation about live access. Defaults to `https://www.trustedlogin.com/about/live-access/`. #### Constants available `TRUSTEDLOGIN_DISABLE_LOCAL_NOTICE` - Set to `true` to disable the local notice. ### `{{button}}` placeholder The button to grant or extend access to the support user. Generated by the `TrustedLogin\Form::generate_button()` placeholder. - If access **has** been granted to the website, the button text will be "Extend [Vendor Display Name] Support Access". - If access **has not** been granted, the button text will be "Grant [Vendor Display Name] Support Access". Here is sample HTML output for the button: #### Screenshot ```html Grant [Vendor Display Name] Support Access ``` ![A screenshot of the Grant Support Access form with the button circled with a green border.](/img/client/form/button.png) ### `{{reference}}` placeholder If reference IDs are being displayed (controlled by the [`trustedlogin/{namespace}/template/auth/display_reference`](hooks#trustedloginnamespacetemplateauthdisplay_reference) filter, render the reference ouput. ```html

{{reference_text}}

``` #### Screenshots ![A screenshot of the Grant Support Access form with the reference ID section circled with a green border.](/img/client/form/reference.png) #### Filters available - [`trustedlogin/{namespace}/template/auth/display_reference`](hooks#trustedloginnamespacetemplateauthdisplay_reference) filter to control whether the reference ID is displayed. ### `{{terms_of_service}}` placeholder If the [`terms_of_service/url` setting](configuration#all-options) is not defined, the terms of service output will not be rendered. If there is a URL defined for Terms of Service, a link to terms of service will be rendered. The anchor text defaults to "Terms of Service". #### HTML output ```html

Terms of Service

``` #### Screenshots ![A screenshot of the Grant Support Access form with the Terms of Service link circled with a green border.](/img/client/form/tos.png) #### Available filters - [`trustedlogin/{namespace}/template/auth/terms_of_service/anchor`](hooks#trustedloginnamespacetemplateauthterms_of_serviceanchor) filter to modify the "Terms of Service" anchor text. ### `{{secured_by_trustedlogin}}` placeholder The "Secured by TrustedLogin" text. #### HTML output ```html

{{secured_by_trustedlogin}}

``` #### Screenshots ![A screenshot of the Grant Support Access form with the "Secured by TrustedLogin" text circled with a green border.](/img/client/form/secured-by.png) ### `{{admin_debug}}` placeholder The admin debug output. Only displayed if the user has `manage_options` capability and `$_GET['debug']` is set. - TrustedLogin Status: `Online` or `Offline` - API Key: The API key used to authenticate with the TrustedLogin API - License Key: If a license key is set, it will be displayed here - Log URL: A link to download the log file - Log Level: The log level set in the TrustedLogin settings - Webhook URL: The URL to the webhook endpoint, if set. `Empty` if not set. - Vendor Public Key: The public encryption key of the vendor, with a link to verify the key #### Screenshots ![A screenshot of the Grant Support Access form with the admin debug section circled with a green border.](/img/client/form/admin-debug.png) ## Examples of Customization ### Removing the Header To customize the Grant Support Access form, you can use the `trustedlogin/{namespace}/template/auth` filter. Here is an example of how to customize the Grant Support Access form: ```php // Replace `{namespace}` with the namespace of your configuration. add_filter( 'trustedlogin/{namespace}/template/auth', 'RENAME_THIS_FUNCTION_remove_header', 10 ); /** * Remove the header, including the logo, from the Grant Support Access form. * * This is an example function name! Replace `RENAME_THIS_FUNCTION_remove_header` with a unique function name. * * @param string $auth_screen_template The HTML template of the Grant Support Access form. * @return string */ function RENAME_THIS_FUNCTION_remove_header( $auth_screen_template ) { return str_replace( '{{header}}', '', $auth_screen_template ); } ``` --- # Developer FAQ Source: https://docs.trustedlogin.com/Client/dev-faq Markdown: https://docs.trustedlogin.com/Client/dev-faq.md # Developer FAQ ## How do I render the authorization screen? {#how-do-i-render-the-authorization-screen} You can trigger the `trustedlogin/{namespace}/auth_screen` action to render the authorization screen. The proper JS and CSS files will be enqueued automatically: ```php

My Plugin

This is a page inside my plugin.

More content here.

``` ## What happens if TrustedLogin service is down? {#what-happens-if-trustedlogin-service-is-down} If the TrustedLogin service is down, the user will be presented with a button to contact support. That button points to the the Support URL (`vendor/support_url`) setting passed to the [`Config` object](configuration/). ## If my `vendor/namespace` isn't unique, what happens? {#if-my-vendornamespace-isnt-unique-what-happens} There will be an issue generating the login screen, but it will cause no security problems. The namespace is not used in encryption or when generating the requests to your website. ## WordPress.org compliance {#wordpressorg-compliance} TrustedLogin requires user action to provide logins. This is in compliance with WordPress.org. All files (vendor logo, CSS, and JS files) must be local (using `plugin_dir_url()` or similar) to comply with WordPress.org rules. --- # User FAQ Source: https://docs.trustedlogin.com/Client/faq Markdown: https://docs.trustedlogin.com/Client/faq.md # User FAQ ## What user data does TrustedLogin collect? {#what-user-data-does-trustedlogin-collect} The only user data TrustedLogin stores is the website URL. The website URL is stored unencrypted in our database. Other than that, we do not collect any user data. When a support user logs into the website, we store the user's WordPress ID on the Vendor's website. ## What data is in the troubleshooting report shared with Vendors? {#what-data-is-in-the-troubleshooting-report-shared-with-vendors} If you check the "Include the Site Health information" checkbox, the data generated by the WordPress Site Health tool is shared with the Vendor. [Learn more about the shared information](https://wordpress.org/documentation/article/site-health-screen/#info). By default, Site Health information does not include any personally-identifiable information. However, if you have installed a plugin that adds personally-identifiable information to the Site Health information, that information will be shared with the Vendor. **Some things to note about the troubleshooting report:** - The data is never received by TrustedLogin - The Vendor defines where this information is sent - TrustedLogin does not control the website that receives the debugging data ## What happens if I don't want to use TrustedLogin on my website? {#what-happens-if-i-dont-want-to-use-trustedlogin-on-my-website} To disable TrustedLogin globally, define a `TRUSTEDLOGIN_DISABLE` constant in the site's `wp-config.php` file. That will prevent all code that uses TrustedLogin from loading TrustedLogin. To prevent a single TrustedLogin installation, you will need to know the namespace. Once you have the namespace, define a `TRUSTEDLOGIN_DISABLE_{NAMESPACE}` constant in the site's `wp-config.php` file. The namespace must be in all caps. ## Is TrustedLogin WordPress.org compliant? {#is-trustedlogin-wordpressorg-compliant} Yes. TrustedLogin requires user action to provide logins. This is in compliance with WordPress.org. When distributing TrustedLogin on WordPress.org, all files (logo, CSS, and JS files) must be local (using `plugin_dir_url()` or similar) to comply with WordPress.org rules. --- # Hooks Source: https://docs.trustedlogin.com/Client/hooks Markdown: https://docs.trustedlogin.com/Client/hooks.md # Hooks Below are all hooks available in the TrustedLogin Client. These hooks can be used to customize the behavior of the Client. ## Actions {#actions} ### `trustedlogin/{namespace}/auth_screen` {#trustedloginnamespaceauthscreen} Renders the Grant Access/Revoke Access screen. Note: TrustedLogin uses the `20` priority to print the auth screen. ### `trustedlogin/{namespace}/logging/log` {#trustedloginnamespacelogginglog} | Parameter | Type | Description | |------------|----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `$message` | `string` | Message to log. Pre-processed to convert `WP_Error` and exceptions into strings. | | `$method` | `string` | Method that called the hook. | | `$level` | `string` | A [PSR-3 log level](https://github.com/php-fig/log/blob/master/Psr/Log/LogLevel.php) ('emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug') | | `$data` | `array` | Additional error data. | ### `trustedlogin/{namespace}/login/before` {#trustedloginnamespaceloginbefore} The support user login flow has begun. **This is run before validation.** If you want to a hook that runs only after successful login, use [`trustedlogin/{namespace}/login/after`](#trustedloginnamespaceloginafter) instead. | Key | Type | Description | |--------------------|----------|-------------------------------------| | `$user_identifier` | `string` | Unique identifier for support user. ### `trustedlogin/{namespace}/login/refused` {#trustedloginnamespaceloginrefused} The identifier fails security checks. | Key | Type | Description | |--------------------|------------|------------------------------------------------------| | `$user_identifier` | `string` | Unique identifier for support user. | `$is_verified` | `WP_Error` | The error encountered when verifying the identifier. Can be triggered with the following error codes: - `brute_force_detected`: Due to the current request triggering brute force checks, the site has entered lockdown mode. - `in_lockdown`: The site is currently in lockdown mode for a period of time. ### `trustedlogin/{namespace}/login/error` {#trustedloginnamespaceloginerror} A support user fails to log in. | Key | Type | Description | |--------------------|------------|------------------------------------------------------| | `$user_identifier` | `string` | Unique identifier for support user. | `$is_verified` | `WP_Error` | The error encountered when verifying the identifier. Can be triggered with the following error codes: - `user_not_found`: There is no longer an existing support user (perhaps possibly because access has been revoked) - `access_expired`: Access has expired due to configuration expiration settings ### `trustedlogin/{namespace}/login/after` {#trustedloginnamespaceloginafter} A support user has logged-in. | Key | Type | Description | |--------------------|----------|-------------------------------------| | `$user_identifier` | `string` | Unique identifier for support user. | ### `trustedlogin/{namespace}/access/created` {#trustedloginnamespaceaccesscreated} Access has been granted. | Key | Type | Description | |---------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `$url` | `string` | The site URL from where the access was granted, as returned by `get_site_url()` | | `$action` | `string` | The type of trigger: `created`, `extended`, or `revoked` | | `$ref` | `string`,`null` | A sanitized reference ID, if passed. Otherwise, null. | | `$debug_data` | `array` | (Not always set) An array of site data generated by `Client::get_debug_data()`. Similar to `\WP_Debug_Data::debug_data()` output with Markdown formatting improvements applied. Added in 1.4.0. | ### `trustedlogin/{namespace}/access/extended` {#trustedloginnamespaceaccessextended} Existing access has been extended. | Key | Type | Description | |-----------|-----------------|------------------------------------------------------------------------------------| | `$url` | `string` | The site URL from where the webhook was triggered, as returned by `get_site_url()` | | `$action` | `string` | The type of trigger: `created`, `extended`, or `revoked` | | `$ref` | `string`,`null` | A sanitized reference ID, if passed. Otherwise, null. ### `trustedlogin/{namespace}/access/revoked` {#trustedloginnamespaceaccessrevoked} Access has been revoked. | Key | Type | Description | |-----------|----------|------------------------------------------------------------------------------------| | `$url` | `string` | The site URL from where the webhook was triggered, as returned by `get_site_url()` | | `$action` | `string` | The type of trigger: `created`, `extended`, or `revoked` | ### `trustedlogin/{namespace}/logged_in` {#trustedloginnamespacelogged_in} A support user has logged-in to a site. | Key | Type | Description | |-----------|----------|------------------------------------------------------------------------------------| | `$url` | `string` | The site URL from where the webhook was triggered, as returned by `get_site_url()` | | `$action` | `string` | Set to `logged_in` ### `trustedlogin/{namespace}/logging/log_{level}` {#trustedloginnamespaceloggingloglevel} Per-level variant of [`trustedlogin/{namespace}/logging/log`](#trustedloginnamespacelogginglog). Fires after the base `logging/log` action, using the PSR-3 level as a dynamic suffix (e.g. `logging/log_error`, `logging/log_warning`). Useful if you only want to observe a single level without filtering inside a generic handler. | Parameter | Type | Description | |------------|----------|:---------------------------------------------------------------------------------| | `$message` | `string` | Message to log. Pre-processed to convert `WP_Error` and exceptions into strings. | | `$method` | `string` | Method that called the hook. | | `$data` | `array` | Additional error data. | ### `trustedlogin/{namespace}/access/revoke` {#trustedloginnamespaceaccessrevoke} Fires when the revoke endpoint is hit but **before** the support user is actually deleted. Use this to run cleanup that must precede user removal. If revocation fails, the companion [`trustedlogin/{namespace}/admin/access_revoked`](#trustedloginnamespaceadminaccess_revoked) does NOT fire — so a listener on this hook is the last guaranteed signal that revocation was attempted. :::note Distinct from [`trustedlogin/{namespace}/access/revoked`](#trustedloginnamespaceaccessrevoked), which fires once per webhook event after successful revocation. ::: | Key | Type | Description | |--------------------|----------|-------------------------------------------------------------------------| | `$user_identifier` | `string` | Unique ID for the support user, or the literal string `"all"`. | ### `trustedlogin/{namespace}/admin/access_revoked` {#trustedloginnamespaceadminaccess_revoked} Fires **only** when revocation triggered from the admin revoke link has fully succeeded — the support user no longer exists with the given identifier. Does not fire if user deletion failed. | Key | Type | Description | |--------------------|----------|-------------------------------------------------------------------------| | `$user_identifier` | `string` | Unique ID for the support user, or the literal string `"all"`. | ### `trustedlogin/{namespace}/lockdown/after` {#trustedloginnamespacelockdownafter} Fires after the site enters lockdown mode due to repeated failed login attempts. Use this hook to notify administrators, trigger additional security measures, or log security events beyond the built-in TrustedLogin reporting. Lockdown is triggered by the brute-force detection in `SecurityChecks`; this action runs after the lockdown transient has been set and the vendor has been notified (when configured). It passes no arguments — call `SecurityChecks` directly if you need lockdown metadata. ## Filters {#filters} ### `trustedlogin/{namespace}/admin/menu/menu_slug` {#trustedloginnamespaceadminmenuslug} Override the menu slug used for the Grant Support Access screen. | Key | Type | Default | Description | |--------------|----------|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| | `$menu_slug` | `string` | `'grant-{namespace}-access'` | Value passed to [`add_menu_page()`](https://developer.wordpress.org/reference/functions/add_menu_page/) as the `$menu_slug` parameter. | ### `trustedlogin/{namespace}/template/auth` {#trustedloginnamespacetemplateauth} Override the structure of the auth form HTML. | Key | Type | Default | Description | |-------------------------|----------|------------------------------------|----------------------------------------------------------| | `$auth_screen_template` | `string` | HTML with placeholders (see below) | The structure for the auth form HTML, with placeholders. | ### `trustedlogin/{namespace}/template/auth/display_reference` {#trustedloginnamespacetemplateauthdisplay_reference} Toggles whether the reference ID, if set, is shown in the auth form. Since Version 1.3. | Key | Type | Default | Description | |----------------------|----------|---------|---------------------------------------------------------------------------------------------| | `$display_reference` | `bool` | `true` | Whether to display the reference ID on the auth screen. | | `$is_login_screen` | `bool` | | Whether the auth form is being displayed on the login screen. Set by Admin::login_screen(). | | `$ref` | `string` | | The reference ID. | ### `trustedlogin/{namespace}/template/auth/terms_of_service/anchor` {#trustedloginnamespacetemplateauthterms_of_serviceanchor} | Key | Type | Default | Description | |-----------|----------|--------------------|-----------------------------------------------| | `$anchor` | `string` | `Terms of Service` | The anchor text of the Terms of Service link. | ### `trustedlogin/{namespace}/template/auth/footer_links` {#trustedloginnamespacetemplateauthfooter_links} Override the footer links shown below the auth form. | Key | Type | Default | Description | |-----------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| | `$footer_links` | `array` | `[ 'Learn About TrustedLogin' => 'https://www.trustedlogin.com/about/easy-and-safe/', 'Visit {vendor/title} support' => {vendor/support_url} ]` | Array of links to show in auth footer (Key is anchor text; Value is URL) | ### `trustedlogin/{namespace}/support_role` {#trustedloginnamespacesupport_role} Change the name (slug) of the role created for the support user. Will be sanitized using `sanitize_title_with_dashes()`. | Key | Type | Default | Description | |--------------|-----------------------|-------------------------------------------|----------------------------------------------------| | `$role_name` | `string` | `'{namespace}-support'` | The name of the role, which is more like the slug. | | `$config` | `TrustedLogin\Config` | Current TrustedLogin configuration object | ### `trustedlogin/{namespace}/support_role/display_name` {#trustedloginnamespacesupport_roledisplay_name} Change the display name of the role created for support users. This will be displayed in the filter menu above the Users table in the Dashboard, as well as in a list of site roles. | Key | Type | Default | Description | |-----------------|-----------------------|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| | `$display_name` | `string` | `'%s Support'` | A string prepared for localization where `%s` is replaced by the `vendor/title` configuration setting (for example, "Acme Support"). | | `$config` | `TrustedLogin\Config` | Current TrustedLogin configuration object | ### `trustedlogin/{namespace}/license_key` {#trustedloginnamespacelicense_key} Modify the license key assigned to the current user. This should ideally be defined using the `auth/license_key` configuration setting. | Key | Type | Default | Description | |----------------|------------------|---------|-------------------------------------------| | `$license_key` | `string`, `null` | `null` | Get the license key for the current user. | ### `trustedlogin/{namespace}/support_url/query_args` {#trustedloginnamespacesupport_urlquery_args} If TrustedLogin fails to grant access to users, a button appears that will link directly to the `vendor/support_url` configuration setting. This filter exists to modify parameters added to that URL. | Key | Type | Default | Description | |---------------|---------|------------------------------|-------------| | `$query_args` | `array` | See default array keys below | | #### $query_args array value {#query_args-array-value} | Key | Type | Default | Description | |----------------------|------------------|-----------------------------------------|-----------------------------------------------------------| | `query_args/message` | `string` | `Could not create TrustedLogin access.` | What error message should be appended to the support URL. | | `query_args/ref` | `string`, `null` | `null` | A sanitized reference ID, if passed. Otherwise, null. | ### `trustedlogin/{namespace}/login_feedback/allowed_referer_urls` {#trustedloginnamespaceloginfeedbackallowedrefererurls} Trusted URLs whose **hosts** are accepted as the `Referer` of a failed support-login POST. When a match is found, the matched URL — not the raw Referer — is rendered as the "Go back" link on the feedback screen, so an attacker who forges a matching host still can't control the path or query string. Vendors with multiple surfaces (marketing site, support portal, white-label domains, staging) should add their additional URLs here. :::tip Return an empty array to disable the "Go back" link entirely. ::: | Key | Type | Default | Description | |-----------------|-----------------------|--------------------------------------------------------------------------|------------------------------------------------------------------------------------------| | `$default_urls` | `string[]` | `[ 'vendor/website', 'vendor/support_url', home_url() ]` (non-empty values only) | Allowed URLs. Only the host component is compared; full URL is rendered as the link. | | `$config` | `TrustedLogin\Config` | Current TrustedLogin configuration object | Useful for namespace-aware extension. | #### Example: allow a support portal on a different domain ```php add_filter( 'trustedlogin/pro-block-builder/login_feedback/allowed_referer_urls', function( $urls, $config ) { $urls[] = 'https://help.problockbuilder.com'; $urls[] = 'https://status.problockbuilder.com'; return $urls; }, 10, 2 ); ``` ### `trustedlogin/{namespace}/envelope/meta` {#trustedloginnamespaceenvelopemeta} Adds custom metadata to be synced via TrustedLogin and stored in the Envelope. **Limited to 1MB.** :::warning Metadata is transferred and stored in plain text. **Do not add any unencrypted sensitive data or identifiable information**! ::: | Key | Type | Default | |-------------|-----------------------|-------------------------------------------| | `$metadata` | `array` | `[]` (empty array) | | `$config` | `TrustedLogin\Config` | Current TrustedLogin configuration object | ### `trustedlogin/{namespace}/logging/enabled` {#trustedloginnamespaceloggingenabled} Toggles whether logging is enabled. It can be helpful to have a filter to override logging outside the configuration array! | Key | Type | Default | Description | |---------------|--------|---------|---------------------------------------------------------| | `$is_enabled` | `bool` | `false` | Whether debug logging is enabled in TrustedLogin Client | ### `trustedlogin/{namespace}/webhook/request_args` {#trustedloginnamespacewebhookrequest_args} Filter the arguments passed to `wp_remote_post()` when the Client sends a webhook. Added in 1.9.1. Since 1.9.1 the Client JSON-encodes the webhook body and sends `Content-Type: application/json` by default. This avoids false-positive XSS blocks from security plugins (Wordfence, etc.) that flagged the legacy `debug_data=…` form-encoded body. The TrustedLogin Connector's REST endpoint accepts both shapes, so the change is drop-in. Use this filter if your custom webhook receiver requires a different shape or additional headers (e.g. a bearer token). | Key | Type | Default | Description | |----------------|----------|----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | `$args` | `array` | `[ 'body' => wp_json_encode($data), 'headers' => [ 'Content-Type' => 'application/json; charset=utf-8' ] ]` | Request arguments passed to `wp_remote_post()`. | | `$webhook_url` | `string` | | The webhook URL being posted to. | | `$data` | `array` | | The original data array being sent to the webhook. | #### Example: revert to legacy form encoding for a custom receiver that reads `$_POST` {#webhook-form-filter-example} ```php add_filter( 'trustedlogin/pro-block-builder/webhook/request_args', function ( $args, $webhook_url, $data ) { // Pre-1.9.1 shape: WP form-encodes the array body automatically. return array( 'body' => $data ); }, 10, 3 ); ``` #### Example: add a bearer token for an authenticated receiver {#webhook-auth-filter-example} ```php add_filter( 'trustedlogin/pro-block-builder/webhook/request_args', function ( $args, $webhook_url, $data ) { $args['headers']['Authorization'] = 'Bearer ' . getenv( 'MY_WEBHOOK_TOKEN' ); return $args; }, 10, 3 ); ``` ### `trustedlogin/{namespace}/vendor/public_key/website` {#trustedloginnamespacevendorpublic_keywebsite} :::warning Only use this filter if the `vendor/website` setting is not the same as the website where the TrustedLogin Connector plugin is running. ::: If the `vendor/website` setting and the website running the TrustedLogin Connector plugin are not the same, use this filter. For example, if the `vendor/website` setting is `https://www.parentcompany.com` but TrustedLogin is running on the `https://child.parentcompany.com`, you would use this filter to point to `https://child.parentcompany.com`. | Key | Type | Default | Description | |-----------------------|----------|--------------------------------------------|-----------------------------------------------------------------| | `$public_key_website` | `string` | The `vendor/website` configuration setting | The root URL of the website where the Connector plugin is running. | ### `trustedlogin/{namespace}/vendor/public_key/endpoint` {#trustedloginnamespacevendorpublic_keyendpoint} :::warning Only use this filter if the REST API endpoint has changed on the Vendor website. ::: Override the path to TrustedLogin's WordPress REST API endpoint. If there have been customizations to the REST API endpoint structure on the Vendor, the path may need to be modified. For example, if the [`rest_url_prefix` filter is used](https://developer.wordpress.org/reference/hooks/rest_url_prefix/) to change the REST API URL from `/wp-json/`, you will need to update the endpoint. | Key | Type | Default | Description | |------------------------|----------|--------------------------------------|----------------------------------------------------------------------------------------------------------------| | `$public_key_endpoint` | `string` | `wp-json/trustedlogin/v1/public_key` | The vendor's signature key REST API endpoint, which will be added to the vendor/website configuration setting. | ## 🛑 Advanced Internal Use Only {#-advanced-internal-use-only} :::warning These filters should not be used in production code. They are included here as helpful developer reference only, and they may change. ::: ### 🚫 You really don't need these filters! 🚫 {#-you-really-dont-need-these-filters-} Using these filters incorrectly may **break everything and make a site insecure**. They are used for advanced use cases like automated end-to-end testing. They're only included here so our documentation is complete. Only use if you know what you're doing! ### `trustedlogin/{namespace}/meets_ssl_requirement` {#trustedloginnamespacemeets_ssl_requirement} Logins will not be synced with TrustedLogin if the site doesn't have proper SSL support. Sometimes, when testing, it's helpful to have a filter to override this behavior. | Key | Type | Default | Description | |-----------|--------|--------------------------------------|------------------------------------------| | `$return` | `bool` | `is_ssl() && $config['require_ssl']` | Does this site meet the SSL requirement? | ### `trustedlogin/{namespace}/api_url` {#trustedloginnamespaceapi_url} Modifies the endpoint URL for the TrustedLogin service. This allows pointing requests to test servers. | Key | Type | Default | Description | |-------------|----------|----------------------------------------|-------------------------| | `$base_url` | `string` | `https://app.trustedlogin.com/api/v1/` | URL of TrustedLogin API | :::warning These filters should not be used in production code. They are included here as helpful developer reference only, and they may change. ::: ### `trustedlogin/{namespace}/vendor_public_key` {#trustedloginnamespacevendor_public_key} Override the public key functions. Encryption will break if this is changed. | Key | Type | Default | |---------------|-----------------------|-------------------------------------------| | `$remote_key` | `string` | Varies | The signature key returned by the Connector plugin's REST API endpoint, acessible at https://www.example.com/wp-json/trustedlogin/v1/signature_key | | `$config` | `TrustedLogin\Config` | Current TrustedLogin configuration object | :::warning These filters should not be used in production code. They are included here as helpful developer reference only, and they may change. ::: ### `trustedlogin/{namespace}/options/endpoint` {#trustedloginnamespaceoptionsendpoint} Modify the namespaced setting name for storing part of the auto-login endpoint. **The endpoint value must be treated carefully.** It is one of the two parts required to log in. | Key | Type | Default | |----------------|-----------------------|-------------------------------------------| | `$option_name` | `string` | `tl_{namespace}_endpoint` | The name for storing the endpoint value in the options table | | `$config` | `TrustedLogin\Config` | Current TrustedLogin configuration object | :::warning These filters should not be used in production code. They are included here as helpful developer reference only, and they may change. ::: ### `trustedlogin/{namespace}/options/vendor_public_key` {#trustedloginnamespaceoptionsvendor_public_key} Modify the site-option name used to cache the vendor's public key. Changing this will cause the cache to miss and the Client to re-fetch the key from the Connector; it will not change the key itself. Provided for disambiguation when a site runs multiple Client instances that must not share the cache. | Key | Type | Default | Description | |-----------------------------|-----------------------|-----------------------------------------|----------------------------------------------------------------------------------------| | `$vendor_public_key_option` | `string` | `tl_{namespace}_vendor_public_key` | The name used to store the cached vendor public key in the options table. | | `$config` | `TrustedLogin\Config` | Current TrustedLogin configuration object | For namespace-aware extension. | --- # AI Integration Prompt Source: https://docs.trustedlogin.com/Client/integration-prompt Markdown: https://docs.trustedlogin.com/Client/integration-prompt.md A self-contained, end-to-end walkthrough for integrating the TrustedLogin Client SDK into a WordPress plugin. Designed for use with AI coding assistants (Claude Code, Cursor, GitHub Copilot Chat, etc.), but readable as a thorough checklist for human integrators too. ## How to use it **With an AI assistant:** Copy everything from "You are a senior WordPress plugin engineer" to the end of this page into a new chat and provide your plugin's repo (or have the assistant clone it). The prompt encodes the full integration workflow — input collection (interactive Q&A or batch YAML), host-side conflict detection, the bootstrap, and a verification checklist. It's self-contained. **Or fetch this page as raw Markdown.** This page is also served at [`/Client/integration-prompt.md`](/Client/integration-prompt.md) — your AI assistant can fetch it directly. See [For AI assistants & tools](/for-ai-tools) for the full set of conventions (per-page Markdown, `` discovery, `/llms.txt`, `/llms-full.txt`). **Or fetch only the prompt itself**, with no preamble or meta sections, at [`/Client/ai-integration-prompt.md`](/Client/ai-integration-prompt.md) — that's the pure body starting with "You are a senior WordPress plugin engineer". Useful for tools that take a prompt URL as input and don't need the human-facing wrapper. **Without an AI assistant:** Read it top-to-bottom as a more thorough version of [Installation](./02-installation) + [Namespacing with Strauss](./namespacing/strauss) + [Configuration](./configuration) + [Merging into an existing composer.json](./namespacing/merging-into-existing-composer). The prompt was distilled from end-to-end testing across real plugins and captures gotchas the per-step docs don't dwell on. The recipe is verified end-to-end by the [integration tests](https://github.com/trustedlogin/docs/tree/main/tests) in this docs repo — if you follow this prompt and the result differs from what the tests assert, the tests are ground truth. ## The prompt > Everything below this line is the prompt to paste into your AI assistant. --- You are a senior WordPress plugin engineer. You will integrate the **TrustedLogin Client SDK** (https://github.com/trustedlogin/client) into the plugin in this repo so that support staff can be granted secure, time-limited, revocable access to customer sites. Treat the SDK's own priority as your priority: **never crash the host site, always be secure, always fail closed on the customer's behalf.** The single rule that everything else flows from: **the SDK must be Strauss-namespaced, the original `vendor/trustedlogin/` must be deleted, and the only autoloader the plugin loads is `vendor-namespaced/autoload.php`.** If any of those three is wrong, two plugins shipping TrustedLogin will silently collide and one will break. Don't ship that. ## How to provide me inputs Two modes: **Interactive:** Ask me one question at a time for each input below. Use this when the integrator wants to think out loud. **Batch:** Paste a YAML block matching the schema below as your first message and I'll skip Q&A. Use this when you already know the answers. ```yaml # All required unless marked omittable prefix: # PascalCase, e.g. ProBlockBuilder (see derivation rule under Inputs #0) namespace: # slug, e.g. pro_block_builder title: # human name, e.g. "Pro Block Builder" display_name: # optional, e.g. "Widgets, Co. Support" email: # e.g. support+{hash}@example.com ({hash} is substituted per-grant) website: # https://example.com support_url: # https://example.com/support/ logo_url: # optional; must be a local plugins_url() asset api_key: value: # required — the literal vendor key, lives in source constant_name: # optional — wp-config.php constant that overrides "value" when defined (for staging) role: editor # WP role to clone clone_role: true # true creates {namespace}-support, false uses role as-is caps_add: {} # { capability: "human reason shown to customer" } caps_remove: {} # { capability: "human reason shown to customer" } decay: default # default | seconds | null (null = never expire — confirm with me first) menu: parent: # admin slug, "null" for top-level, "false" for no menu, or "edit.php?post_type=foo" for CPT submenu title: Grant Support Access icon_url: # optional priority: # optional, default 100 position: # optional webhook: # set to "omit" or fill out the block below url: format: form # form (SDK default) or json (recommended for sites behind WAFs) debug_data: false create_ticket: false terms_of_service_url: # optional; adds the ToS line under the button license_key: # optional; maps to auth/license_key — only set if you want help-desk widgets to look the user up by license rather than by an opaque hash sodium_polyfill: false # true ONLY if your plugin supports WP 4.1–5.1; see "WordPress 4.1+ support" below ``` ## Inputs (with rules and gotchas) 0. **`prefix` — PascalCase namespacing prefix.** Required. Must be unique to your business or plugin. Derivation: take your slug, drop punctuation, capitalize the first letter of each word. `pro-block-builder` → `ProBlockBuilder`. `widgets_co` → `WidgetsCo`. `acme` → `Acme`. This is what Strauss uses to prefix every namespaced class — do not collide with another Composer dependency you ship. 1. **`namespace`** — slug, 5–96 chars, unique. NOT one of: `trustedlogin`, `trusted-login`, `client`, `vendor`, `admin`, `administrator`, `wordpress`, `support`. Used in URLs, role names, hook names, and the disable constant. **Prefer underscores over hyphens.** The SDK uppercases the namespace verbatim to build constants like `TRUSTEDLOGIN_DISABLE_{NS}`, so a hyphenated slug yields a constant with a hyphen — legal in PHP via `define()` but awkward to document and impossible to reference with bare `CONST_NAME` syntax. An underscored slug yields a clean, conventional constant name. 2. **`title`** — human name shown to customers (e.g. "Pro Block Builder"). Required. 3. **`display_name`** — optional. If set, the UI says `"{title} {display_name}"` (e.g. "Pro Block Builder Support"). 4. **`email`** — support inbox. **Use plus-addressing with the literal `{hash}` token** (e.g. `support+{hash}@example.com`). The SDK substitutes `{hash}` with each grant's site-identifier hash so each grant arrives from a distinct sender — your help desk can match grants to tickets. Don't escape the braces. 5. **`website`, `support_url`** — both must be valid http(s) URLs. `support_url` is also the fallback link shown when the SaaS is unreachable. 6. **`logo_url`** — optional. **Must be a local asset** (use `plugins_url(...)` against the plugin's main file constant). External URLs fail WordPress.org review. 7. **`api_key`** — from https://app.trustedlogin.com → API Keys. **This is the vendor's key, identical on every customer's site, and it lives in version-controlled PHP source — never in the database, never in a settings UI.** The customer never sees it, never enters it, never has a reason to know it exists. Rotate via the TrustedLogin dashboard if leaked. Two valid forms: - **Inline literal** — `'api_key' => '12345678'` directly in the bootstrap or in a `$config` array you pass to `Config`. Simplest; default. - **Constant override with hardcoded fallback** — `defined('MY_PLUGIN_TL_API_KEY') ? MY_PLUGIN_TL_API_KEY : '12345678'`. Lets staging point at a separate TrustedLogin account via `wp-config.php` without forking the source. Putting a config array in a separate file (e.g. `includes/trustedlogin-config.php`) and `require`ing it is fine — that's still source code, not the database. What's NOT fine: `get_option()`, transients, env files read at runtime, anything customer-editable. If you reach for `wp_options`, you've made a category error — that's the Connector's job, not the Client's. 8. **Role policy** — which WP role to clone (`role`, default `editor`). `clone_role: true` (recommended) creates `{namespace}-support` with that role's caps and *automatically strips* `create_users`, `delete_users`, `edit_users`, `promote_users`, `delete_site`, `remove_users` — even if you list them in `caps_add`. `clone_role: false` uses the role as-is, with no stripping. 9. **`caps_add` / `caps_remove`** — capability => human reason. **The reasons are shown verbatim to the customer in the Grant Access UI**, so write them in plain English. Bad: `"need this"`. Good: `"Support needs to view your forms and entries to diagnose the issue."`. 10. **`decay`** — seconds support access lives. Default 604800 (1 week). Min 86400 (1 day), max 2592000 (30 days). `null` disables expiry — only use if I explicitly confirm. 11. **Menu placement.** See the "Admin menu integration" architecture section below for the decision tree. `menu.parent` is what gets passed to `add_submenu_page`: - String slug (e.g. `"options-general.php"`) — submenu of an existing admin page. - String CPT path (e.g. `"edit.php?post_type=foo"`) — submenu of a CPT, alongside its other entries. Often the right answer for plugins built around a CPT. - `null` — adds a new top-level menu (only correct if your plugin has no admin presence of its own). - `false` — registers no menu. **Use this when you embed the form inline in your own settings page**, otherwise you'll have two entry points for the same UI. 12. **Webhook (optional).** `webhook.format`: `'form'` is the SDK default (URL-encoded body). `'json'` was added in 1.9.1 and is preferred — JSON request bodies don't trip WAF XSS heuristics on form-encoded payloads. Use `json` unless your endpoint can only parse `application/x-www-form-urlencoded`. 13. **`terms_of_service_url`** — optional. Adds the line "By granting access, you agree to the Terms of Service" under the button. 14. **`license_key`** — optional. Maps to the SDK's `auth/license_key` config. The SaaS uses this as the lookup key when a help-desk widget asks "does this customer have an active grant?". If unset, the SDK generates an opaque hash and uses that instead — which is fine for most integrations. Only set this if your support stack already keys off license keys. If the integrator hasn't decided on something optional, omit it entirely — do not pass empty strings. ## Required architecture ### 1. Composer + Strauss namespacing The SDK uses the global `TrustedLogin\` namespace. Two un-namespaced copies in the same WordPress install means whichever loads first wins and the other silently breaks. Strauss is mandatory; PHP-Scoper is the alternative. Update (or create) `composer.json`: ```json { "require": { "trustedlogin/client": "dev-main" }, "require-dev": { "brianhenryie/strauss": "dev-master", "scssphp/scssphp": "^1.11.0" }, "config": { "allow-plugins": { "composer/installers": true } }, "extra": { "strauss": { "target_directory": "vendor-namespaced", "namespace_prefix": "{{prefix}}\\", "classmap_prefix": "{{prefix}}_", "classmap_output": true, "delete_vendor_packages": true, "packages": ["trustedlogin/client"] } }, "scripts": { "strauss": ["@php vendor/bin/strauss"], "trustedlogin": [ "@php vendor-namespaced/trustedlogin/client/bin/build-sass --namespace={{prefix}}" ], "post-install-cmd": ["@strauss", "@trustedlogin"], "post-update-cmd": ["@strauss", "@trustedlogin"] } } ``` **Why each line matters:** - `"strauss": ["@php vendor/bin/strauss"]` — Strauss is a `require-dev` package, so Composer installs `vendor/bin/strauss` for you. Do NOT use `@php strauss.phar` — that will fatal `Could not open input file: strauss.phar` on every install unless you also `curl` the phar first, and the two paths conflict. - `"delete_vendor_packages": true` — after Strauss copies the SDK to `vendor-namespaced/`, it deletes the original `vendor/trustedlogin/` so the un-namespaced source isn't sitting on your dev disk. **Side effect to know about:** Strauss also generates `vendor/composer/autoload_aliases.php`, a dev-time back-compat shim that registers an SPL autoloader for the bare `\TrustedLogin\*` namespace. Per Strauss's own README this file "should not be included in your production code" — and it isn't, because `/vendor` is `export-ignore`d / `.distignore`d below. The shim is dev-only convenience; in dev environments where a *second* plugin ships un-namespaced TrustedLogin, the alias autoloader can theoretically cause a class-redeclaration fatal depending on load order, but production is unaffected because no plugin ships `/vendor`. Strauss currently has no flag to disable the aliasing alone (TODO in source); accept the trade-off and move on. - `classmap_output: true` — Strauss writes a self-contained autoloader at `vendor-namespaced/composer/autoload_static.php`. Loading `vendor-namespaced/autoload.php` is sufficient and self-contained. - **No `"autoload": { "classmap": ["vendor"] }` block.** That re-introduces the bare classes if anyone ever scans `vendor/` (which Composer does by default if the block is present). Leave Composer's autoload section minimal — Strauss owns this. - `build-sass` runs from `vendor-namespaced/trustedlogin/client/bin/build-sass` — Strauss copies the SDK's full package tree (including `bin/`), so the namespaced binary exists post-install. `vendor/bin/build-sass` also survives Strauss's deletion as a Composer-installed proxy file, but the explicit path is more honest about where the script actually lives. - `config.allow-plugins.composer/installers: true` prevents Composer 2.9+ from blocking `install` on an interactive trust prompt for `composer/installers`. Without it, CI / non-interactive runs hang. **You may see "Ambiguous class resolution" warnings** about `Composer\InstalledVersions`, `RoundingMode`, or `Deprecated` on some Strauss/Composer combinations — they're harmless transitive-dep collisions inside Strauss's own tree, unrelated to your integration. Recent versions don't emit them. **`vendor/` will be ~17 MB.** Strauss + scssphp pull in ~56 dev packages. Your production zip should ship **only** `vendor-namespaced/` (and your own code), never `vendor/`. **How** you exclude vendor depends on the host's existing build mechanism — see Deliverable #2 for detection logic. If the host has no production-zip mechanism at all, the canonical fallback is `.gitattributes export-ignore` for `/vendor`, `/composer.json`, `/composer.lock`, and `/strauss.phar`. But check first: most plugins that already ship to customers already have a build pipeline. ### 1a. Merging into an existing `composer.json` If the plugin already ships a `composer.json`, you'll merge into the host's setup rather than create one. Several host-side configurations can conflict with Strauss; here's what to look for. Skip this section entirely if the plugin has no prior Composer setup — the canonical template above just works. **Platform PHP version.** This is a **build-time** concern only — the shipped, namespaced SDK supports PHP 5.3+ at runtime regardless of what version Strauss ran on. But if the host pins `config.platform.php` below `7.4`, Composer will refuse to install Strauss `dev-master` (it requires `nikic/php-parser ^5`, which needs PHP ≥ 7.4). Two options: - Bump `config.platform.php` to `7.4` (doesn't change customer-site runtime requirements — that's set by the plugin's `Requires PHP` header, not by Composer's platform setting). - Or pin `"brianhenryie/strauss": "^0.21"` for the legacy `nikic/php-parser ^4` line. You'll lose newer Strauss features but install works on lower platform versions. Either way, your customers can still run PHP 5.3+ — Strauss only runs on the *building* machine. **Dependency conflicts.** Old static-analysis dev deps sometimes cap `nikic/php-parser` at `^4` (e.g. `exussum12/coverage-checker`, older `phpstan/phpstan-wordpress`), which conflicts with Strauss `dev-master`. Either remove/upgrade the conflicting dep, or pin Strauss to `^0.21` per above. **`config.classmap-authoritative: true`** is incompatible with Strauss-via-composer-bin. Under classmap-authoritative, Composer ignores PSR-4 lookups, so `vendor/bin/strauss` can't resolve `Composer\Factory` — it exits 0 silently with no output, and you'll wonder why nothing happened. Set `config.classmap-authoritative: false` for the build, or restore it post-install. **Strict `Composer\` autoload.** Some hosts' autoload setups don't expose `Composer\Factory` to consumed scripts even with classmap-authoritative off. Symptom: same as above — `vendor/bin/strauss` runs to exit 0 with no output. Add to the host `composer.json`: ```json "autoload-dev": { "psr-4": { "Composer\\": "vendor/composer/composer/src/Composer/" } } ``` Only add this if you see Strauss exit silently — for most hosts it's unnecessary. **`autoload.files` entries that reference WordPress classes.** This is the most common host-side trap. **Don't add the bootstrap to `autoload.files`** — `require_once` it from the main plugin file instead (which already has an ABSPATH check). Composer eagerly loads everything in `autoload.files` during `composer install`, *before* WordPress is loaded; any file there that references WP-only classes (e.g. `class Foo extends WP_Widget`) or short-circuits with `if ( ! defined( 'ABSPATH' ) ) { exit; }` will silently abort Strauss mid-run. If the host plugin already has files in `autoload.files` that reference WP classes, guard them: ```php -*` class names (e.g. `--namespace=ProBlockBuilder` → `tl-problockbuilder-*`) so two TrustedLogin installs don't fight over the same selectors. `build-sass` lowercases the prefix; CSS class names are case-insensitive in HTML but lowercase is the convention. If `scssphp/scssphp` can't be a dev dep in your release flow: ship a CI step that runs `composer install` (with dev deps), then `php vendor-namespaced/trustedlogin/client/bin/build-sass --namespace={{prefix}}`, then `composer install --no-dev` for the final zip. **`composer install --no-dev` will not regenerate the CSS** — scssphp won't be available — so the build must run while dev deps are installed. ### 4. Bootstrap (the only place that touches TrustedLogin) Create one file (e.g. `includes/class-trustedlogin-bootstrap.php`) that: - **Loads `vendor-namespaced/autoload.php` and only that.** Do NOT `require_once vendor/autoload.php` for the SDK — that autoloader points at `vendor/trustedlogin/` (deleted by Strauss) and serves no purpose for the integration. If your plugin uses Composer for other things, that's fine — host code can still include `vendor/autoload.php` separately, but the TrustedLogin glue must use the namespaced one. - Loads on **every** page load — admin and front-end. The SDK self-registers on `init:100`, `admin_init:100`, and `template_redirect:99`; if your bootstrap doesn't fire by then, lockdown checks and the login endpoint silently break. - Hooks instantiation on `plugins_loaded`. - **Wraps `new Client( new Config( $config ) )` in `try { } catch ( \Exception $e ) { }`.** The SDK throws on invalid config, global disable, namespace disable, or missing sodium. An uncaught exception white-screens the host. Log via `error_log()` (or your logger) and return — never re-throw. - Short-circuits on `TRUSTEDLOGIN_DISABLE` and `TRUSTEDLOGIN_DISABLE_{NAMESPACE_UPPER}` — but the SDK already does this, so prefer just letting Config throw and the catch swallow it. - Uses the **namespaced** class names: `\{{prefix}}\TrustedLogin\Client` and `\{{prefix}}\TrustedLogin\Config`. Never reference `\TrustedLogin\Client`. **Heads up:** `Config::__construct` reaches into WordPress globals (`WEEK_IN_SECONDS`, `wp_kses_bad_protocol`, `is_ssl`, `current_time`, `is_wp_error`, `WP_Error`, `shortcode_atts`, `sanitize_title_with_dashes`, `plugin_dir_url`). It is not safe to construct outside a WordPress request. Anyone trying to unit-test the bootstrap from a non-WP CLI script needs a substantial WP shim. ### 5. Configuration object The YAML batch schema is for human input; `$config` is the nested PHP array the SDK actually consumes. The shapes don't match — translate per this worked example, then drop into the bootstrap: ```php $config = [ 'auth' => [ 'api_key' => defined( 'PRO_BLOCK_BUILDER_TL_API_KEY' ) ? PRO_BLOCK_BUILDER_TL_API_KEY : '12345678', ], 'vendor' => [ 'namespace' => 'pro_block_builder', 'title' => 'Pro Block Builder', 'display_name' => 'Widgets, Co. Support', 'email' => 'support+{hash}@example.com', 'website' => 'https://example.com', 'support_url' => 'https://example.com/support/', ], 'role' => 'editor', 'clone_role' => true, 'caps' => [ 'add' => [ 'manage_options' => 'Support needs this to view and adjust your settings while diagnosing the issue.', ], ], 'menu' => [ 'slug' => 'edit.php?post_type=my_cpt', // YAML "menu.parent" → PHP "menu.slug" 'title' => 'Grant Support Access', 'position' => 100, ], ]; ``` YAML → PHP mappings worth flagging: - YAML `caps_add` / `caps_remove` (flat) → PHP `caps.add` / `caps.remove` (nested under `caps`). - YAML `menu.parent` → PHP `menu.slug`. The SDK uses "slug" but it really means "parent admin page slug" — renamed in YAML for clarity. - YAML `terms_of_service_url` → PHP `terms_of_service.url`. - YAML `license_key` → PHP `auth.license_key` (sibling of `auth.api_key`). - YAML `webhook.format` → PHP `webhook.format` (no rename, just nested). - Omit any optional block entirely — don't pass empty arrays or empty strings. Validation rules to know: - URLs (`vendor.website`, `vendor.support_url`, `vendor.logo_url`, `webhook.url`) must be valid http(s) or `Config` throws. - `decay` outside [86400, 2592000] is rejected (omit to use the 604800 default). - `vendor.namespace` collisions don't break security but produce broken UI; pick something unique to the vendor company, not the plugin (multiple plugins from one vendor can share a namespace). - Never log `auth.api_key` or any secret — SDK exception messages don't include it, so logging `$e->getMessage()` is safe. ### 6. Admin menu integration Three placement patterns. Pick one. Don't double up. **Pattern A — SDK-managed submenu of your plugin's existing menu (default for most plugins).** Set `menu.parent` to the parent slug of your plugin's admin menu. The SDK adds "Grant Support Access" as a child entry. Examples: - Plugin uses a top-level menu with slug `my_plugin`: `menu.parent: "my_plugin"`. - Plugin's UI hangs off a CPT: `menu.parent: "edit.php?post_type=my_cpt"`. The Grant Access entry appears alongside Add New / All Items / etc. - Plugin uses an `options-general.php` submenu approach: `menu.parent: "options-general.php"`. This is right whenever your plugin has *any* admin presence already. Customers find support-related actions where they expect to find them. **Pattern B — SDK-managed top-level menu (rare).** Set `menu.parent: null`. The SDK calls `add_menu_page` and creates a new top-level item. Use this only if your plugin has zero existing admin menu of its own (uncommon — even Settings-only plugins usually hang off `options-general.php`). **Pattern C — Inline embed in an existing settings page.** Set `menu.parent: false` so the SDK registers no menu, then render the form yourself wherever you want it: ```php // Inside your own settings page render callback, or a "Support" tab. if ( current_user_can( 'create_users' ) ) { do_action( 'trustedlogin/{namespace}/auth_screen' ); } ``` `do_action( 'trustedlogin/{namespace}/auth_screen' )` enqueues the namespaced CSS/JS and renders the full Grant Access form. Use this when your plugin already has a tabbed settings UI and "Support" is one of those tabs — adding a separate menu item would split the user's mental model. **Capability is hardcoded to `create_users`.** The SDK requires `create_users` to view the Grant Access page or trigger a grant — both for SDK-registered menus and for any place you call `do_action( 'trustedlogin/{namespace}/auth_screen' )`. This is **not filterable**, by design: granting support access is itself a security-sensitive action, so non-admins shouldn't see the entry point. Don't try to relax this. (Note: this is the cap to *grant* access, not the role granted to support — those are independent.) **Multisite gotcha:** on multisite, only Super Admins have `create_users` by default — subsite admins (`administrator` role on a subsite) do NOT. If your plugin runs on multisite networks, document that only Network Admins can grant TrustedLogin access, or expect support tickets asking why the menu is missing. **Default URL.** The page slug defaults to `grant-{namespace}-access`, so the admin URL is `wp-admin/admin.php?page=grant-{namespace}-access` (Patterns A and B). Override via the `trustedlogin/{namespace}/admin/menu/menu_slug` filter if you need a specific URL — but the default works for most cases. **Customer-facing URL (independent of menu placement).** `wp-login.php?action=trustedlogin&ns={namespace}&ref={ticket_id}` works regardless of menu config — it's the SDK's login-screen endpoint, not an admin page. Pair with a Help Scout / Zendesk saved reply that injects `&ref={%conversation.number%}` so each grant ties back to a ticket. This is the URL to give customers in support emails. **Position.** `menu.position` is a float passed to `add_submenu_page`. For Pattern A, position the Grant Access entry **last** in the menu cluster — after Settings, License, About, etc. Customers reach for it rarely; keeping it at the bottom prevents accidental clicks and matches the "destructive last" convention used elsewhere in WordPress admin. ### 7. Hooks integrators rely on Wire all integration code to **namespaced** hooks. Never reference internal SDK classes directly. Actions: - `trustedlogin/{namespace}/access/created` — fired after a grant is issued. - `trustedlogin/{namespace}/access/extended` - `trustedlogin/{namespace}/access/revoked` - `trustedlogin/{namespace}/logged_in` - `trustedlogin/{namespace}/login/before|after|refused|error` — for auditing. - `trustedlogin/{namespace}/lockdown/after` — fired when brute-force lockdown engages. Filters worth knowing about (don't apply unless asked): - `trustedlogin/{namespace}/template/auth/footer_links` — modify the footer link list. - `trustedlogin/{namespace}/support_role/display_name` — rename the role as shown to admins. - `trustedlogin/{namespace}/webhook/request_args` — add auth headers / bearer tokens to webhook calls. - `trustedlogin/{namespace}/login_feedback/allowed_referer_urls` — add cross-domain support portals. The "advanced internal use" filters (`api_url`, `vendor_public_key`, `meets_ssl_requirement`) are **off-limits** unless I explicitly ask — they exist for the SDK's own tests. ### 8. Local & staging environments - The SDK auto-disables security checks when `wp_get_environment_type()` returns `local` or `development`. - For staging that reports as `production` but isn't, define `TRUSTEDLOGIN_TESTING_{NAMESPACE_UPPER}` in `wp-config.php` to bypass lockdown without weakening prod. - Define `TRUSTEDLOGIN_DISABLE_LOCAL_NOTICE` to suppress the "won't work locally" notice. - Real grants need a public hostname; ngrok works for testing. ### 9. Things you must NOT do - Don't instantiate without try/catch. - Don't include `vendor/autoload.php` for the SDK — only `vendor-namespaced/autoload.php`. - Don't omit `delete_vendor_packages: true` from the Strauss config. - Don't put `auth/api_key` in `wp_options`, transients, or any customer-editable settings UI. The vendor's api_key lives in version-controlled source. If you think you need DB storage for it, you're building a Connector, not a Client integration. - Don't grant `create_users`, `delete_users`, `edit_users`, `promote_users`, `delete_site`, or `remove_users` via `caps/add` — the SDK strips these regardless under `clone_role: true`, but requesting them is a smell. - Don't reference an external CDN for `vendor/logo_url`, `paths/css`, or `paths/js` — WordPress.org rejects this. - Don't write to `wp-content/uploads/trustedlogin-logs/` in production. Leave `logging/enabled = false` (the default) unless triaging a specific issue, and prefer hooking `trustedlogin/{namespace}/logging/log` into your existing logger. - Don't reference internal SDK classes (`Encryption`, `Envelope`, `Remote`, `SupportUser`, `Endpoint`) from plugin code. Use only `Client`, `Config`, and the documented hooks/filters. ## Workflow - **Work on a new branch.** Don't touch `main`/`master` directly. Use the host repo's naming convention — check `git log --all --oneline -50` and `git for-each-ref --sort=-committerdate refs/remotes/` for patterns. Common: `feature/` (e.g. `feature/trustedlogin`), `-` (e.g. `2216-round-up`). - **If the host already has a branch in flight for this work, STOP.** Don't fight an existing integration. Stopping means: do not edit any files, do not run `composer install`, do not create a branch. Output a short report naming the branch you found, its commit count ahead of main, and a one-line summary of what it implements. Then return control to the integrator and let them decide whether to proceed (e.g., resuming on the existing branch, or starting fresh with rationale). Don't speculate about whether the existing work is correct — surface and yield. - **Commit as you go.** One coherent commit per logical step (composer.json + Strauss config; bootstrap; readme/changelog) — match the host's commit-message style (length, prefix, voice). - **Don't push and don't open a PR.** Leave the work as local commits on the branch. The integrator will review, fix up if needed, and push themselves. - **Don't `git config` anything.** Use whatever identity is already on the working tree. - **Respect existing hooks in `.git/hooks/`.** Don't modify or remove them. If a hook blocks an action, that's intentional — surface the failure and stop, don't work around it. ## Deliverables 1. The patched `composer.json` with Strauss + scssphp dev deps, the Strauss `extra` block (with `delete_vendor_packages: true` and `classmap_output: true`), `config.allow-plugins.composer/installers: true`, and post-install/update scripts wired through `vendor/bin/strauss`. 2. Repo hygiene — **match the host's existing conventions; don't introduce competing mechanisms.** Detect first, then act. Note: gitignore handling and production-zip exclusion are **independent decisions** — a host can gitignore `/vendor` (so devs don't commit dev deps) AND ship `/vendor` in production (built fresh via CI then included via an allowlist). Investigate each axis separately: **For `.gitignore`:** - **If we created `composer.json` from scratch** (host had no prior Composer setup): add `/vendor` to `.gitignore` and gitignore `/vendor-namespaced/` too. - **If the host already gitignores `/vendor`**: do nothing. Also gitignore `/vendor-namespaced/` to match — the host clearly relies on a build pipeline; let it produce both trees. - **If the host commits `/vendor`**: don't touch their `.gitignore`. Commit `vendor-namespaced/` alongside `vendor/` to match. **For production-zip exclusion:** Find the host's existing mechanism — don't add a new one. Look for, in order: - An existing `.distignore` (used by `wp dist-archive`). - An existing `.gitattributes` with `export-ignore` rules. - An existing build allowlist (e.g. `build/file-allowlist.txt`, a Phing/Grunt/shell script under `build/`, a GitHub Actions release workflow). - A `package.json` release script. If you find one: add the new artifacts there in the host's style. For an allowlist-based system, add `vendor-namespaced/**` to the allow list. For an exclude-based system, ensure `vendor`, `composer.json`, `composer.lock`, and `strauss.phar` are excluded. **Only create a new `.distignore` / `.gitattributes` if no production-zip mechanism exists at all.** Most plugins that ship to wordpress.org or via custom CI already have one — find it before reaching for a new file. Always commit: `composer.json`, `composer.lock`, and the bootstrap file. Whether to commit `vendor-namespaced/` follows the host's vendor convention above. 3. A single bootstrap file with the `try/catch` instantiation, hooked on `plugins_loaded`, loading **only** `vendor-namespaced/autoload.php`. 4. A README section (under "Support") documenting: - How a customer grants access (the `wp-login.php?action=trustedlogin&ns=...` URL). - The `TRUSTEDLOGIN_DISABLE_{NS}` opt-out constant. - That the role created is `{namespace}-support`, with reasons for any added/removed caps. 5. A CHANGELOG entry noting the new dependency and the opt-out path. (If the plugin tracks changelogs in `readme.txt`, write it there; don't create a CHANGELOG.md just for this.) 6. (If applicable) a developer note for the support team with the saved-reply URL pattern. ## Verification checklist (each item must be RUN, not reasoned about) - [ ] `composer install` exits 0 (after applying any §1a remedies for host-side conflicts; first-run failure on a host with existing composer.json is expected if §1a's preconditions weren't already in place). Ignore any "Ambiguous class resolution" warnings — they're harmless Strauss transitive-dep collisions. - [ ] `vendor-namespaced/trustedlogin/client/src/Client.php` exists, and `grep '^namespace ' vendor-namespaced/trustedlogin/client/src/Client.php` shows `namespace {{prefix}}\TrustedLogin;` (the namespace declaration is around line 21, not line 1 — `head -1` would return the `* support user. Access expires in 7 days." The banner is only shown to the freshly-logged-in support user — regular admins never see it, even if they construct the URL directly. --- ## 3. Already-logged-in edge case > **Heads-up:** it's possible to hit a TrustedLogin access link while already signed in to the target site — for example, an admin was testing something, then clicked their own grant link. When this happens, TrustedLogin does **not** swap their session for a support user. The existing login stays put and a calm info notice explains what happened. ![Already-logged-in info notice](/img/client/login-feedback/04-already-logged-in.png) **Notice copy:** "You were already signed in as *\*, so the support login was skipped. You can grant or revoke access as usual." --- ## 4. Security-check failure If the request fails a security check (unknown identifier, site in lockdown, brute-force threshold tripped), the agent sees a generic failure screen. ![Security-check failed](/img/client/login-feedback/05-security-check-failed.png) **Message copy:** "This login request was blocked for security reasons. If this continues, please contact your support provider." The screen offers three navigation options: - **← Go back** — sends the agent back to the vendor surface they came from (see [§6 below](#6-the-go-back-link)). - **Contact support** — links to the `vendor/support_url` configured in the integrator's setup. - A back-to-site link so the agent can exit cleanly. The message is deliberately vague. The exact reason (which check failed, which identifier was tried, what the endpoint was) is never surfaced in the UI, in the URL, or in the page source — an attacker probing the form learns nothing from the response. --- ## 5. Login failed (access expired) Same screen, different cause: the identifier was recognized but the support user's access has expired or was already used. ![Login failed](/img/client/login-feedback/06-login-failed.png) **Message copy:** "Support access could not be started. The access key may have expired or already been used." The copy is different from §4 because the real-world cause for this branch is usually expiry or a one-time key that's already been used — both of which the agent can act on by asking for a fresh access key. --- ## 6. The "Go back" link The **← Go back** link on the failure screens returns the agent to wherever they started the support flow — a vendor dashboard, a help portal, a ticket page. Getting this link right while keeping it safe is why this section exists. ### Default behavior By default, a Go-back link is shown whenever the agent arrived from one of three trusted surfaces: - the `vendor/website` URL in the integrator's Client config, - the `vendor/support_url` URL in the integrator's Client config, - the customer's own site (`home_url()`). If the agent arrived from somewhere else — or from nowhere at all (direct URL, email client, automation) — the Go-back link is simply omitted. The rest of the failure screen still works. ### 6a. Multi-surface vendors — extending the allowlist Vendors that run support from multiple surfaces (marketing site, help portal, white-label domains, staging environments) can add extra trusted URLs using the `login_feedback/allowed_referer_urls` filter: ```php add_filter( 'trustedlogin/pro-block-builder/login_feedback/allowed_referer_urls', function ( $urls, $config ) { $urls[] = 'https://support.vendor.test/portal'; $urls[] = 'https://status.vendor.test'; return $urls; }, 10, 2 ); ``` Agents arriving from any of those hosts will see a working Go-back link. When the link is rendered, it points to the **configured** URL (e.g. `https://support.vendor.test/portal`) — not whatever deep path or query string the agent's browser actually sent: ![Go-back link points at filter-added URL — deep path and query are discarded](/img/client/login-feedback/08-goback-filter-extended.png) Full reference: [`trustedlogin/{namespace}/login_feedback/allowed_referer_urls`](./hooks.md#trustedloginnamespaceloginfeedbackallowedrefererurls). ### 6b. Spoofed origin — Go-back is quietly omitted An attacker can forge the header the browser sends to claim the agent came from, say, `evil.example`. TrustedLogin compares the claimed origin against the allowlist, finds no match, and **does not show a Go-back link at all**. The attacker-chosen host appears nowhere on the page, so the attacker can't use the failure screen as a phishing surface. ![Spoofed origin → Go-back link absent, no leaked attacker host](/img/client/login-feedback/09-goback-spoof-rejected.png) ### 6c. No origin at all Direct URL, copy/paste, script-driven POST — sometimes there's no origin to go back to. The failure screen renders without the Go-back link; **Contact support** and the back-to-site link remain. ![No origin → Go-back omitted, Contact support still present](/img/client/login-feedback/10-goback-no-referer.png) ### Why host-match, not URL-match Browsers vary what they send (deep paths, tracking query strings, anchor fragments). Requiring an exact URL match would break the Go-back link for real agents constantly. TrustedLogin matches on **host only** — and then renders the *configured* URL rather than the raw sent value — so legitimate flows always work and attacker-controlled data never reaches the page. --- ## 7. Crafted URLs show nothing Anyone can construct a URL like `wp-login.php?action=trustedlogin&tl_error=login_failed` and paste it into a victim's browser. Without a matching internal signal from an actual failed login, TrustedLogin shows nothing — just the standard WordPress login form. ![Crafted URL shows nothing](/img/client/login-feedback/07-crafted-url-no-message.png) This blocks the "your support login failed, click here to retry" phishing shape, where an attacker would otherwise craft a convincing fake TrustedLogin error screen. --- ## What the agent sees, by scenario | Scenario | What the agent sees | |---|---| | Malformed or probing POST | Plain home page — nothing that hints at TrustedLogin | | Valid grant, login succeeds | wp-admin with a green success banner | | Agent is already signed in | wp-admin with an info notice explaining the skip | | Unknown access key / lockdown | Generic "blocked for security reasons" screen | | Expired or already-used access key | Generic "access could not be started" screen | | Crafted `?tl_error=…` URL with no real failure behind it | Plain WordPress login form — nothing else | | Agent came from a trusted vendor surface | Go-back link points at the matched configured URL | | Agent came from an unknown origin | Go-back link is omitted; other actions remain | --- ## Customizing for integrators The Client SDK exposes these public hooks for this flow. Full signatures and examples live in the [Client hooks reference](./hooks.md). - [`trustedlogin/{namespace}/login_feedback/allowed_referer_urls`](./hooks.md#trustedloginnamespaceloginfeedbackallowedrefererurls) — add extra trusted URLs for the Go-back link. - [`trustedlogin/{namespace}/template/auth/footer_links`](./hooks.md#trustedloginnamespacetemplateauthfooter_links) — add or modify the footer links shown underneath the failure message. - [`trustedlogin/{namespace}/support_url/query_args`](./hooks.md#trustedloginnamespacesupport_urlquery_args) — tweak the URL parameters appended when the agent clicks **Contact support** from a failure screen. --- # CSS Namespacing Source: https://docs.trustedlogin.com/Client/namespacing/css-namespacing Markdown: https://docs.trustedlogin.com/Client/namespacing/css-namespacing.md # Namespacing CSS Files TrustedLogin CSS files are namespaced so that they don't conflict with other plugins or themes that are using TrustedLogin. To namespace the files, you can use the `build-sass` script included with the TrustedLogin client inside the `bin/` directory. The `build-sass` script accepts the following arguments: - `--namespace`: The namespace to use for the CSS files. This is required. - `--assets_dir`: The path to the TrustedLogin client directory, used locate the SCSS source files. Optional. Default: `(vendor-namespaced|vendor-prefixed)/trustedlogin/client/src/assets/`. - `--export_dir`: The path to the output directory where the generated CSS will be saved. Optional. Default: to `(vendor-namespaced|vendor-prefixed)/trustedlogin/client/src/assets/`. The default way to namespace files is [as a Composer script](/Client/01-intro.md), but this may not work with your build process: the default implementation shown adds the required SCSS package (`scssphp/scssphp`) to the `require-dev` array, which may not work with your release flow. If you move `scssphp/scssphp` to the `require` array, the scssphp library will be included in your autoloader, which adds bloat for something that should be used one-time. :::info ### When you see `ProBlockBuilder`, make sure to replace with your own namespace! {#when-you-see-problockbuilder-make-sure-to-replace-with-your-own-namespace} In the examples below, we're going to pretend your plugin or theme is named "Pro Block Builder" and your business is named Widgets, Co. These should not be the names you use—make sure to update the sample code below to match your business and plugin/theme name! ::: Here are alternate ways to namespace the CSS files: ## Manually namespacing the CSS files {#manually-namespacing-the-css-files} If you'd like to manually namespace the CSS files (for instance, in a GitHub Actions workflow), first `cd` into your plugin or theme directory. Then use the following command (update it to match your namespace and path to TrustedLogin client directory): ```bash php vendor-namespaced/trustedlogin/client/bin/build-sass --namespace=ProBlockBuilder ``` This will generate the namespaced CSS files in the `vendor-namespaced/trustedlogin/client/src/assets/` directory. You can then copy the files to your plugin or theme directory. If this fails with a message `command not found: php`, then PHP isn't installed on your machine. [Install PHP](https://www.php.net/manual/en/install.php) and try again. ## Namespacing via an SCSS mixin {#namespacing-via-an-scss-mixin} If you'd like to use an SCSS mixin to namespace CSS files, you can use the following code: ```scss @import "vendor-namespaced/trustedlogin/client/src/assets/src/variables"; // Variables used in the mixins (all !default) @import "vendor-namespaced/trustedlogin/client/src/assets/src/auth"; // Mixins for authentication screen @import "vendor-namespaced/trustedlogin/client/src/assets/src/buttons"; // Mixins for buttons @import "vendor-namespaced/trustedlogin/client/src/assets/src/global"; $namespace: "ProBlockBuilder"; @include trustedlogin-auth( $namespace ); @include trustedlogin-button( $namespace ); ``` --- # Namespacing Source: https://docs.trustedlogin.com/Client/namespacing/index Markdown: https://docs.trustedlogin.com/Client/namespacing/index.md Namespacing is vital for any instance of TrustedLogin to not conflict with other code running TrustedLogin. There are two parts of the code that must be namespaced for TrustedLogin to function properly: ## PHP - [Strauss](/Client/namespacing/strauss) (recommended) - [PHP-Scoper](/Client/namespacing/php-scoper) ## CSS - [CSS Namespacing](/Client/namespacing/css-namespacing) ## Integrating into a plugin that already has a `composer.json` - [Merging into an existing composer.json](/Client/namespacing/merging-into-existing-composer) — host-side gotchas (platform PHP, classmap-authoritative, dependency conflicts, audit advisories, stale lockfile, autoload.files traps). --- # Merging into an existing composer.json Source: https://docs.trustedlogin.com/Client/namespacing/merging-into-existing-composer Markdown: https://docs.trustedlogin.com/Client/namespacing/merging-into-existing-composer.md # Merging into an existing `composer.json` If your plugin already ships a `composer.json`, you'll merge the TrustedLogin integration into the host's setup rather than create one from scratch. Several host-side configurations can conflict with the namespacing tools — here's what to look for. If your plugin has no prior Composer setup, skip this page — the [Strauss](./strauss) and [PHP-Scoper](./php-scoper) recipes work cleanly out of the box. ## Platform PHP version If the host pins `config.platform.php` below `7.4`, Composer refuses to install Strauss `dev-master` (which requires `nikic/php-parser ^5`, needing PHP ≥ 7.4). This is a **build-time** concern only — your shipped, namespaced SDK supports PHP 5.3+ at runtime regardless of what version Strauss ran on. Two options: - Bump `config.platform.php` to `7.4`. This doesn't change customer-site runtime requirements (those come from your plugin's `Requires PHP` header, not Composer's platform setting). - Pin `"brianhenryie/strauss": "^0.21"` for the legacy `nikic/php-parser ^4` line. You'll lose newer Strauss features but install will work on lower platform versions. Either way, customers can still run PHP 5.3+. Strauss only runs on the *building* machine. ## Dependency conflicts Old static-analysis dev deps sometimes cap `nikic/php-parser` at `^4`, which conflicts with Strauss `dev-master`. Common offenders: `exussum12/coverage-checker`, older versions of `phpstan/phpstan-wordpress`. Either remove/upgrade the conflicting dep, or pin Strauss to `^0.21`. ## `classmap-authoritative: true` This setting is incompatible with running Strauss via composer-bin. Under classmap-authoritative, Composer ignores PSR-4 lookups, so `vendor/bin/strauss` can't resolve `Composer\Factory` — it exits 0 silently with no output, and you'll wonder why nothing happened. Set `config.classmap-authoritative: false` for the build, or restore it post-install. ## `autoload.files` entries that reference WordPress classes This is a common host-side trap. **Don't add the TrustedLogin bootstrap to `autoload.files`** — `require_once` it from your plugin's main file instead (which already has an ABSPATH check). Composer eagerly loads everything in `autoload.files` during `composer install`, *before* WordPress is loaded. Any file there that references WP-only classes (e.g. `class Foo extends WP_Widget`) or short-circuits with `if ( ! defined( 'ABSPATH' ) ) { exit; }` will silently abort the installation mid-run. If the host plugin already has files in `autoload.files` that reference WP classes, guard them: ```php ` to re-resolve only the packages you added: ```bash composer update brianhenryie/strauss scssphp/scssphp trustedlogin/client ``` `composer install` (without arguments) replays the existing lockfile and silently ignores your new requirements otherwise. Symptom if you don't: "I added Strauss to require-dev but `vendor/bin/strauss` doesn't exist." ## Composer audit advisories Composer 2.9+ refuses to install if any package in the resolution graph has an open security advisory — even one unrelated to your TrustedLogin integration. If the host pins old major versions of `phpunit/phpunit`, `yoast/phpunit-polyfills`, `symfony/*`, etc., you'll see `Your requirements could not be resolved` naming packages that have nothing to do with TrustedLogin, with a small note about "affected by security advisories." Two fixes: - Bump the offending dev dep (preferred — but may cascade through other deps). - Add `"config": { "audit": { "ignore": ["PKSA-xxxx-xxxx-xxxx"] } }` to skip the specific advisory. The advisory ID is in the resolution failure output. Audit blocks only trigger on `install` / `update`, not at runtime, so customers never see them — but they will wedge your build until resolved. ## Strict `Composer\` autoload (rare) Some host setups don't expose `Composer\Factory` to consumed scripts even with classmap-authoritative off. Symptom: same as classmap-authoritative — `vendor/bin/strauss` runs to exit 0 with no output. Add to the host `composer.json`: ```json "autoload-dev": { "psr-4": { "Composer\\": "vendor/composer/composer/src/Composer/" } } ``` Only add this if you see Strauss exit silently — for most hosts it's unnecessary. ## Repo hygiene How you handle `vendor/` and `vendor-namespaced/` (or `build/`) in version control depends on the host's existing convention. The rules are: - **If `/vendor` is already gitignored:** do nothing. Your new artifacts inherit the rule. Also gitignore `/vendor-namespaced/` (or `/build/`) — the host clearly relies on a build pipeline. - **If the host commits `/vendor`:** don't touch their `.gitignore`. Commit your new artifacts alongside. Production-zip exclusion follows the host's existing build mechanism — a `.distignore`, `.gitattributes export-ignore`, build allowlist (e.g. `build/file-allowlist.txt`), or release workflow. Add to that mechanism rather than introducing a competing one. --- # Namespacing with PHP-Scoper Source: https://docs.trustedlogin.com/Client/namespacing/php-scoper Markdown: https://docs.trustedlogin.com/Client/namespacing/php-scoper.md # Namespacing with PHP-Scoper PHP-Scoper is an alternative to [Strauss](./strauss) for namespacing the TrustedLogin Client SDK. Both achieve the same goal: prevent class collisions when multiple plugins ship the SDK. PHP-Scoper rewrites the SDK into a `build/` directory with a prefixed namespace, leaving the original `vendor/` alone. You'll then point Composer's classmap at `build/`, regenerate the autoloader, and remove the un-namespaced original from `vendor/`. :::info ### When you see `ProBlockBuilder`, replace with your own namespace The examples below use "Pro Block Builder" / "Widgets, Co." as a placeholder. Use a unique prefix for your business or plugin. ::: If you're integrating into a plugin that already has its own `composer.json`, also see [Merging into an existing composer.json](./merging-into-existing-composer) for common host-side gotchas. ## Step 1. Install PHP-Scoper ```bash composer require --dev humbug/php-scoper ``` This installs PHP-Scoper at `vendor/bin/php-scoper`. ## Step 2. Install the TrustedLogin Client SDK ```bash composer require trustedlogin/client:dev-main composer require scssphp/scssphp --dev ``` `scssphp` is used to namespace the bundled CSS. Skip if you've already installed it or are [using an alternative way to namespace CSS](/Client/namespacing/css-namespacing). ## Step 3. Create `scoper.inc.php` Create `scoper.inc.php` in your project root: ```php [ Finder::create()->files()->in( 'vendor/trustedlogin/client' )->name( [ 'LICENSE', 'composer.json' ] ), Finder::create()->files()->in( 'vendor/trustedlogin/client/src' )->name( [ '*.php', '*.css', '*.js' ] ), ], 'patchers' => [ function ( $file_path, $prefix, $content ) { // Classes and functions that TrustedLogin uses that should NOT be prefixed. $allowlist = [ 'DateTime', 'Exception', 'ImagickException', 'RuntimeException', 'WP_Admin_Bar', 'WP_Debug_Data', 'WP_Error', 'WP_Filesystem_Base', 'WP_Filesystem', 'WP_User', 'wp_get_environment_type', ]; foreach ( $allowlist as $class ) { $content = str_replace( [ $prefix . '\\' . $class, $prefix . '\\\\' . $class ], $class, $content ); } return $content; }, ], ]; ``` ## Step 4. Update your `composer.json` Add the `autoload.classmap` entry pointing at `build/`, the `classmap-authoritative` setting (so bare-namespace lookups can't fall through to PSR-4), and the build script: ```json "autoload": { "classmap": ["build"] }, "config": { "allow-plugins": { "composer/installers": true }, "classmap-authoritative": true }, "scripts": { "php-scoper": [ "@php vendor/trustedlogin/client/bin/build-sass --namespace=ProBlockBuilder --assets_dir=vendor/trustedlogin/client/src/assets --export_dir=vendor/trustedlogin/client/src/assets", "vendor/bin/php-scoper add-prefix --prefix=ProBlockBuilder --force --quiet", "rm -rf vendor/trustedlogin", "@composer dump-autoload --classmap-authoritative" ], "post-install-cmd": [ "@php-scoper" ], "post-update-cmd": [ "@php-scoper" ] } ``` :::warning **Do not include `"vendor"` in this classmap.** Adding `vendor` re-exposes the bare un-namespaced `\TrustedLogin\` classes alongside your prefixed ones, defeating the namespacing. Point the classmap at `build/` only. ::: :::note Cross-platform note The `rm -rf vendor/trustedlogin` line is shell, not PHP, so it requires Mac/Linux/WSL. On native Windows you'll need to substitute the equivalent (`rmdir /S /Q vendor\trustedlogin` from `cmd`, or `Remove-Item -Recurse -Force vendor\trustedlogin` from PowerShell), or replace it with a small PHP cleanup script committed to your plugin. ::: ## Step 5. Create the empty `build/` directory before the first install Composer's autoload generation scans the `autoload.classmap` entries during `composer install`, *before* `post-install-cmd` runs. If `build/` doesn't exist on the first install, the scan errors out. Create it once: ```bash mkdir -p build ``` (After the first run, `build/` is populated by PHP-Scoper and stays present across subsequent installs. The `mkdir` is only needed once per fresh checkout.) ## Step 6. Run `composer install` ```bash composer install ``` This installs the dependencies, then triggers the `php-scoper` script via `post-install-cmd`: 1. `build-sass` compiles the SDK's SCSS sources with your prefix, writing the namespaced CSS to `vendor/trustedlogin/client/src/assets/trustedlogin.css` (selectors like `.tl-problockbuilder-auth` — `build-sass` lowercases the prefix — instead of the default `.tl-test-auth`). 2. PHP-Scoper copies the SDK (including the just-compiled CSS) into `build/`, rewriting PHP namespaces. 3. The shell command removes the un-namespaced original at `vendor/trustedlogin/`. 4. `composer dump-autoload --classmap-authoritative` regenerates the autoload, scanning `build/` for the prefixed classes and skipping the (now missing) `vendor/trustedlogin/`. After this completes: - `build/` contains the prefixed SDK with `build/src/assets/trustedlogin.css` carrying your prefixed selectors. - `vendor/trustedlogin/` is gone. - `vendor/composer/autoload_classmap.php` resolves your prefixed `\ProBlockBuilder\TrustedLogin\Client` to `build/src/Client.php`. ## Step 7. Include the autoloader In your plugin's bootstrap: ```php require_once trailingslashit( dirname( __FILE__ ) ) . 'vendor/autoload.php'; ``` After steps 4–6, `vendor/autoload.php` resolves your prefixed classes (via the classmap pointing at `build/`) and the bare un-namespaced classes are unreachable (because `vendor/trustedlogin/` is gone and `classmap-authoritative` disables PSR-4 fallback). ## Step 8. Configure and instantiate the Client Follow [the directions to configure and instantiate the client](../configuration). Use your prefix: ```php new \ProBlockBuilder\TrustedLogin\Client( new \ProBlockBuilder\TrustedLogin\Config( $config ) ); ``` --- # Namespacing with Strauss Source: https://docs.trustedlogin.com/Client/namespacing/strauss Markdown: https://docs.trustedlogin.com/Client/namespacing/strauss.md # Namespacing with Strauss Strauss namespaces the TrustedLogin Client SDK so it can ship safely alongside other plugins that also use TrustedLogin without class collisions. :::info ### When you see `ProBlockBuilder`, replace with your own namespace The examples below use "Pro Block Builder" / "Widgets, Co." as a placeholder. Use a unique prefix for your business or plugin. ::: If you're integrating into a plugin that already has its own `composer.json`, also see [Merging into an existing composer.json](./merging-into-existing-composer) for common host-side gotchas. If you're using an AI coding assistant for the integration, see the [AI Integration Prompt](../integration-prompt) — it bundles this recipe with input collection, host-side conflict detection, and a verification checklist into a single self-contained prompt you paste into the assistant. ## Step 1. Add Strauss as a dev dependency You'll add `brianhenryie/strauss` to `require-dev` in your `composer.json` (see step 2 below). Composer installs Strauss at `vendor/bin/strauss`, so no manual `curl` of `strauss.phar` is needed. ## Step 2. Update `composer.json` Update your `composer.json` to require the SDK and configure Strauss: ```json { "require": { "trustedlogin/client": "dev-main" }, "require-dev": { "brianhenryie/strauss": "dev-master", "scssphp/scssphp": "^1.11.0" }, "config": { "allow-plugins": { "composer/installers": true } }, "extra": { "strauss": { "target_directory": "vendor-namespaced", "namespace_prefix": "ProBlockBuilder\\", "classmap_prefix": "ProBlockBuilder_", "classmap_output": true, "delete_vendor_packages": true, "packages": ["trustedlogin/client"] } }, "scripts": { "strauss": ["@php vendor/bin/strauss"], "trustedlogin": [ "@php vendor-namespaced/trustedlogin/client/bin/build-sass --namespace=ProBlockBuilder" ], "post-install-cmd": ["@strauss", "@trustedlogin"], "post-update-cmd": ["@strauss", "@trustedlogin"] } } ``` ### Why each line matters - **`"@php vendor/bin/strauss"`** — Strauss is installed by Composer when listed in `require-dev`, so the binary lives at `vendor/bin/strauss`. Don't use `@php strauss.phar` — that requires you to manually `curl` the phar separately, and trips a `Could not open input file` fatal on every `composer install` if you don't. - **`delete_vendor_packages: true`** — after Strauss copies the SDK to `vendor-namespaced/`, this option deletes the original `vendor/trustedlogin/`. Without it, the bare un-namespaced classes are still reachable through Composer's classmap, defeating the purpose of namespacing. **Side effect:** Strauss also generates `vendor/composer/autoload_aliases.php`, a dev-time back-compat shim that registers an SPL autoloader for the bare `\TrustedLogin\*` namespace. Per Strauss's own README, this file "should not be included in your production code" — and it isn't, because `/vendor` should be excluded from your release zip via your build pipeline (`.gitattributes export-ignore`, `.distignore`, build allowlist, etc.). - **`config.allow-plugins.composer/installers: true`** — Composer 2.9+ refuses to install plugin packages without explicit trust. Without this, `composer install` hangs on an interactive prompt (or fails in CI). - **`classmap_output: true`** — Strauss writes a self-contained classmap autoloader at `vendor-namespaced/composer/autoload_static.php`. You'll load `vendor-namespaced/autoload.php` from your bootstrap. ## Step 3. Run `composer install` ```bash composer install ``` The `post-install-cmd` script runs Strauss and the CSS namespacing build automatically. After `composer install` completes: - `vendor-namespaced/trustedlogin/client/` exists with the namespaced SDK. - `vendor/trustedlogin/` does **not** exist (Strauss deleted it per `delete_vendor_packages: true`). - The bundled CSS at `vendor-namespaced/trustedlogin/client/src/assets/trustedlogin.css` has been regenerated with prefixed class names. ## Step 4. Include the namespaced autoloader In your plugin's main file (or wherever you bootstrap), load the Strauss-generated autoloader on every page load: ```php // For a plugin or theme: require_once trailingslashit( dirname( __FILE__ ) ) . 'vendor-namespaced/autoload.php'; ``` :::warning **Don't load `vendor/autoload.php` for the SDK** — that resolves to the un-namespaced original (`\TrustedLogin\Client`) via the bare-namespace classmap (or, with `delete_vendor_packages: true`, an autoloader pointing at deleted files). Strauss's own self-contained autoloader is at `vendor-namespaced/autoload.php` regardless of the `classmap_output` setting. ::: If your plugin uses Composer for its own dependencies and already loads `vendor/autoload.php`, that's fine — both autoloaders can coexist. The namespaced TrustedLogin SDK lives only at `vendor-namespaced/`. ## Step 5. Configure and instantiate the Client Follow [the directions to configure and instantiate the client](../configuration). Use your prefix when referencing the namespaced classes: ```php new \ProBlockBuilder\TrustedLogin\Client( new \ProBlockBuilder\TrustedLogin\Config( $config ) ); ``` --- # Security Details Source: https://docs.trustedlogin.com/Client/security Markdown: https://docs.trustedlogin.com/Client/security.md # Security Details ## Logging in {#logging-in} Every time a login occurs using a TrustedLogin link, the login is also verified by the TrustedLogin service. See [TrustedLogin Flow](/flows) for details. ## Auto-expiring access {#auto-expiring-access} Accounts created with TrustedLogin auto-expire after a period of time defined in the Client configuration. Also, secrets stored in the Vault contain expiration timestamps. If the secret is older than the configured expiration time, the secret is deleted the next time it is requested. ## Capabilities {#capabilities} When creating a support user in TrustedLogin using the default `clone_role=true` configuration, it's not possible to assign these capabilities to the generated users: - `create_users` - `delete_users` - `edit_users` - `promote_users` - `delete_site` - `remove_users` In order to maintain a higher level of security, users created by TrustedLogin with the `clone_role` configuration enabled are not able to create other users. This will help prevent the possibility for support agents to create secret users for themselves. ## Access control {#access-control} At any time, a website administrator may revoke TrustedLogin access. When access is revoked locally, the secret is also deleted from the SaaS. ## Lockdown mode {#lockdown-mode} TrustedLogin should not generate multiple User Identifiers in frequent succession. If many User Identifiers are being used to attempt a login, it may be the sign of a brute force attack on the website. When TrustedLogin identifies more than 3 User Identifiers have been used in 10 minutes, TrustedLogin enables lockdown mode for the plugin for 20 minutes. **Lockdown mode:** - Prevents all site access using the plugin's TrustedLogin link - Notifies the TrustedLogin service of the lockdown - Runs the `trustedlogin/{namespace}/lockdown/after` action so developers can customize behavior ### Preventing sites from going into lockdown: {#preventing-sites-from-going-into-lockdown} When setting up TrustedLogin on a testing site, it may be helpful to temporarily disable lockdown mode. Security checks will automatically be disabled for `local` and `development` sites based on the value of the [`wp_get_environment_type()`](https://developer.wordpress.org/reference/functions/wp_get_environment_type/) function. You can also define a `TRUSTEDLOGIN_TESTING_{NAMESPACE}` constant in the site's `wp-config.php` file. ```php define( 'TRUSTEDLOGIN_TESTING_EXAMPLE', true ); ``` --- # Troubleshooting Source: https://docs.trustedlogin.com/Client/troubleshooting Markdown: https://docs.trustedlogin.com/Client/troubleshooting.md ## Redirects happen from the Connector plugin, but logins aren't happening. This can be caused by Client SDK initialization that is either too late, or initialization that doesn't occur on the front-end. (such as `admin_init`). - Check to make sure your initialization hook is early enough in the process. `init` is a good default. The `template_redirect` hook is the last possible hook you can use. [Here is an ordered list of WordPress hooks](https://developer.wordpress.org/apis/hooks/action-reference/). - Make sure your initialization hook is also running on the front-end. If you are using `admin_init`, it will not run on the front-end. Use `init` instead. ### Testing on staging/development servers {#testing-staging} :::note Development/Testing Only This section is only relevant if you're testing TrustedLogin on a staging or development server. Production use cases don't require this configuration. ::: If you see "Cannot reach [domain]" errors when testing TrustedLogin on a staging server that uses a production domain name, you may need to configure your hosts file. **Quick fix:** Edit your hosts file on the machine running the Connector plugin (the support person's computer): **Mac/Linux:** `sudo nano /etc/hosts` **Windows:** `C:\Windows\System32\drivers\etc\hosts` Add: ``` 192.168.1.100 example.com www.example.com ``` Replace `192.168.1.100` with your staging server IP and `example.com` with the actual domain. **Why:** When staging uses a production domain name but runs on a different IP address than DNS points to, your browser needs to be told where to connect. The hosts file overrides DNS on your local machine only. **Important:** - Include both www and non-www (DNS treats them as different hosts) - Remove this entry when done testing to access production normally - Each team member needs their own hosts file entry ### Nginx: Login requests fail with 301 redirect If you're using Nginx and login attempts fail silently, the issue may be a trailing slash redirect. Nginx (or WordPress) may be 301-redirecting requests to your TrustedLogin endpoint to add a trailing slash. The problem: 301 redirects convert POST requests to GET requests, which loses the authentication data needed for login. **How to diagnose:** Check your server logs or browser network tab for a 301 redirect on the login request URL. The URL will be redirected from a path without a trailing slash to one with a trailing slash. **The fix:** Add a 307 redirect rule to your Nginx configuration for the path being redirected. Unlike 301, a 307 redirect preserves the original HTTP method (POST stays POST). ```nginx # Replace "/your-path" with the actual path being 301-redirected # This could be a subdirectory where WordPress is installed, # or the TrustedLogin endpoint path itself location = /your-path { return 307 /your-path/; } ``` After adding this rule, reload Nginx: ```bash sudo nginx -t && sudo nginx -s reload ``` ### Check the TrustedLogin SDK log - Enable [logging in the configuration array](/Client/configuration) by setting `logging/enabled` to `true` and `logging/threshold` to `debug`. - Attempt a login. - Check the log file (the default location of the log is located at `wp-content/uploads/trustedlogin-logs/trustedlogin-client-debug-{date}-{hash}-.log`) If there are no new log items, then the Client SDK is not being initialized, likely due to the initialization hook not being early enough in the process or not running on the front-end. ## Troubleshooting the Grant Support Access screen First things first: make sure you have the [latest version of TrustedLogin](https://github.com/trustedlogin/client/releases) installed. Make sure you are logged in as an administrator and then add `&debug=true` to the end of the URL. That will activate Debug Mode, which shows more information about what's happening behind the scenes. ### Verify the Vendor public encryption key If access keys are generated, but the keys aren't working to log into a site, it may be a mismatched Vendor public encryption key. This is the key that TrustedLogin uses to encrypt the data sent to the Vendor. If this key is incorrect, the Vendor won't be able to decrypt the data and the Grant Support Access screen won't work. 1. With the debug information showing, click the "Verify Public Key" link. 2. This will open a new tab with the public key displayed in a JSON response: ```json { "publicKey": "a12bcd34e56db687a153b0a1aee26b196a75zba064ac62d8d41440455a8fb40f" } ``` 3. Check that the `publicKey` value matches the public key displayed in the debug information. **What to do if the public key doesn't match:** wait 10 minutes and try again. The Vendor public key is cached for a maximum of 10 minutes. If you check it again after 10 minutes, and it still doesn't match, contact TrustedLogin support. ### If the CSS isn't loading on the Grant Support Access page {#if-the-css-isnt-loading-on-the-grant-support-access-page} If you have [modified the CSS namespacing](/Client/namespacing/css-namespacing), that is the likley culprit. Otherwise, this is likely an issue with the `build-sass` script not being passed the same `namespace` flag as the Client is using. Make sure the `--namespace=` setting in the Composer file: ```javascript "trustedlogin": [ // highlight-next-line "@php vendor/bin/build-sass --namespace=example-namespace" ], ``` Matches the `vendor/namespace` setting in the Config settings array: ```php $config = [ // ... 'vendor' => [ // highlight-next-line 'namespace' => 'example-namespace', // ... ]; ``` If those are not the same, the CSS rules will not match the HTML generated and won't be applied. ## Security plugins blocking webhook requests {#security-plugins-blocking-webhooks} Some security plugins like Wordfence may block TrustedLogin webhook POST requests, flagging them as potential XSS attacks. This happens because the form-encoded POST body can trigger false positives in firewall rules. ### Symptoms - Webhooks aren't being received by your endpoint - Wordfence logs show "XSS: Cross Site Scripting in POST body" blocks - The webhook URL is correct but no data arrives ### Solution: Use JSON format Set the `webhook/format` configuration option to `'json'`. This sends the webhook data as JSON with a proper `Content-Type: application/json` header, which is less likely to trigger security plugin false positives. ```php $config = [ // ... 'webhook' => [ 'url' => 'https://hooks.example.com/webhook/', 'format' => 'json', // Use JSON format to avoid security plugin blocks ], ]; ``` Alternatively, use the filter: ```php add_filter( 'trustedlogin/your-namespace/webhook/request_args', function( $args, $webhook_url, $data, $format ) { $args['body'] = wp_json_encode( $data ); $args['headers']['Content-Type'] = 'application/json'; return $args; }, 10, 4 ); ``` :::note If your webhook endpoint is already configured to receive form-encoded data, you may need to update it to parse JSON instead when switching formats. ::: ## Composer-related issues with the namespacing setup ### Strauss isn't running — `vendor/bin/strauss` doesn't exist after `composer install` This is usually a stale `composer.lock`. Composer replays the existing lockfile and silently ignores newly-added requirements. Two fixes: - Delete `composer.lock` before running `composer install` so Composer re-resolves from scratch. - Or run `composer update brianhenryie/strauss scssphp/scssphp trustedlogin/client` to re-resolve only the packages you added. ### `composer install` fails with "Your requirements could not be resolved" mentioning packages I didn't change Composer 2.9+ enforces security advisories at install time. If your plugin pins old major versions of dev dependencies (commonly `phpunit/phpunit ^7.5` or `yoast/phpunit-polyfills 1.x`) with known advisories, `composer install` fails with an error naming those packages — even though they're unrelated to your TrustedLogin integration. The error includes the advisory ID (e.g. `PKSA-z3gr-8qht-p93v`). Two fixes: - **Bump the offending dev dep** (preferred — addresses the actual security issue). - **Skip the specific advisory** in `composer.json`: ```json "config": { "audit": { "ignore": ["PKSA-z3gr-8qht-p93v"] } } ``` Replace the advisory ID with whatever Composer reports. ### Strauss runs but produces no output (exit 0) This is usually `config.classmap-authoritative: true` in your `composer.json`. Under classmap-authoritative, Composer ignores PSR-4 lookups, so Strauss can't resolve `Composer\Factory` and exits silently. Set: ```json "config": { "classmap-authoritative": false } ``` You can restore it after install if your release pipeline relies on it. ### `composer install` fatals on plugin code that references WordPress classes Symptom: install fatals mid-Strauss-run with a confusing error like `Class WP_Widget not found`. This happens when your `composer.json` has `autoload.files` entries that reference WordPress-only classes — Composer eagerly loads `autoload.files` during install, *before* WordPress is loaded. **Don't add the TrustedLogin bootstrap to `autoload.files`.** Require it from your plugin's main file instead (which already has an ABSPATH check). If you have *other* `autoload.files` entries that reference WP classes, guard them: ```php visit this page on your website [UPDATE THE DOMAIN] and click "Grant Pro Block Builder Access". That will grant us temporary access to your site so that we can assist you with this issue.
``` Note: There's currently no way for Help Scout to dynamically insert the customer's URL, so you'll need to manually update the domain in the saved reply. We added an `[UPDATE THE DOMAIN]` reminder: the customer support agent will need to change the URL to match the customer's domain. Once the domain is updated, the agent can remove the `[UPDATE THE DOMAIN]` reminder. ![Help Scout Saved Reply](/img/client/saved-reply.png) ### 2. Integrating with your support form If you have a support form on your website, you can add a TrustedLogin field to proactively request access support while customers are creating their support ticket. This is a great way to streamline the process and make it easier for customers to grant access. We are in the process of creating TrustedLogin integrations with the following WordPress form plugins: - Gravity Forms - WS Form - Ninja Forms - WPForms - Formidable Forms - Contact Form 7 - Forminator - Fluent Forms - Elementor Forms If you are using one of these form plugins, you can expect an update soon that will allow you to add a TrustedLogin field to your support form. ## Adding reference IDs to Grant Access requests {#reference-ids} Reference IDs are useful when you want to attach a specific ticket ID or conversation ID to a login. Reference IDs can be passed via URL like so: `wp-login.php?action=trustedlogin&ns={namespace}&ref=[123]` When a Reference ID exists, users will see the reference while granting access: ![Reference ID is shown below the footer links in the Grant Access screen](/img/client/reference-id.png) --- # TrustedLogin Connector Plugin Source: https://docs.trustedlogin.com/Connector/intro Markdown: https://docs.trustedlogin.com/Connector/01-intro.md # TrustedLogin Connector Plugin A plugin to connect TrustedLogin's encrypted storage infrastructure using encrypted access keys. ## Why it's used {#why-its-used} ### Incomplete information {#incomplete-information} The [design of TrustedLogin](/flows) ensures that no site access data stored in the SaaS is sensitive: every access requires that the SaaS and Connector agree that the credentials are valid. ### Help Desk integration {#help-desk-integration} The Connector plugin is the bridge used for support desk integrations: when providing customer support in Help Scout, for example, the email address is sent to the Connector plugin. The plugin generates a list of licenses that are connected to that email address, generate hashes that are used as Secret IDs, then and ask the SaaS for a list of any matching Secret IDs. In this way, the SaaS knows nothing about the customer being supported, only the request for matching Secret IDs. #### [Read how to configure the Help Scout integration](./help-scout). {#read-how-to-configure-the-help-scout-integration} ## See developer docs: {#see-developer-docs} - [Local Development](./development) - [WordPress Hooks](./hooks) --- # Local Development Source: https://docs.trustedlogin.com/Connector/development Markdown: https://docs.trustedlogin.com/Connector/development.md # TrustedLogin Connector Plugin Development Plugin to interact with TrustedLogin's encrypted storage infrastructure to redirect support staff into an authenticated session on client installations. ## To Compile {#to-compile} The plugin will need to be built. Here's how: 1. Change directories to the plugin directory (`cd /path/to/directory`) 1. Run `composer install --no-dev` ## Code Standards Installation {#code-standards-installation} 1. Change directories to the plugin directory (`cd /path/to/directory`) 1. Run `composer install` - this will also install the code standards directory 1. Run `./vendor/bin/phpcs` ## Local Development Environment {#local-development-environment} A [docker-compose](https://docs.docker.com/samples/wordpress/)-based local development environment is provided. - Start server - `docker-compose up -d` - Acess Site - [http://localhost:6300](http://localhost:6100) - Run WP CLI command: - `docker-compose run wp cli wp ...` - `docker-compose run wpcli wp db reset` In the local development container, the constant `DOING_TL_VENDOR_TESTS` is set to true, as is `WP_DEBUG`. ### Running PHPUnit in Docker {#running-phpunit-in-docker} There is a special phpunit container for running WordPress tests, with WordPress and MySQL configured. - Enter container - `docker-compose run phpunit` - Test - `phpunit` ### Server-to-Server HTTP Requests {#server-to-server-http-requests} If the eCommerce app (the SaaS) is also running in `docker-compose`, this WordPress and the "web" service of app should be in `tl-dev` network. This allows you to make an HTTP request to the eCommerce app like this: ```php $r = wp_remote_get( 'http://web:80', ['sslverify' => false] ); ``` If this doesn't work, make sure a `tl-dev` network exists: ```bash docker network ls ``` If it does not, create one: ```bash docker network create tl-dev ``` --- # Encrypted Messages Source: https://docs.trustedlogin.com/Connector/encrypted-messages Markdown: https://docs.trustedlogin.com/Connector/encrypted-messages.md # Encrypted Messages TrustedLogin now encrypts the notifications your customers' sites send to yours when they grant, extend, or revoke support access. ## What Changed Previously, when a customer clicked "Grant Access," their site sent a plaintext webhook to your Connector — the access key, site URL, and optional debug data were visible in transit and in your server logs. Now, those notifications are **encrypted before they leave the customer's site** and buffered on the TrustedLogin SaaS until your Connector picks them up. Your Connector is the only system that can decrypt them. ## How It Works You don't need to change anything. If you're running Connector v1.4+, encrypted messages are enabled automatically alongside the existing webhook for backward compatibility. 1. **Customer grants access** → their site encrypts the notification with your Connector's public key. 2. **Notification is stored** on TrustedLogin's servers as an opaque blob — TrustedLogin cannot read it. 3. **Your Connector polls** every 5 minutes and decrypts the messages. 4. **Help desk integrations** (Help Scout, FreeScout) receive the decrypted data, same as before. ## Benefits - **No more plaintext in server logs.** The access key and debug data are encrypted end-to-end. - **Works behind firewalls.** Customers on VPN-only or firewalled networks can now send notifications — they only need outbound access to `app.trustedlogin.com`, which they already have. - **Reliable delivery.** Messages are buffered for up to 30 days, so a temporary outage on your side doesn't lose notifications. - **No configuration needed.** The Connector handles polling automatically. ## Polling Interval Your Connector checks for new messages every 5 minutes via WP-Cron. If you need real-time delivery, the TrustedLogin SaaS can push a notification to your Connector to trigger an immediate poll (coming in a future update). You can also click "Poll now" on the TrustedLogin Settings page to check immediately. ## Key Rotation Safety If you run "Reset All" on your Connector (which generates new encryption keys), there's a brief window where customer sites may still be using the old public key. TrustedLogin now handles this gracefully — old keys are retained for 20 minutes after rotation, so messages encrypted with the old key can still be decrypted. ## Backward Compatibility During the transition period, the client SDK sends **both** a traditional webhook POST (if configured) and an encrypted message. You can remove your webhook URL from client SDK settings once you've confirmed encrypted messages are working. ## FAQ ### Do I need to update the client SDK on customer sites? Yes — customers need to be running a client SDK version that supports encrypted messages (v1.10+). Older client SDK versions continue to send plaintext webhooks, which still work. ### Does this replace Help Scout / FreeScout integration? No. Encrypted messages are the *transport* — they replace the plaintext webhook, not the help desk integration. Once your Connector decrypts a message, it fires the same `trustedlogin_connector/message_received` action that your Help Scout or FreeScout integration hooks into. ### What if the TrustedLogin SaaS is down? Messages are buffered on the SaaS for up to 30 days. If the SaaS is temporarily unreachable, the customer's site logs the failure and moves on — the access grant still works, only the notification is delayed. ### Can TrustedLogin read my messages? No. The SaaS stores encrypted bytes it cannot decrypt. Only your Connector's private key can open them. --- # Help Scout App Source: https://docs.trustedlogin.com/Connector/help-scout Markdown: https://docs.trustedlogin.com/Connector/help-scout.md # Help Scout App Note: The TrustedLogin Help Scout app only works when a license key is passed using [the `auth/license_key` configuration setting](../Client/configuration). The email in Help Scout is matched against active licenses on your website. If matching licenses are found, the license key is used as a search key for access that has been granted. The Help Scout app currently supports Easy Digital Downloads Software Licensing. Other integrations are available upon request. --- :::tip Help Scout released a new Apps platform in August 2023. We are planning on supporting this in the near future, but for now, the TrustedLogin Help Scout integration requires you [create a Legacy Dynamic App](https://secure.helpscout.net/apps/custom). ::: ## Create a [Custom Help Scout App](https://secure.helpscout.net/apps/custom) {#create-a-custom-help-scout-app} [Click this link to create a Legacy Dynamic App](https://secure.helpscout.net/apps/custom). On this page, click on the Create App button. ![Create App button in the left sidebar](/img/vendor/help-scout/step-03.png) Now, switch to your website where you're running the TrustedLogin Connector plugin. ## Grab the configuration values from the TrustedLogin plugin {#grab-the-configuration-values-from-the-trustedlogin-plugin} If you haven't added any teams to the TrustedLogin Connector plugin yet, [do that first!](../01-intro) Then, on the TrustedLogin Teams page, click on the Configure Help Desk link. ![Inside the WordPress admin, with TrustedLogin "Teams" sub-menu selected and a teams list layout showing](/img/vendor/help-scout/step-04.png) Copy the Secret Key and Callback URL from the "Configure Help Desk" popup. ![A modal showing the Secret Key and Callback URL fields, both with copy icons next to them](/img/vendor/help-scout/step-05.png) ## Switch back to Help Scout {#switch-back-to-help-scout} After switching back to Help Scout, paste the Secret Key and Callback URL into the Help Scout Custom App inputs of the same name: !["Custom App" form with App Name, Content Type, Callback URL, Secret Key, Debug Mode, and Inboxes fields](/img/vendor/help-scout/step-06.png) :::warning If you don't see the "Content Type" dropdown, Help Scout is not running Legacy Dynamic Apps. Make sure you're using [this link](https://secure.helpscout.net/apps/custom) to create your Help Scout App. ::: Save the app and navigate to a Help Scout ticket. ## The TrustedLogin widget {#the-trustedlogin-widget} Now, in the sidebar, you'll see the TrustedLogin widget. ![](/img/vendor/help-scout/step-07.png) When someone grant access to their site who has an email associated with a license key, the widget will show a link to "Access Website". Click that link to be automatically redirected into your customers' site! ![A close-up of the TrustedLogin widget, showing "Access Website" link and "License is active" text](/img/vendor/help-scout/step-08.png) --- # WordPress Hooks Source: https://docs.trustedlogin.com/Connector/hooks Markdown: https://docs.trustedlogin.com/Connector/hooks.md # WordPress Hooks ## Filters {#filters} ## Secrets & infrastructure {#secrets-infrastructure} ### Trusted proxies
`trustedlogin/connector/trusted-proxies` {#trusted-proxies} Array of proxy `REMOTE_ADDR` values whose `X-Forwarded-For` and `CF-Connecting-IP` headers the plugin will trust when determining the client IP. **Default:** empty array — no forwarded headers are honored, `REMOTE_ADDR` wins. | Parameter | Type | Description | Default | Since | | --- | --- | --- | --- | --- | | `$proxies` | `string[]` | Exact-match `REMOTE_ADDR` values of trusted proxies/edges. | `[]` | `1.4` | This filter gates every use of `TrustedLogin\Vendor\Utils::get_ip()`, which feeds: - Per-IP rate limiting on the Secrets fetch (`GET /wp-json/trustedlogin/v1/secrets/{token}`), passphrase verification (`POST …/verify`), and reveal-nonce generation (`POST …/prepare`). - The `actor_ip` column on `wp_tl_secret_audit` rows. **If you run TrustedLogin Connector behind Cloudflare, a reverse proxy, or a load balancer, add your proxy IPs here.** Otherwise rate-limit buckets and audit logs will show the proxy/edge IP instead of the actual client. That's safe (unauthenticated attackers can't spoof out of their bucket by sending an `X-Forwarded-For` header) but can cause shared rate-limit collisions between unrelated recipients sitting behind the same edge. ```php add_filter( 'trustedlogin/connector/trusted-proxies', function ( $ips ) { return array_merge( $ips, [ '192.0.2.10', // reverse proxy '192.0.2.11', // reverse proxy (HA pair) ] ); } ); ``` Entries can be exact IPs or CIDR ranges (`192.0.2.0/24`, `2a06:98c0::/29`). For Cloudflare specifically — and for a full walkthrough of **why** this filter is opt-in, **what breaks without it**, and a copy-paste snippet covering every Cloudflare edge CIDR — see [Running behind a reverse proxy or CDN](./running-behind-a-proxy.md). ### Disable Secrets rate limiting
`trustedlogin/connector/secrets/rate-limit/enabled` {#secrets-rate-limit-enabled} Boolean gate on per-IP rate limits for the Secrets endpoints. Return `false` to disable. **Default:** `true`. | Parameter | Type | Description | Default | Since | | --- | --- | --- | --- | --- | | `$enabled` | `bool` | Whether to apply rate limiting to the current request. | `true` | `1.4` | | `$action` | `string` | The rate-limit bucket: `'fetch'` or `'verify'`. | — | `1.4` | | `$ip` | `string` | The requester's IP (after `trusted-proxies` resolution). | — | `1.4` | Only intended for controlled environments (e2e stacks, internal load tests). **Must remain `true` in production** — it's the primary brute-force defense on passphrase-protected secrets. ```php add_filter( 'trustedlogin/connector/secrets/rate-limit/enabled', '__return_false' ); ``` ### Override the debug constant
`trustedlogin/connector/debug-constant` {#debug-constant} Overrides the effective value of the `TRUSTEDLOGIN_DEBUG` constant at runtime. Useful for forcing debug logging on or off in environments where the constant has already been defined and cannot be modified. | Parameter | Type | Description | Default | Since | | --- | --- | --- | --- | --- | | `$debug` | `bool` | The effective debug state. | Value of `TRUSTEDLOGIN_DEBUG`, or `false` | `1.3` | ```php add_filter( 'trustedlogin/connector/debug-constant', '__return_true' ); ``` ## Help Scout integration {#help-scout-integration} ### Modify returned licenses array
`trustedlogin/connector/customers/licenses` {#modify-returned-licenses-arraybrtrustedloginvendorcustomerslicenses} | Parameter | Type | Description | Default | Since | | --- | --- | --- | --- | -- | | `$licenses` | `\EDD_SL_License[]`,`false` | License keys associated with the passed emails. | `[]` | `1.0.0` | | `$customer_emails` | `array` | Email addresses Help Scout associates with the customer. | `[]` | `1.0.0` | ### Widget template overrides {#widget-template-overrides} You can modify the template output implemented in the support desk (Help Scout or FreeScout) integrations using the following filters. Replace the `(helpscout|freescout)` placeholder in the filter name with the support desk you are using (`helpscout` with `freescout`). #### `trustedlogin/connector/helpdesk/(helpscout|freescout)/template/wrapper` {#trustedloginvendorhelpdeskhelpscouttemplatewrapper} HTML output of the wrapper HTML elements. #### `trustedlogin/connector/helpdesk/(helpscout|freescout)/template/item` {#trustedloginvendorhelpdeskhelpscouttemplateitem} HTML output of the individual items HTML elements. #### `trustedlogin/connector/helpdesk/(helpscout|freescout)/template/no-items` {#trustedloginvendorhelpdeskhelpscouttemplateno-items} HTML output of the HTML elements when no items are found. ## Actions {#actions} ### `trustedlogin_connector` {#trustedlogin_connector} This action is triggered after the plugin is initialized. ### `trustedlogin_connector_settings_saved` {#trustedlogin_connector_settings_saved} This action is triggered after the settings are saved or reset. --- # Running behind a reverse proxy or CDN Source: https://docs.trustedlogin.com/Connector/running-behind-a-proxy Markdown: https://docs.trustedlogin.com/Connector/running-behind-a-proxy.md # Running behind a reverse proxy or CDN If TrustedLogin Connector sits behind Cloudflare, a reverse proxy (nginx, HAProxy, AWS ALB), or a load balancer, this page is for you. It covers **why** the plugin doesn't trust forwarded IP headers by default, **what you'll see if you skip configuration**, and **how to configure it** for common setups — including a copy-paste snippet for Cloudflare. ## The short version By default the Connector trusts only `$_SERVER['REMOTE_ADDR']` (the TCP peer address) when determining the requester's IP. If you run behind a proxy, `REMOTE_ADDR` is your proxy's IP for every request — which degrades two things: - **Per-IP rate limits** on the Secrets endpoints bucket by proxy IP, so unrelated recipients behind the same proxy can collide in a single bucket. - **`actor_ip`** on the Secrets audit log shows the proxy IP, not the recipient's real IP, making after-the-fact incident response harder. To fix both, add your proxy's IPs to the `trustedlogin/connector/trusted-proxies` filter. The plugin will then read `X-Forwarded-For` or `CF-Connecting-IP` (the forwarded headers that carry the real client IP) — **but only for requests that actually came from a proxy you listed.** ## Why the opt-in default? A request's HTTP headers are plain text in the request body. Any client can send `CF-Connecting-IP: 1.2.3.4` from anywhere — it's not validated by TCP or TLS. The only field an attacker **can't** forge over a real HTTPS connection is `REMOTE_ADDR`, because the TCP three-way handshake won't complete if they can't actually receive packets at the address they claim to be sending from. So HTTP headers are trustworthy **only because of where they came from.** If Cloudflare set `CF-Connecting-IP`, it's accurate — Cloudflare fills in the value from data it can see and strips anything the original client tried to send. If anyone else set it, it's whatever they typed. The rule the plugin uses: > Trust a forwarded header **only when** the TCP peer (`REMOTE_ADDR`) is a proxy you've pre-declared. Pre-1.4, those headers were trusted unconditionally, which let any unauthenticated client burn a different IP's rate-limit bucket by sending a spoofed `X-Forwarded-For` — a rate-limit bypass. That's the hole closed in 1.4. ## What breaks without the filter Nothing dangerous — the defaults are **safe but degraded**: | Surface | Default behavior (filter empty) | With filter configured | |---|---|---| | Secrets per-IP rate limit | Bucketed by proxy IP — two unrelated recipients behind the same edge share a bucket | Bucketed by real client IP | | `wp_tl_secret_audit.actor_ip` | Proxy IP | Real client IP | | Spoofing resistance | Attacker can't spoof — forwarded headers are ignored | Attacker can't spoof from outside the proxy either (the `REMOTE_ADDR` gate still applies) | If you're a small-volume self-hoster, the degraded state may be fine. If you run a busy support operation behind Cloudflare, configure the filter. ## Configuring for Cloudflare Cloudflare publishes their edge IPs as CIDR ranges at [cloudflare.com/ips/](https://www.cloudflare.com/ips/). As of this writing, the public list is 15 IPv4 CIDRs and a handful of IPv6 CIDRs. The `trustedlogin/connector/trusted-proxies` filter expects an array of exact `REMOTE_ADDR` strings. When paired with the CIDR-matching support added alongside this page, you can list CIDRs directly: ```php Secrets** in your WordPress admin. 2. Paste the sensitive information into the text area. 3. Optionally set an expiration time (default: 24 hours) and a passphrase for extra protection. 4. Click **Create secret link**. 5. Copy the generated URL and paste it into your Help Scout reply, Slack message, or wherever you communicate with the customer. That's it. When the customer opens the link, they'll see a confirmation page, click "Reveal," and the secret appears. Once they close the page, the secret is gone forever. ## Passphrase Protection For extra-sensitive secrets, you can set a passphrase. The customer will need to type it before seeing the secret. **Important:** Share the passphrase through a *different channel* than the link. If you send both in the same email, the passphrase doesn't add any protection. After 5 wrong passphrase attempts, the secret is automatically destroyed. ## What the Customer Sees When a customer opens your link, they see a simple, clean page: 1. **"Click to reveal"** — a big button confirming they want to view the secret. This prevents link-preview bots (Slack, iMessage) from accidentally consuming the secret. 2. **The secret** — displayed in a read-only text box with a "Copy to clipboard" button. 3. **"You can close this window"** — once they've copied what they need. If they try to open the link again, they'll see: *"Information no longer available. Contact the person who sent you this link and ask them to create a new secret."* ## Expiration Options | Duration | Best for | |----------|----------| | 1 hour | Quick credential handoff during a live chat | | 24 hours (default) | Normal support ticket response | | 3 days | Customer who might not check email right away | | 7 days | Multi-day integrations or staging setups | | 14 days | Sprint-length or trial-length workflows | | 30 days (max) | Long-running projects | If the customer doesn't open the link before it expires, you'll see "unopened, expired" in your history — and you'll know to send a new one. ## Your Secret History Below the creation form, you'll see a table of your recent secrets with their status: - **Active** — the secret exists and hasn't been opened yet. - **Viewed** — the recipient successfully saw the secret. - **Expired** — the secret expired before anyone opened it. - **Burned** — you (or the system) destroyed it before it was viewed. ## Running Behind a Proxy or CDN If your site is behind Cloudflare, a reverse proxy, or a load balancer, tell TrustedLogin which proxies to trust so rate-limit buckets and the audit log can show the real client IP instead of the edge. Without this, every request from the same edge collapses into a single rate-limit bucket, which means one abusive recipient can trip the limit for everyone else sitting behind the same CDN — and `actor_ip` values in **TrustedLogin → Secrets → History** will all look like the edge. Add the filter from your theme's `functions.php`, a must-use plugin, or a site-specific plugin: ```php add_filter( 'trustedlogin/connector/trusted-proxies', function ( $ips ) { return array_merge( $ips, [ '192.0.2.10', // your reverse proxy / load balancer ] ); } ); ``` Entries can be exact IP strings or CIDR ranges (e.g. `192.0.2.0/24`, `2a06:98c0::/29`). Forwarded headers (`X-Forwarded-For`, `CF-Connecting-IP`) are only read when `REMOTE_ADDR` matches an entry here — this prevents unauthenticated clients from spoofing their IP to sidestep rate limits. For Cloudflare specifically, along with a full explanation of the trust model, see [Running behind a reverse proxy or CDN](./running-behind-a-proxy.md) — it includes a copy-paste snippet covering all Cloudflare edge CIDRs and guidance for nginx / Apache / HAProxy / cloud load balancers. ## Hardening (Optional) For maximum security, your site admin can add a dedicated security key to `wp-config.php`: ```php define( 'TRUSTEDLOGIN_SECRETS_HMAC_KEY', 'your-random-key-here' ); ``` Generate a key by running this in your terminal: ```bash php -r "echo bin2hex(random_bytes(32));" ``` This ensures that even a full database breach can't tamper with your secrets. Without this, TrustedLogin derives the key from your existing WordPress secret keys — still secure against most attacks, but this is an extra layer. ## FAQ ### Can I view my own secret? Yes, but you'll see a warning: *"You created this secret. If you view it, the recipient will not be able to see it."* Viewing your own secret consumes it — the customer won't be able to open it after you do. ### What if the customer's link doesn't work? The most common reason is that someone (or a bot) already opened it. Ask the customer to confirm, and create a new secret if needed. The audit trail in your history will show exactly what happened. ### Is the secret stored in my database? Only in encrypted form, and only temporarily. Your WordPress site stores ciphertext that it literally cannot decrypt — the key is in the URL fragment, which your server never receives. Once the secret is viewed or expires, even the ciphertext is permanently deleted. ### Does this work if my site has a Redis cache? Yes. TrustedLogin's secret storage intentionally bypasses WordPress's object cache to prevent cache eviction from silently destroying secrets. --- # Troubleshooting Source: https://docs.trustedlogin.com/Connector/troubleshooting Markdown: https://docs.trustedlogin.com/Connector/troubleshooting.md ## Cannot reach Client site (staging/development only) {#cannot-reach-staging} :::note Development/Testing Only This section only applies when testing TrustedLogin on staging/development servers. Production usage doesn't require this configuration. ::: If you see "Cannot reach [domain]" when testing on a staging server that uses a production domain name, you need to configure your hosts file. ### Quick fix Edit your hosts file **on your machine** (the support person's computer): **Mac/Linux:** `sudo nano /etc/hosts` **Windows:** `C:\Windows\System32\drivers\etc\hosts` (as Administrator) Add this line: ``` 192.168.1.100 example.com www.example.com ``` Replace `192.168.1.100` with your staging server IP and `example.com` with the actual domain. ### Why this is needed When staging uses a production domain but runs on a different IP than DNS points to, your browser needs to know where to connect. The hosts file overrides DNS locally. ### Important notes - Include both www and non-www (DNS treats them as different hosts) - Each team member needs their own hosts file entry - **Remove this entry when done** or you can't access production normally - Flush DNS cache after editing: `sudo dscacheutil -flushcache` (Mac) or `ipconfig /flushdns` (Windows) ## Access key not working If you enter an access key but receive an error like "Invalid access key" or "No matching sites found": ### Check the team/account selection If you have multiple TrustedLogin accounts/teams configured in the Connector: - Verify you selected the correct team from the dropdown - The access key must match the team that the Client site granted access to - Different products/teams have different encryption keys ### Verify the access key - Access keys are exactly 64 characters long - Check for copy/paste errors (extra spaces, line breaks) - Keys are case-sensitive - Keys expire after the configured access period (default: 7 days) ### Check the Client site On the Client site, verify: - Access was actually granted (check the TrustedLogin admin screen) - The access hasn't been revoked - The access period hasn't expired - The Client site can reach the TrustedLogin SaaS (https://app.trustedlogin.com) ## Access key login shows "Redirecting..." but never completes This usually indicates the POST request to the Client site failed. **Common causes:** 1. **DNS/hosts file issue** - See [Cannot reach domain](#cannot-reach-staging) above 2. **Firewall blocking POST request** - Check firewall rules 3. **Client SDK not initialized** - Client SDK must be initialized early enough (see [Client troubleshooting](/Client/troubleshooting)) 4. **Network timeout** - Client site takes too long to respond **Check browser console:** Open browser developer tools (F12) and check the Console tab for errors like: - `net::ERR_NAME_NOT_RESOLVED` - DNS issue, use hosts file - `net::ERR_CONNECTION_REFUSED` - Server not responding on that IP - `net::ERR_CONNECTION_TIMED_OUT` - Network timeout, check firewall/routing ## Help Scout integration not working See [Help Scout integration documentation](./help-scout) for specific troubleshooting steps. ## Enabling debug logs When diagnosing Connector issues, you can enable debug logging to capture detailed information about what the plugin is doing. ### Turn logging on There are two ways to enable logging: 1. **Settings toggle** — In the WordPress admin, go to **TrustedLogin → Settings** and enable **Error Logging**. 2. **Constant** — Define `TRUSTEDLOGIN_DEBUG` in `wp-config.php`. This forces logging on regardless of the settings toggle: ```php define( 'TRUSTEDLOGIN_DEBUG', true ); ``` ### Where logs are stored Logs are written to the WordPress uploads directory: ``` wp-content/uploads/trustedlogin-logs/vendor-{hash}.log ``` - `{hash}` is a randomized SHA-256 string generated once per site and stored in the `trustedlogin_vendor_log_location` option, so the filename is not guessable. - The `trustedlogin-logs/` directory is created automatically and protected with an `index.html` file to prevent directory browsing. - If the random hash can't be generated for some reason, logs fall back to `wp-content/uploads/trustedlogin-connector.log`. ### Reading the log Each line is formatted as: ``` [YYYY-MM-DD HH:MM:SS] [level] message {optional JSON context} ``` You can find the exact path for your site by checking the **TrustedLogin → Settings** screen, which displays the current log file location when logging is enabled. ### Disable logging when done Debug logs may contain sensitive request data. Turn the setting off (or remove the `TRUSTEDLOGIN_DEBUG` constant) once you've finished troubleshooting, and delete the log file from the uploads directory. --- # SaaS Intro Source: https://docs.trustedlogin.com/SaaS/intro Markdown: https://docs.trustedlogin.com/SaaS/01-intro.md # TrustedLogin SaaS (Hosted Application) The application handles account management, profiles, and billing. The SaaS receives and **processes login and validation requests** from the [Client SDK](../Client/intro) and [TrustedLogin Connector plugin](../Connector/intro). ## SLA {#service-level-agreement} We have a 99.99% uptime commitment for our Enterprise-level customers. Please [read the SLA on our website](https://www.trustedlogin.com/service-level-agreement/). ## HTTP API {#http-api} The TrustedLogin API is [documented on the TrustedLogin website](https://app.trustedlogin.com/docs/api/): - [Authenticating requests](https://app.trustedlogin.com/docs/api/#authenticating-requests) - [Accounts API](https://app.trustedlogin.com/docs/api/#accounts-api) - [Endpoints](https://app.trustedlogin.com/docs/api/#endpoints) - [Sites API](https://app.trustedlogin.com/docs/api/#sites-api) There's also a [Postman collection](https://app.trustedlogin.com/docs/collection.json) available. ## Server Setup {#server-setup} The TrustedLogin application is powered by Laravel and run on a Dockerized, high-availability, Kubernetes cluster. **[Learn more about the Server Setup](./server-setup)** --- # Data Storage Source: https://docs.trustedlogin.com/SaaS/data-storage Markdown: https://docs.trustedlogin.com/SaaS/data-storage.md # Data Storage ## Application {#application} The following PII (or potentially identifiable) data is stored in the MySQL database: - The site URL where access has been granted - The license key of software connected to granted access - User data - Email - Phone - Address ## Application Logs {#application-logs} Logs are sent to [Datadog](https://www.datadoghq.com/). The limited PII sent to Datadog includes: - The URL of the website where access has been granted - The User ID of the user who granted access - The IP address of the support person logging into sites using the Site Access Key ## Backups {#backups} Kubernetes cluster backups are kept for 3 days (72 hours), then are deleted. Backups are managed using Velero and are stored on Digital Ocean Spaces. ## Data Retention {#data-retention} Please refer to Our [Privacy Policy](https://www.trustedlogin.com/privacy-policy/#retention) for details. --- # Subcontractors Source: https://docs.trustedlogin.com/SaaS/subcontractors Markdown: https://docs.trustedlogin.com/SaaS/subcontractors.md # Subcontractors ## What subcontractors does TrustedLogin use? {#what-subcontractors-does-trustedlogin-use} ### TrustedLogin marketing site {#trustedlogin-marketing-site} | Service | URL | Description | |------------------|----------------------------------|------------------------------------------------------------| | Digital Ocean | https://www.digitalocean.com/ | Web hosting | | SpinupWP | https://spinupwp.com/ | WordPress hosting management | | Cloudflare | https://www.cloudflare.com/ | DNS and CDN | | Google Analytics | https://www.google.com/analytics/ | Analytics | | Help Scout | https://www.helpscout.net/ | Customer support. | | Google Fonts | https://fonts.google.com/ | Fonts | | BetterStack | https://betterstack.com | Uptime monitoring & reporting | | Jetpack | https://jetpack.com/ | WordPress security, spam filtering, and account management | | EasyDMARC | https://easydmarc.com | Email DMARC report monitoring | ### TrustedLogin Application {#trustedlogin-application} | Service | URL | Description | |------------------|--------------------------------|--------------------------------------------------------------| | Digital Ocean | https://www.digitalocean.com/ | Managed Kubernetes hosting | | Cloudflare | https://www.cloudflare.com/ | DNS and CDN | | Google Fonts | https://fonts.google.com/ | Fonts | | Mailgun | https://mailgun.com/ | Email | | ip2c.org | https://ip2c.org/ | IP to Country | | Help Scout | https://www.helpscout.net/ | Customer support | | GitHub | https://www.github.com | Source code repository, automated testing, docs site hosting | | Google Workspace | https://workspace.google.com | Organization email, docs, calendar | | Oh Dear! | https://www.ohdear.co/ | Uptime monitoring | | BetterStack | https://betterstack.com | Uptime monitoring & reporting | | Datadog | https://www.datadoghq.com/ | Logs | | EasyDMARC | https://easydmarc.com | Email DMARC report monitoring | --- # Support Access Diagrams Source: https://docs.trustedlogin.com/flows Markdown: https://docs.trustedlogin.com/flows.md # Support Access Diagrams TrustedLogin is designed to be simple, secure, and easy way for users to grant access to a support team. Thanks to the design of the service, **login credentials are end-to-end encrypted** and unable to be accessed by TrustedLogin. Below are simplified visualizations of the flow of data between the various components of TrustedLogin. **The three parts of TrustedLogin:** 1. [**TrustedLogin service**](Saas/intro), running on [app.trustedlogin.com](https://app.trustedlogin.com) 2. [**Connector plugin**](Connector/intro), running on the software provider's website 3. [**Client**](Client/intro), either as a stand-alone TrustedLogin plugin or the SDK integrated with a WordPress plugin or theme Together, these three components allow for site access to be granted securely and with minimal effort. ## Support Access Flow {#support-access-flow} ### What happens when a customer or client grants access to their website: {#what-happens-when-a-customer-or-client-grants-access-to-their-website} [![Flow of customer granting access to a website](/img/TrustedLogin-Support-Access-Flow.jpg)](/img/TrustedLogin-Support-Access-Flow.jpg) ### Step 1: User Grants Access {#step-1-user-grants-access} User grants access to Vendor via the Client SDK. ![Grant Access form](/img/flows/grant-access/step-01.png) This creates a user in WordPress with the defined roles. A "User Identifier" is created and a hash is stored in the WordPress user meta (see [`\TrustedLogin\Client\SupportUser::setup()`](https://github.com/trustedlogin/client/blob/main/src/SupportUser.php#L534)). The User Identifier will be used when the Vendor logs in. In addition, a `Secret ID` is generated and added to the usermeta. This hash is used as the storage ID when the site is added to the SaaS Vault. ### Step 2: Public Key is Requested {#step-2-public-key-is-requested} The Client SDK requests the public key from the `wp-json/trustedlogin/v1/public_key` endpoint from the Vendor's website. :::note The public key is fetched by default from the [URL defined in the `vendor/website` setting](./Client/configuration). It's possible to override this using the [`trustedlogin/{namespace}/vendor/public_key/endpoint` filter](./Client/hooks#trustedloginnamespacevendorpublic_keyendpoint). ::: ### Step 3: Public Key is Generated {#step-3-public-key-is-generated} The public key request is handled by [`\TrustedLogin\Vendor\Endpoints\PublicKey::get()`](https://github.com/trustedlogin/trustedlogin-connector/blob/aac75e18d21728155b76537f908031fc17cd562a/php/Endpoints/PublicKey.php#L20), which uses [`\TrustedLogin\Vendor\Encryption::generateKeys()`](https://github.com/trustedlogin/trustedlogin-connector/blob/aac75e18d21728155b76537f908031fc17cd562a/php/Encryption.php#L100) to generate two sets of encryption keys (`crypto_sign` and `crypto_box` key pairs) but only returns the `crypto_box` public key. ### Step 4: Envelope Created & Encrypted {#step-4-envelope-created--encrypted} The envelope is generated and encrypted using Vendor public keys. The Client [`\TrustedLogin\Client\Envelope::get()`](https://github.com/trustedlogin/client/blob/main/src/Envelope.php#L60) uses [`\TrustedLogin\Client\Encryption::generate_keys()`](https://github.com/trustedlogin/client/blob/main/src/Encryption.php#L351), [`\TrustedLogin\Client\Encryption::encrypt()`](https://github.com/trustedlogin/client/blob/main/src/Encryption.php#L273), [`\TrustedLogin\Client\Encryption::get_vendor_public_key()`](https://github.com/trustedlogin/client/blob/main/src/Encryption.php#L176). The Vendor's public key is stored in the Client using the WordPress options table. The key expires after 10 minutes and will be re-fetched. ### Step 5: Client `POST`s Envelope to SaaS {#step-5-client-posts-envelope-to-saas} The Client SDK, using, [`\TrustedLogin\SiteAccess::sync_secret()`](https://github.com/trustedlogin/client/blob/main/src/SiteAccess.php#L51) makes a `POST` request to `https://app.trustedlogin.com/api/v1/sites`. This is handled by [`\App\Http\Controllers\SiteController::createSite()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Controllers/SiteController.php#L89). [See endpoint documentation](https://app.trustedlogin.com/docs/api/#create-a-site). ### Step 6: SaaS Stores Envelope in Vault {#step-6-saas-stores-envelope-in-vault} In the SaaS, [`SiteController::createSite()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Controllers/SiteController.php#89) generates Vault tokens to create a secret and stores the envelope in the Vault. The [`SiteCreatedEvent()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Controllers/SiteController.php#L129) event is triggered in Laravel. This logs the event to Elasticsearch. The successful response from the SaaS to the Client is: ```json { "success": true } ``` The **unsuccessful** response is: ```json { "message": "'Error', or \Exception::getMessage() value." } ``` ## Support Logging Into a Customer/Client Website {#support-logging-into-website} [![Swimlane diagram of the login flow for accessing a client website](/img/TrustedLogin-Login-Flow.jpg)](/img/TrustedLogin-Login-Flow.jpg) ### Step 1: A Person Submits Site Access Form {#step-1-a-person-submits-site-access-form} ![Site Access Key login form](/img/flows/login/step-01.png) The form submits a `POST` HTTP request via AJAX that is received by the [`TrustedLogin\Vendor\AccessKeyLogin::handle()`](https://github.com/trustedlogin/trustedlogin-connector/blob/main/php/AccessKeyLogin.php#L106) method. Receiving that request, [`TrustedLogin\Vendor\AccessKeyLogin::verifyGrantAccessRequest()`](https://github.com/trustedlogin/trustedlogin-connector/blob/main/php/AccessKeyLogin.php#L200) verifies that the nonce is valid and that the request is coming from inside the site. In addition, [`TrustedLogin\Vendor\Traits\VerifyUser::verifyUserRole()`](https://github.com/trustedlogin/trustedlogin-connector/blob/develop/php/Traits/VerifyUser.php#L17) checks to make sure the user is logged-in and has one or more of the roles that are required to access the site. ### Step 2: Vendor Requests List of Matching Site IDs {#step-2-vendor-requests-list-of-matching-site-ids} The Connector plugin requests a list of Site IDs that match that access key by sending a `POST` request to the `accounts/{$account_id}/sites/` SaaS endpoint. The request includes an `Authorization: Bearer {hashed private key}` header as well as the following body: ```json { "searchKeys": [ "The submitted Site Access Key"] } ``` ### Step 3: SaaS Verifies Request and Returns Site IDs {#step-3-saas-verifies-request-and-returns-site-ids} The SaaS verifies the hashed Bearer token passed in the `Authorization` header using [`\App\Http\Middleware\CheckPrivateKey::handle()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Middleware/CheckPrivateKey.php#L36). Then the SaaS checks to make sure the Vendor account isn't in "Pause Mode", which is triggered by brute force attempts. When Pause Mode is enabled, new access may be granted, but site login and lookups are restricted. See [`/Http/Middleware/CheckPauseMode.php`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Middleware/CheckPauseMode.php#L20). `\App\Http\Controllers\SiteController::siteByLicenseOrAccessKeys()` is called to retrieve a list of sites stored in the Vault. [Read the endpoint documentation](https://app.trustedlogin.com/docs/api/#lookup-site-by-access-keys-or-hashed-licesne-keys). An array of Secret IDs is returned. These are not the envelope itself; Secret IDs refer to the IDs of Vault secrets. ```json { "accessKey1": [ "secretId1" ], "accessKey2": [ "secretId2", "secretId3" ] "accessKey3": [ "secretId2", "secretId3" ] } ``` ### Step 4: Connector Plugin Requests Matching Envelope(s) from SaaS {#step-4-connector-plugin-requests-matching-envelopes-from-saas} The Connector plugin uses the Secret IDs to retrieve the envelopes from the Vault. In addition to the Bearer token, the request generates a signed nonce in [`TrustedLogin\Vendor\Encryption::createIdentityNonce()`](https://github.com/trustedlogin/trustedlogin-connector/blob/develop/php/Encryption.php#L399). The method: - Generates a cryptographic nonce (in [`TrustedLogin\Vendor\Encryption::generateNonce()`](https://github.com/trustedlogin/trustedlogin-connector/blob/develop/php/Encryption.php#L485) using `random_bytes()`), - Signs the nonce with the `sign_private_key` pair (in [`TrustedLogin\Vendor\Encryption::sign()`](https://github.com/trustedlogin/trustedlogin-connector/blob/develop/php/Encryption.php#L512), using `sodium_crypto_sign_detached()`), and - Verifies that the signed nonce has been properly generated (using `sodium_crypto_sign_verify_detached()`) The nonce and signed nonce are both sent in the request, helping to verify that this site is indeed the sender of the data. A `POST` request is made to `sites/{account_id}/{secret_id}/get-envelope` to retrieve the envelope from the Vault. The Bearer token is passed in the `Authorization` header, and a `X-TL-TOKEN` header is also sent. The `X-TL-TOKEN` header is a hash of the Vendor private and public keys. ### Step 5: SaaS Verifies Request and Returns Envelope(s) {#step-5-saas-verifies-request-and-returns-envelopes} The SaaS verifies the hashed Bearer token and ensures that the Vendor account isn't in Pause Mode (see Step 3). The SaaS verifies the signed nonce using [`\App\Http\Middleware\CheckSignedNonce::handle()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Middleware/CheckSignedNonce.php#L35). The request is handled by [`\App\Http\Controllers\SiteController::getEnvelope()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Controllers/SiteController.php#L293), which retrieves the envelope from the Vault. Inside `getEnvelope()`, the `X-TL-TOKEN` token is verified against the Vendor's account information. The envelope with encrypted credentials is returned to the Vendor. ### Step 6: Connector Plugin Receives & Decrypts Envelope {#step-6-connector-plugin-receives--decrypts-envelope} The Connector plugin receives the envelope. It includes the Site URL associated with the Site Access Key but not the endpoint, which is required to log in. The Connector plugin decrypts the envelope and extracts the credentials, then cryptographically generates the URL to access Client site (using [`TrustedLogin\Vendor\TrustedLoginService::envelopeToUrl()`](https://github.com/trustedlogin/trustedlogin-connector/blob/a62ec370bb5e715eed3524bf92c77482e785d273/php/TrustedLoginService.php#L395)). The site URL and the access parts are returned as an AJAX response, completing the request started in Step 1. ### Step 7: Connector Plugin `POST`s to Client Site {#step-7-connector-plugin-posts-to-client-site} A temporary form [is created using JavaScript](https://github.com/trustedlogin/trustedlogin-connector/blob/a62ec370bb5e715eed3524bf92c77482e785d273/src/components/AccessKeyForm.js#L259-L281) with the Client Site URL set as the form `action` property. A `POST` request is submitted, preventing the submitted data from being logged. The form submits the following to the Client Site URL: ```http request [ method: 'POST', action: 'trustedlogin', endpoint: {endpoint}, identifier: {identifier} ] ``` When the form submits, the user on the Vendor website is automatically redirected to the Client site. ### Step 8: Client Verifies Login Request {#step-8-client-verifies-login-request} The login request is received by the Client in `{Client}\TrustedLogin\Endpoint::maybe_login_support()`. The SDK performs security checks, including: 1. The raw User Identifier value is found (using [`{Client}\TrustedLogin\Endpoint::get_user_identifier_from_request()`](https://github.com/trustedlogin/client/blob/main/src/Endpoint.php#L277)) and then verified (using `{Client}\TrustedLogin\SecurityChecks::verify()`). 2. The SDK checks whether a brute-force attack is underway (via `{Client}\TrustedLogin\SecurityChecks::do_lockdown()`). If an attack is determined, the code prevents login and enters Lockdown Mode. [See the Security doc](./Client/security#lockdown-mode) for more information about Lockdown Mode. 3. The SDK determines whether the user access period has expired. If it has, the user is deleted and the login is prevented. 4. The SDK sends a request to the SaaS to confirm that the validity of the request. The following information is sent to `sites/{secret_id}/verify-identifier` using a HTTP `POST` request: :::note `{secret_id}` Refers to the Secret ID stored in user meta. It is returned using `{Client}\TrustedLogin\SupportUser::get_secret_id()`. ::: ```json { 'timestamp': time(), 'user_agent': $_SERVER['HTTP_USER_AGENT'], 'user_ip': $this->get_ip(), 'site_url': get_site_url(), } ``` ### Step 9: SaaS Also Verifies Login Request {#step-9-saas-also-verifies-login-request} The SaaS receives the `verify-identifier` request and processes it using `App\Http\Controllers\VerifyIdentifierController::handle()`. The method verifies that the secret still exists in the Vault (it hasn't been deleted), and that the Vendor account is not in Pause Mode. If success, the SaaS returns an empty JSON response `[]` with a `204` HTTP status code. Possible error responses are indicated using the HTTP status codes `423` and `404`: - `423`: The Vendor account is in Pause Mode - `404`: The Secret ID does not match any secrets in the Vault ### Step 10: Client Logs User In {#step-10-client-logs-user-in} If the security checks pass in Step 8 and 9, the SDK calls `{Client}\TrustedLogin\Endpoint::login()` to log the support user in. The user is logged-in by calling `wp_set_current_user()`, `wp_set_auth_cookie()` and `do_action( 'wp_login' )`. Yay! 🎉 The user's now logged-in. ### Step 11: Action Is Triggered {#step-11-action-is-triggered} After login, the SDK triggers the following WordPress action: `trustedlogin/{namespace}/logged_in`. This allows other plugins to perform actions and to trigger webhooks. The SDK hooks into the action to run any webhooks configured in the [Config array](./Client/configuration#webhooks). ### Revoke Login {#revoke-login} At any time, a website administrator may revoke TrustedLogin access. When access is revoked, the Client sends a HTTP `DELETE` request to the `sites/{secret_id}` endpoint along with a `X-TL-TOKEN` header. The body of the request is: ```json { 'publicKey': {Client SDK API Key} } ``` If the public key has been cycled, the request will fail. Handled by [`\App\Http\Controllers\SiteController::deleteSite()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/master/app/Http/Controllers/SiteController.php#L378). Possible responses are indicated using the HTTP status codes: - `201`: Secret successfully deleted - `200`: Secret failed to be deleted in [`\App\Http/Clients/Vault::deleteSite()`](https://github.com/trustedlogin/trustedlogin-ecommerce/blob/522ac00bcfc02926604e852cd372571873d91710/app/Http/Clients/Vault.php#L91) - `404`: The Secret ID does not match any secrets in the Vault or there - `500`: An exception occurred --- # For AI assistants & tools Source: https://docs.trustedlogin.com/for-ai-tools Markdown: https://docs.trustedlogin.com/for-ai-tools.md # For AI assistants & tools Every page on this site is published in three forms so AI assistants and content tools can ingest the documentation efficiently: ## Per-page Markdown Append `.md` to any URL on this site to fetch the raw Markdown source instead of the rendered HTML. Examples: | Rendered HTML | Raw Markdown | | --- | --- | | [`/Client/intro`](/Client/intro) | [`/Client/intro.md`](/Client/intro.md) | | [`/Client/integration-prompt`](/Client/integration-prompt) | [`/Client/integration-prompt.md`](/Client/integration-prompt.md) | | [`/Client/troubleshooting`](/Client/troubleshooting) | [`/Client/troubleshooting.md`](/Client/troubleshooting.md) | The Markdown form has the same content as the rendered page, with public-friendly frontmatter (`title`, `description`, `keywords` only — Docusaurus-internal fields like `sidebar_position` are stripped). Admonitions like `:::tip` and `:::warning` are preserved verbatim — most Markdown parsers handle them transparently. ## `` auto-discovery Every rendered page declares its Markdown counterpart in the document head: ```html ``` Crawlers, Reader-mode extensions, and AI assistants that follow the `alternate` discovery convention pick up the Markdown form automatically. ## `/llms.txt` and `/llms-full.txt` Following the [llmstxt.org](https://llmstxt.org) convention: - **[`/llms.txt`](/llms.txt)** — A single-file index of every documentation page, organized by section, with title and description. AI crawlers fetch this once to discover the full site structure. - **[`/llms-full.txt`](/llms-full.txt)** — The full Markdown content of every page concatenated into a single document. AI assistants can fetch this once and have the entire docs corpus in context, no further fetches needed. Both files are regenerated on every site build, so they're always in sync with the published docs. ## Recommended ingestion workflow If you're an AI assistant integrating TrustedLogin into a customer's plugin: 1. **For a focused integration task:** fetch [`/Client/ai-integration-prompt.md`](/Client/ai-integration-prompt.md) — pure prompt body, no preamble. Self-contained walkthrough including input collection, host-side conflict detection, the bootstrap, and a verification checklist. (The same page wrapped with human-friendly intro is at [`/Client/integration-prompt.md`](/Client/integration-prompt.md).) 2. **For broader context (multiple docs):** fetch [`/llms-full.txt`](/llms-full.txt) — single fetch, full corpus. 3. **For navigation and discovery:** fetch [`/llms.txt`](/llms.txt) and follow the per-section links. The Markdown source is the ground truth; if you hit ambiguity in any rendered page, fetching its `.md` form is the fastest way to disambiguate. --- # Getting Started Source: https://docs.trustedlogin.com/getting-started Markdown: https://docs.trustedlogin.com/getting-started.md # Getting Started ## Adding TrustedLogin to your project involves: 1. Setting up an account on [trustedlogin.com](https://app.trustedlogin.com) 2. Configure your settings on TrustedLogin 3. Install and configure the TrustedLogin Connector plugin 4. Including and configuring the client SDK ("Software Development Kit") Let's get started! ---- ## 1. Create an Account on [TrustedLogin.com](https://app.trustedlogin.com/register) {#1-create-an-account-on-trustedlogincom} 1. Visit [TrustedLogin.com to register](https://app.trustedlogin.com/register) ![Screenshot of the registration form](/img/saas/registration-form.png) You will be sent an email to verify your email address. Once you've verified your email address, you can log in to TrustedLogin. Note: Two-Factor Authentication (2FA) is required for all users with access to TrustedLogin.com. There is no way to disable 2FA. ## 2. Configure your team settings on TrustedLogin {#2-create-a-team} When you register, a starter team is created for you. You can create additional teams for each plugin, theme, or client you are working with. ### Once logged-in to TrustedLogin's admin, click on the "Team Settings" link ![TrustedLogin menu with the Team Settings link highlighted](/img/saas/team-settings-sidebar.png) #### Name Enter the name of your team. This could be the name of your plugin, theme, or client. #### REST API Endpoint Enter the full REST API URL path to the website where you will run the TrustedLogin Connector plugin. This URL should include the path to the JSON REST API endpoint. For example, if your website is `https://example.com`, the REST API URL is, by default, `https://example.com/wp-json/`. #### Support URL Enter the URL to the support page for your plugin, theme, or client. ### Save your settings ## 3. Install the TrustedLogin Connector plugin {#3-install-the-trustedlogin-connector-plugin} Here, for example, is how GravityView's settings are configured: ![GravityView settings configuration: Project Name, REST API URL, and Support URL.](/img/saas/team-settings.png) :::info Don't close the tab! We'll be coming back here to grab the Account ID, Public Key, and Private Key in the next step. ::: ## 3. Install the TrustedLogin Connector plugin {#3-install-the-trustedlogin-connector-plugin} The TrustedLogin Connector plugin is a WordPress plugin that you host on your own site. The Connector plugin is what makes TrustedLogin so secure: secrets are encrypted and decrypted using keys that are generated by the Connector plugin. 1. [Download the Connector plugin](https://github.com/trustedlogin/trustedlogin-connector/releases/download/v1.1.1.0/trustedlogin-connector-1.1.1.zip) 2. Upload the plugin to your WordPress installation 3. Click the new "TrustedLogin" menu item in the sidebar menu ![The TrustedLogin sidebar menu item](/img/vendor/trustedlogin-sidebar-menu.png) ### Enter the Account ID, Public Key, and Private Key from TrustedLogin.com Now configure the plugin using the Account ID, Public Key, and Private Key values from the TrustedLogin.com Team page. In addition, select the WordPress user roles that should be able to use TrustedLogin. A common configuration is to allow Administrators and Editors to use TrustedLogin. These settings can be updated later. ![A screenshot of the Add Team screen in TrustedLogin Connector plugin. There is a form showing the described fields.](/img/vendor/add-team.png) When a connection is successfully established, you will see "All Teams Connected" and see your team in a list. ![The Teams screen, showing Pro Block Builder as a team row and an All Teams Connected badge at the top.](/img/vendor/teams-screen.png) ### If you don't see "All Teams Connected", enable logging If you don't see "All Teams Connected", enable logging in the TrustedLogin Connector plugin settings. This will help you troubleshoot any issues. 1. Go to the TrustedLogin menu, click Settings, enable Debug Logging. 2. Then try connecting again. 3. Go back to Settings and copy the path to the log file. 4. Open the log file in your browser to see what's going on. :::warning Make sure to disable logging when you're done troubleshooting. The log file can contain sensitive information. ::: ## 4. You're ready to integrate with your plugin or theme!{#4-integrate-with-your-plugin-or-theme} Now check out the [Client SDK Integration](client/installation) instructions for how to integrate with your plugin or theme. --- # Security Source: https://docs.trustedlogin.com/security Markdown: https://docs.trustedlogin.com/security.md # Security :::info For Client SDK security, see [Client SDK](/Client/security). ::: ## Encryption {#encryption} The ID of the user who granted access to the website, the URL of the website where access is being granted as well as vendor-defined array of metadata are stored unencrypted. Login credentials are encrypted using Sodium [sealed boxes](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes) using keys generated using on the Vendor website. Because cryptobox encryption cannot verify the identity of the sender, during decryption requests, the clients send additional headers (`X-TL-TOKEN`) with each request. The `X-TL-TOKEN` hash includes private keys only known to the Vendor and SaaS. Those private keys, if compromised, can be cycled SaaS-side. ## Encrypted-at-rest data storage {#encrypted-at-rest-data-storage} Secrets are encrypted and stored using the [Sodium Secret Box](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes) algorithm in the Hashicorp Vault. :::note For more information around data storage, see [SaaS Data Storage](/SaaS/data-storage). ::: ## SaaS application security {#saas-application-security} ### IP restrictions {#ip-restrictions} The SaaS Vault, Elasticsearch, and Kibana are protected behind IP restrictions using Traefik. [See how Traefik is used](/SaaS/server-setup#traefik). ### Strong-password policy {#strong-password-policy} The TrustedLogin application has a minimum password length of 12 characters. Passwords are required to meet [zxcvbn level 4](https://github.com/dropbox/zxcvbn): "very unguessable: strong protection from offline slow-hash scenario." ### 2FA {#2fa} The application requires two-factor authentication (2FA) to create an account and 2FA is required on every login. ## Cleanup {#cleanup} When accounts are deleted, the secrets associated with the team are deleted from the Vault. Deleting a team triggers the `Laravel\Spark\Events\Teams\TeamDeleted` event. The following listeners are triggered by the `Laravel\Spark\Events\Teams\TeamDeleted` event: - `\App\Listeners\RemoveTeamFromVault` - `\App\Listeners\DeleteTeamElasticSearchData` :::note Team-specific data is removed from Elasticsearch, but non-identifiable usage data is kept for administrative reporting purposes. :::