Skip to main content

AI Integration Prompt

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 — your AI assistant can fetch it directly. See For AI assistants & tools for the full set of conventions (per-page Markdown, <link rel="alternate"> discovery, /llms.txt, /llms-full.txt).

Or fetch only the prompt itself, with no preamble or meta sections, at /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 + Namespacing with Strauss + Configuration + Merging into an existing composer.json. 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 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.

# 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)

  1. 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-builderProBlockBuilder. widgets_coWidgetsCo. acmeAcme. This is what Strauss uses to prefix every namespaced class — do not collide with another Composer dependency you ship.

  2. 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.

  3. title — human name shown to customers (e.g. "Pro Block Builder"). Required.

  4. display_name — optional. If set, the UI says "{title} {display_name}" (e.g. "Pro Block Builder Support").

  5. 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.

  6. website, support_url — both must be valid http(s) URLs. support_url is also the fallback link shown when the SaaS is unreachable.

  7. logo_url — optional. Must be a local asset (use plugins_url(...) against the plugin's main file constant). External URLs fail WordPress.org review.

  8. 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 fallbackdefined('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 requireing 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.

  9. 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.

  10. 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.".

  11. 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.

  12. 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.
  13. 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.

  14. terms_of_service_url — optional. Adds the line "By granting access, you agree to the Terms of Service" under the button.

  15. 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:

{
"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-ignored / .distignored 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:

"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.filesrequire_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
if ( ! defined( 'ABSPATH' ) ) {
return;
}
// rest of file

(Use return; not exit;exit aborts the entire composer install process; return; just stops loading the one file.)

Existing allow-plugins blocks. If the host already declares config.allow-plugins, add composer/installers: true alongside whatever's there rather than overwriting.

Stale composer.lock. Critical and easy to miss: composer install replays the host's existing lockfile verbatim and silently ignores anything new you added to require / require-dev. After editing composer.json, either delete composer.lock so Composer re-resolves from scratch, or run composer update trustedlogin/client brianhenryie/strauss scssphp/scssphp to re-resolve only the packages you added. Symptom if you don't: "I added Strauss 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 change. 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).
  • Add "config": { "audit": { "ignore": ["PKSA-xxxx-xxxx-xxxx"] } } to skip the specific advisory. The advisory ID is in the resolution failure output.

Audit blocks are only triggered on install / update, not at runtime, so customers never see them — but they will wedge your build until resolved.

2. WordPress 4.1+ support (only if needed)

The SDK uses libsodium. WordPress 5.2+ ships sodium natively — if your plugin's minimum WP is 5.2, skip this section. If you support WP 4.1–5.1, add to require:

"paragonie/random_compat": "<9.99",
"paragonie/sodium_compat": "^1.12"

These also need to be Strauss-namespaced — add them to extra.strauss.packages.

3. CSS namespacing

The SDK's bundled CSS uses generic class names. The bin/build-sass --namespace={{prefix}} step (wired into post-install-cmd above) emits tl-<prefix lowercased>-* class names (e.g. --namespace=ProBlockBuildertl-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:

$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:

// 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/<topic> (e.g. feature/trustedlogin), <issue-number>-<slug> (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 <?php opener).
  • vendor/trustedlogin/ does not exist after composer install (this is what delete_vendor_packages: true enforces — if the directory is still there, Strauss didn't run, or the option isn't being honored).
  • Generated CSS in vendor-namespaced/trustedlogin/client/src/assets/trustedlogin.css contains class names prefixed with {{prefix}} lowercased (e.g. .tl-problockbuilder-auth for --namespace=ProBlockBuilderbuild-sass lowercases the prefix when emitting CSS), not the bare default tl-test- selectors.
  • php -l clean on the bootstrap and on vendor-namespaced/trustedlogin/client/src/Client.php.
  • Activating the plugin on a clean WP install does not warn or fatal.
  • Visiting wp-login.php?action=trustedlogin&ns={namespace} renders the Grant Access screen with the vendor's title, logo, and capability reasons.
  • define( 'TRUSTEDLOGIN_DISABLE_{NS_UPPER}', true ); in wp-config.php removes the menu and disables the endpoint.
  • Defining a deliberately bad auth/api_key (or invalid URL) results in a logged exception, not a fatal.
  • PHP 5.3 / WP 5.2 minimums are respected in any glue code you write (no arrow fns, no typed properties, no ??, no match).
  • PHPCS (WPCS) and PHPStan, if configured, pass on your new files.

If any item fails, fix the underlying cause — do not relax the check.