# Battle Ready Tech extension development
This file is generated from the same versioned sources used by the human documentation.
# BRT website extension developer kit
Build a website extension that a store can install, configure, and run without giving your code access to the storefront session or DOM.
This directory is the source for the public contract at `https://developers.battlereadytech.com/extensions/v1`. The files here explain how to build against extension protocol version 1.
## Start in five minutes
Requirements: Node.js 20 or newer. The developer CLI has no package dependencies.
```bash
node tools/web-extensions/brt-extension.mjs init ./my-extension \
--namespace your-company/my-extension \
--name "My extension"
node tools/web-extensions/brt-extension.mjs preview ./my-extension
node tools/web-extensions/brt-extension.mjs check ./my-extension
```
Open the preview URL printed by the command. Stop it with `Ctrl+C`.
When the extension is ready, generate a publisher key once. Keep the private key outside the project and outside source control.
```bash
node tools/web-extensions/brt-extension.mjs keygen \
--private-key ~/.config/brt/extensions/publisher-private.pem \
--public-key ./publisher-public-key.txt
node tools/web-extensions/brt-extension.mjs package ./my-extension \
--out ./dist/my-extension-0.1.0 \
--private-key ~/.config/brt/extensions/publisher-private.pem
node tools/web-extensions/brt-extension.mjs verify ./dist/my-extension-0.1.0
```
Upload the complete output directory in the developer console. It contains only public release material:
```text
bundle.zip
manifest.json
publisher-public-key.txt
signature.txt
submission.json
```
Never send or commit the private key.
## Project layout
```text
my-extension/
├── .brt/
│ ├── brt-extension.d.ts
│ ├── contract.json
│ └── manifest.schema.json
├── .vscode/settings.json
├── AGENTS.md
├── BRT_EXTENSION_CONTEXT.md
├── README.md
├── jsconfig.json
├── manifest.json
└── runtime/
├── index.html
├── main.js
└── styles.css
```
Only files inside `runtime/` are placed in `bundle.zip`. `manifest.json` stays beside the signed bundle so BRT can validate its exact bytes.
The scaffold includes a versioned contract snapshot, editor configuration, JavaScript type checking, and one self-contained LLM context file. Keep them in the project. Regenerate the scaffold or context when adopting a newer protocol contract. Do not edit the files under `.brt/` by hand.
## How an extension reaches a store
1. A developer creates and checks a project with this kit.
2. The publisher signs an immutable release and submits the five generated review files.
3. BRT verifies the publisher, permissions, code, signature, and user experience.
4. A BRT operator separately trusts the publisher, approves the release, and reviews any public marketplace listing. If a BRT operator submitted the release, a different operator must approve it.
5. The extension appears in the store user's extension catalog. Nothing runs until that user installs it and approves its requested capabilities.
There is no self-publishing path and no unsigned upload path. Passing automated validation does not approve or publish a release. Publishers can issue expiring, limited-use links for private packages or publisher-managed paid access. Opening a private link only shows its terms. The signed-in store must explicitly accept it before access is granted.
## Read next
- [Authoring guide](https://developers.staging.battlereadytech.com/extensions/v1/docs/authoring-guide): build, preview, and test an extension.
- [Manifest reference](https://developers.staging.battlereadytech.com/extensions/v1/docs/manifest-reference): declare identity, permissions, and visual controls.
- [Runtime API](https://developers.staging.battlereadytech.com/extensions/v1/docs/runtime-api): use the frozen `window.BRT` bridge.
- [Submission and review](https://developers.staging.battlereadytech.com/extensions/v1/docs/submission-and-review): protect keys, package a release, and submit it safely.
- [Troubleshooting](https://developers.staging.battlereadytech.com/extensions/v1/docs/troubleshooting): fix common validation and runtime failures.
- [Machine-readable contract](https://developers.staging.battlereadytech.com/extensions/v1/contract.json): exact limits, capabilities, actions, and file rules.
- [Manifest JSON Schema](https://developers.staging.battlereadytech.com/extensions/v1/manifest.schema.json): editor and CI validation.
- [TypeScript declarations](https://developers.staging.battlereadytech.com/extensions/v1/brt-extension.d.ts): editor completion for `window.BRT`.
- [LLM authoring guide](https://developers.staging.battlereadytech.com/extensions/v1/docs/llm-authoring): a deterministic workflow for coding agents.
- [Reference extension](https://developers.staging.battlereadytech.com/extensions/v1/download): a complete working example.
## One file for an LLM
Every new scaffold includes `BRT_EXTENSION_CONTEXT.md`. Give that file and the project directory to the developer's LLM. For an existing project or a context-only handoff, generate a fresh self-contained file:
```bash
node tools/web-extensions/brt-extension.mjs context \
--out ./brt-extension-context.md
```
Then ask the LLM to follow the instructions in that file. The generated context includes the contract, schema, API declarations, and reference extension.
An LLM with web access can read the current hosted source directly:
```text
https://developers.battlereadytech.com/llms.txt
https://developers.battlereadytech.com/llms-full.txt
https://developers.battlereadytech.com/extensions/v1/contract.json
https://developers.battlereadytech.com/extensions/v1/manifest.schema.json
```
## Contract authority
The production PHP validators are the final authority during BRT review. The developer CLI catches the same common failures before submission. CI tests keep the public capability list and reference extension aligned with the production validators.
Protocol and schema version 1 are closed contracts. Do not invent manifest fields, capabilities, actions, or message shapes. Request a platform change when the current contract cannot support the extension.
# Authoring guide
## Choose the right tool
Build an installable extension when the same signed feature will be installed on more than one BRT site or needs an approved BRT capability such as catalog access or cart updates.
Use a builder Code component for one site's isolated custom interaction. Use Embed code for a supported third-party embed. Neither is a substitute for an installable, versioned extension.
## Create a project
Run the scaffold command from the extracted BRT developer kit directory:
```bash
node tools/web-extensions/brt-extension.mjs init ./lane-status \
--namespace acme/lane-status \
--name "Lane status"
```
The publisher slug and package slug form the immutable namespace. Both use lowercase letters, numbers, and single hyphens. Choose them before the first release.
The scaffold includes `AGENTS.md`. Keep it in the project. Coding agents use it to avoid unsupported browser APIs and invalid package layouts.
## Build the interface
Put runtime files under `runtime/`.
- Keep exactly one HTML file. Its path must match `manifest.json` `entrypoint`.
- Put executable JavaScript in local `.js` or `.mjs` module files.
- Load JavaScript with ``.
- Put CSS and assets in the bundle. Remote scripts, styles, fonts, and images will not load.
- Attach event listeners from JavaScript. Inline handlers such as `onclick` are rejected.
- Use `textContent`, DOM construction, or carefully controlled templates for data returned by BRT. Treat every string as untrusted.
- Design for a responsive iframe. Do not assume a fixed width or height.
The runtime permits HTML, CSS, JavaScript modules, AVIF, GIF, JPEG, PNG, WebP, WOFF, and WOFF2 files. It blocks network connections, forms, nested frames, workers, plug-ins, popups, top navigation, application cookies, and parent DOM access.
## Read settings and call BRT
The platform injects a frozen `window.BRT` object before your entry module runs.
```js
const {settings, capabilities} = await window.BRT.ready;
if (capabilities.includes('catalog.products.read')) {
const result = await window.BRT.catalog.products({limit: 12});
console.log(result.products);
}
document.querySelector('h1').textContent = settings.heading || 'Products';
```
Every API call is checked twice: once in the frame and again by the storefront host. Declaring a capability does not grant it. A store user must approve it.
Handle rejected promises and unavailable capabilities. A user can revoke a permission after installation.
```js
try {
await window.BRT.cart.manage('add', {productId, quantity: 1});
} catch (error) {
status.textContent = error?.code === 'CAPABILITY_NOT_GRANTED'
? 'Cart access is not approved.'
: 'The item could not be added.';
}
```
## Add visual controls
Manifest properties become controls in the BRT builder. Use them for public presentation settings, not credentials or private data.
```json
{
"heading": {
"type": "text",
"label": "Heading",
"default": "Featured products"
},
"card_count": {
"type": "number",
"label": "Number of products",
"default": 6,
"min": 1,
"max": 12
}
}
```
Unknown settings are rejected. Existing installations keep their saved settings during upgrades, so preserve property names and compatible types whenever possible.
## Preview locally
```bash
node tools/web-extensions/brt-extension.mjs preview ./lane-status --port 4179
```
The local preview injects a mock `window.BRT` with sample catalog, cart, theme, booking, form, SMS, waiver, and analytics behavior. It is for interface development. It does not replace the production validator or an installed-site test.
Exercise important fallbacks without changing source code:
```bash
# Permission was not granted.
node tools/web-extensions/brt-extension.mjs preview ./lane-status \
--deny cart.manage
# A configured platform adapter failed.
node tools/web-extensions/brt-extension.mjs preview ./lane-status \
--fail catalog.products.read
# The catalog returned no products.
node tools/web-extensions/brt-extension.mjs preview ./lane-status \
--empty-catalog
```
Pass more than one capability to `--deny` or `--fail` as a comma-separated list.
## Check continuously
```bash
node tools/web-extensions/brt-extension.mjs check ./lane-status --json
```
Run the check in CI and before every package command. It validates the manifest, paths, sizes, entrypoint, local module references, disallowed HTML, capabilities, property defaults, and obvious resource references that the production CSP would block.
## Test for failure
At minimum, test these states:
1. A requested permission was not granted.
2. An API call failed or timed out.
3. A catalog or availability query returned no rows.
4. A setting is empty or uses its default.
5. The iframe is narrow.
6. The extension initializes more than once after a preview refresh.
7. Buttons are usable by keyboard and controls have accessible names.
Do not collect data just because a capability makes it technically possible. Request the fewest capabilities needed for the feature the user can see.
# Manifest reference
`manifest.json` is a UTF-8 JSON object. It is signed as exact bytes and cannot exceed 65,536 bytes. Reformatting it after signing invalidates the release.
Use [manifest.schema.json](https://developers.staging.battlereadytech.com/extensions/v1/manifest.schema.json) for editor completion and CI. The production validator remains authoritative where JSON Schema cannot express byte-length rules.
## Complete example
```json
{
"schema_version": 1,
"protocol_version": 1,
"namespace": "acme/featured-products",
"version": "1.0.0",
"name": "Featured products",
"description": "Show selected storefront products and add them to the cart.",
"entrypoint": "index.html",
"capabilities": [
"site.theme.read",
"catalog.products.read",
"cart.manage"
],
"properties": {
"heading": {
"type": "text",
"label": "Heading",
"default": "Featured products"
}
}
}
```
Schema version 1 rejects unknown top-level fields, including `$schema`. Configure your editor to associate [manifest.schema.json](https://developers.staging.battlereadytech.com/extensions/v1/manifest.schema.json) with files named `manifest.json` instead of adding a schema field to the release manifest. The scaffold already uses a valid release manifest.
## Required fields
| Field | Rule |
| --- | --- |
| `schema_version` | Integer `1`. |
| `protocol_version` | Integer `1`. |
| `namespace` | Exact `publisher-slug/package-slug`. Immutable after registration. |
| `version` | Semantic version such as `1.2.0` or `2.0.0-beta.1`. |
| `name` | 1 to 120 UTF-8 bytes. |
| `entrypoint` | Relative `.html` path inside `runtime/`. |
| `capabilities` | A list of up to 32 known capability keys. Use `[]` when none are needed. |
| `properties` | An object containing up to 64 visual controls. Use `{}` when none are needed. |
## Optional fields
| Field | Rule |
| --- | --- |
| `description` | Up to 500 UTF-8 bytes. Describe the visible outcome. |
Unknown fields are rejected.
## Property controls
Every property definition requires `type` and `label`. Supported types are `boolean`, `color`, `image`, `number`, `select`, `text`, `textarea`, and `url`.
| Field | Applies to | Rule |
| --- | --- | --- |
| `type` | All | One supported type. |
| `label` | All | 1 to 100 bytes. |
| `default` | All | Must match the property type. |
| `required` | All | Boolean. |
| `help` | All | Up to 300 bytes. |
| `options` | `select` | 1 to 100 unique `{label, value}` objects. |
| `min`, `max` | `number` | Numeric bounds. `min` cannot exceed `max`. |
Property names begin with a lowercase letter and contain only letters, numbers, and underscores. They can be at most 64 characters.
Runtime setting limits are 500 bytes for text and select values, 4,096 bytes for textarea values, and 2,048 bytes for image and URL values. Colors use six- or eight-digit hex notation. URL and image values may be empty, site-relative, `http`, or `https` URLs. Full URLs require a valid DNS name or IP address and cannot contain credentials. A required string property cannot have a blank default. All defaults together cannot exceed 16,384 JSON-encoded bytes.
Properties are public. Never put secrets, tokens, customer information, or private configuration in a default or setting.
## Capabilities
The closed capability list is documented in [Runtime API](https://developers.staging.battlereadytech.com/extensions/v1/docs/runtime-api) and represented exactly in [contract.json](https://developers.staging.battlereadytech.com/extensions/v1/contract.json). Declaring a capability only asks for access. New permissions are off until the store user approves them.
Adding a capability in a later release triggers another permission review. Removing a capability automatically removes the obsolete grant.
## Entrypoint and bundle
The ZIP contains exactly one HTML file, and it must be the declared entrypoint. Paths are relative, case-distinct only once, and limited to safe ASCII path segments. Symlinks are rejected.
The entrypoint cannot contain:
- inline JavaScript;
- inline event handlers;
- authored Content Security Policy headers;
- frames, objects, embeds, or applets;
- remote or missing module scripts.
The platform injects its own base URL, parent origin, and runtime module at launch.
# Runtime API
The platform injects one frozen global, `window.BRT`, before your entry module runs. It contains the approved bridge between the isolated extension frame and the storefront.
Use [brt-extension.d.ts](https://developers.staging.battlereadytech.com/extensions/v1/brt-extension.d.ts) for TypeScript and editor completion. The exact machine-readable operations are in [contract.json](https://developers.staging.battlereadytech.com/extensions/v1/contract.json).
## Initialization
Wait for `BRT.ready` before reading settings or calling an API.
```js
const {capabilities, settings} = await window.BRT.ready;
```
Initialization fails after 10 seconds when the host cannot establish a valid channel. Calls also time out after 10 seconds.
`BRT.capabilities()` returns the currently granted capability keys. `BRT.settings()` returns the public values configured in the builder. Both snapshots are deeply frozen.
A store user can decline or revoke a capability. Check availability, handle rejected promises, and leave the section useful when optional access is missing.
## Theme
Capability: `site.theme.read`
```js
const theme = await BRT.theme.read();
```
Returns strings for `primary`, `secondary`, `background`, `surface`, `text`, `mutedText`, `link`, `button`, `headingFont`, `bodyFont`, `sectionGap`, and `buttonRadius`.
Treat values as CSS values, not HTML. Set known style properties or custom properties directly.
## Catalog
Capability: `catalog.products.read`
```js
const result = await BRT.catalog.products({
search: 'optic',
page: 1,
limit: 12,
includeOutOfStock: false,
});
```
Use a limit of `12` or `24`. The result has this shape:
```js
{
products: [{
id: 123,
name: 'Product name',
url: '/product/example?pid=123',
image: '/storage/example.webp',
price: '$99.00',
inStock: true,
}],
total: 1,
page: 1,
lastPage: 1,
}
```
Product values are public storefront data. Still render strings with `textContent` or equivalent safe DOM APIs.
The `url` identifies the product's storefront route, but protocol version 1 does not provide a product-navigation action. Do not navigate the isolated frame or try to reach its parent. If product-detail navigation is required, report that platform gap during review.
## Cart
Capability: `cart.manage`
```js
await BRT.cart.manage('add', {productId: 123, quantity: 1});
await BRT.cart.manage('update', {productId: 123, quantity: 2});
await BRT.cart.manage('remove', {productId: 123});
```
Returns `{ok, itemCount, total}`. The host also dispatches its normal cart-updated event so the surrounding storefront can refresh.
## Booking
Capabilities: `booking.availability.read` and `booking.create`
```js
const availability = await BRT.booking.availability({
laneId: 4,
date: '2026-09-15',
durationMinutes: 60,
});
await BRT.booking.start({
laneId: 4,
startsAt: '2026-09-15T16:00:00-06:00',
endsAt: '2026-09-15T17:00:00-06:00',
});
```
Availability returns the public response from the site's range-availability endpoint. `booking.start` opens the BRT booking flow and returns `{opened, url}`.
## Waivers
Capability: `waivers.start`
```js
await BRT.waivers.start('range');
```
The accepted values are `range`, `rental`, and `class`. A missing value defaults to `range`. The call opens the BRT waiver flow and returns `{opened, url}`.
## Approved forms
Capability: `forms.submit`
Newsletter:
```js
await BRT.forms.submit('newsletter', {email: 'visitor@example.com'});
```
Contact:
```js
await BRT.forms.submit('contact', {
questionType: 9,
name: 'Visitor name',
email: 'visitor@example.com',
phone: '555-555-0100',
subject: 'Store question',
message: 'Question text',
});
```
Both return `{ok}`. BRT supplies its own anti-abuse state. Extensions never receive CSRF tokens or application sessions.
## Text messaging
Capability: `communications.sms.open`
```js
await BRT.communications.text('I have a question about this item.');
```
The call opens the visitor's messaging app with the store's configured number. It returns `{opened: true}`. It fails with `ADAPTER_UNAVAILABLE` when the site has no valid text-message link.
## Analytics
Capability: `analytics.events.write`
```js
await BRT.analytics.track('Select product', {
label: 'Featured products',
value: 99,
});
```
Returns `{tracked}`. BRT honors the site's tracking configuration and consent state. Do not encode customer information in event names, labels, or values.
## Raw requests
`BRT.request(capability, action, payload)` is available for forward-compatible low-level use. Prefer the named methods above. The host rejects unknown capabilities and actions even when a raw request is syntactically valid.
Messages must contain plain JSON-compatible data. Functions, DOM nodes, class instances, non-finite numbers, prototype keys, excessive nesting, and payloads above 65,536 bytes are rejected.
## Errors
Rejected calls use an `Error` with a stable `code` property. Handle at least:
| Code | Meaning |
| --- | --- |
| `CAPABILITY_NOT_GRANTED` | The store user did not grant or later revoked access. |
| `ADAPTER_UNAVAILABLE` | The site cannot provide the requested operation. |
| `INVALID_REQUEST` | The action or payload failed validation. |
| `INVALID_PAYLOAD` | The request contains a value that cannot cross the isolated bridge. |
| `INITIALIZATION_TIMEOUT` | The host did not initialize the extension within 10 seconds. |
| `REQUEST_TIMEOUT` | No response arrived within 10 seconds. |
| `TOO_MANY_REQUESTS` | The frame already has 64 pending calls. |
| `PAYLOAD_TOO_LARGE` | Serialized request data exceeds 65,536 bytes. |
| `PAYLOAD_TOO_COMPLEX` | Data exceeds depth, node, or collection limits. |
| `REQUEST_FAILED` | The host rejected the operation without a more specific public code. |
Show a useful local fallback. Do not display raw exception details to storefront visitors.
# Submission and review
BRT releases are immutable, signed snapshots. A site never runs mutable JavaScript from a publisher's server.
## Generate a publisher key once
```bash
node tools/web-extensions/brt-extension.mjs keygen \
--private-key ~/.config/brt/extensions/publisher-private.pem \
--public-key ./publisher-public-key.txt
```
The command creates an Ed25519 key pair. The private key file is written with owner-only permissions when the operating system supports them.
- Keep at least one protected active key per publisher identity.
- Store it in a secret manager or protected developer configuration directory.
- Back it up through an approved secure process.
- Never put it in the extension project, ZIP, submission directory, chat, ticket, or source control.
- Send BRT only the public-key file and its displayed SHA-256 fingerprint.
If a private key is lost or exposed, add a replacement public key in the developer console, move protected signing to the replacement, then revoke the old key. You cannot revoke the last active key. A revoked publisher cannot be restored.
## Package a release
Update `manifest.json` `version`, then run:
```bash
node tools/web-extensions/brt-extension.mjs package ./my-extension \
--out ./dist/my-extension-1.0.0 \
--private-key ~/.config/brt/extensions/publisher-private.pem
```
The command performs a clean validation, builds a deterministic ZIP, computes exact SHA-256 hashes, signs the BRT release envelope, and writes five review files.
Running the command twice against unchanged source and the same key produces the same manifest hash, artifact hash, and signature. This makes review and CI results reproducible.
Do not edit any output file. Repackage after every source or manifest change.
## Verify before sending
```bash
node tools/web-extensions/brt-extension.mjs verify ./dist/my-extension-1.0.0
```
The verification command reopens the ZIP, validates its contents, recomputes all hashes, checks the public-key fingerprint, and verifies the signature.
## Submit in the developer console
Create the publisher and package in the developer console, then upload the five generated files together:
```text
bundle.zip Signed runtime files
manifest.json Exact signed manifest bytes
publisher-public-key.txt Base64 Ed25519 public key
signature.txt Base64 release signature
submission.json Names, version, canonical path, hashes, and key fingerprint
```
The private key is never part of a submission.
The console runs the production validator immediately. A passing report joins the BRT review queue. A failing report stays with the submission and shows what to correct. You may correct, sign, and resubmit the same semantic version until BRT approves it. Once approved, that release version is immutable and can never be reused.
BRT keeps the submission record, hashes, validation report, and decision trail. Uploaded working files remain private while a passing submission awaits review. BRT deletes those working files after approval, withdrawal, rejection, a changes-requested decision, or automated validation failure. The approved bundle is copied to immutable release storage before the working files are deleted.
## What BRT reviews
BRT runs the production validators without publishing or installing the extension:
```bash
php artisan siteadmin:review-extension ./dist/my-extension-1.0.0
php artisan siteadmin:review-extension ./dist/my-extension-1.0.0 --json
```
This verifies the exact manifest, ZIP contract, hashes, Ed25519 signature, public-key fingerprint, requested capabilities, and visual property definitions. The command makes no database or storage changes.
Passing validation is necessary, not sufficient. BRT also reviews:
1. Publisher identity and key custody.
2. Whether each capability is necessary for the visible feature.
3. Safe rendering of BRT-returned and visitor-entered values.
4. Accessibility, responsive behavior, empty states, and error states.
5. Claims, labels, and navigation behavior.
6. Dependency provenance and license obligations.
7. Upgrade compatibility and property migration impact.
Trusting a publisher, approving a release, and publishing a marketplace listing are separate BRT operator decisions. An extension cannot publish itself. A BRT operator cannot approve a release they submitted. A verified release grants no site capability until a store user reviews and approves it during installation.
## Public, private, and paid distribution
- A public free extension can be installed from the marketplace after the store user reviews its requested capabilities.
- A private or unlisted extension is available only to a store that redeems a publisher-issued installation link.
- Paid and contact-pricing listings are discoverable in the marketplace, but BRT does not collect the publisher's extension fee. The publisher completes its own agreement and sends the store a private installation link for the approved release.
- A private link can be locked to one store ID, limited by use count, and given an expiration in UTC. BRT stores only a hash of the link token.
- Opening a private link is a read-only preview. The signed-in store must select **Accept private extension** to grant access. Reopening an already accepted link for the same store does not consume another use.
- Revoking a link stops future redemption. Revoking a release or publisher stops the code from launching.
## Publisher team invitations
Publisher owners add owners, administrators, and developers with a private, seven-day invitation link. Creating an invitation does not add the recipient to the team and does not reveal whether that email already has a BRT account. The signed-in recipient must use the matching email address and explicitly accept the invitation. Invite a second owner before removing an owner or deleting an owner’s BRT user account. BRT operators can assign the first owner to a trusted legacy publisher that predates the developer console, but they cannot replace an existing owner.
## Release updates
- Increase the semantic version for every release whose behavior or source has changed after an earlier version was approved.
- Keep the namespace unchanged.
- You may reuse a version only while correcting an unapproved, rejected, or withdrawn submission. Never reuse an approved version.
- Preserve property names and types when possible.
- Treat a newly requested capability as a product change that needs a clear explanation.
- Test the previous configuration against the new version.
BRT keeps the prior verified release for one-step rollback. Do not rely on rollback as a substitute for upgrade testing.
# LLM authoring guide
This workflow lets a coding agent build a valid BRT website extension without guessing at platform behavior.
## Hosted context
Give a web-capable coding agent `https://developers.battlereadytech.com/llms-full.txt`. It is generated from the same versioned Markdown, JSON schemas, contract, and TypeScript declarations shown in the developer portal.
If the agent cannot use the web, download the developer kit or attach `BRT_EXTENSION_CONTEXT.md` from a new scaffold.
## Give the agent authoritative context
New projects already contain `BRT_EXTENSION_CONTEXT.md`. For an existing project or when the contract has changed, generate a fresh context file from the downloaded developer kit:
```bash
node tools/web-extensions/brt-extension.mjs context \
--out ./brt-extension-context.md
```
Attach that file to the LLM conversation. Also give the agent the extension project directory and a short feature brief containing:
- the visible user outcome;
- the publisher and package namespace;
- the data the interface must show;
- the actions a visitor may take;
- required visual controls;
- accessibility and browser targets.
Do not provide publisher private keys to an LLM service. A human or protected CI job should perform release signing.
## Recommended prompt
```text
Build a BRT website extension from the attached BRT extension context.
Follow the manifest and runtime contracts exactly. Do not invent capabilities,
actions, fields, network access, or browser privileges. Request the fewest
capabilities required for the visible behavior. Keep all runtime dependencies
inside runtime/. Use safe DOM APIs for every external string. Include useful
loading, empty, denied-permission, and failure states. Make the interface
responsive and keyboard accessible.
Run the BRT extension check command and fix every error. Do not sign the
release and do not request or handle a private publisher key. Return the final
file tree, declared capabilities with a reason for each, tests performed, and
any platform limitation that prevented part of the request.
```
## Required agent process
1. Read `AGENTS.md` in the scaffolded project.
2. Read the manifest schema and runtime API before writing code.
3. Map each requested feature to a documented named API method.
4. Remove any feature that needs an undocumented capability. Report the gap.
5. Declare only the capabilities used by code.
6. Build with local HTML, CSS, JavaScript modules, images, and fonts.
7. Render untrusted strings with `textContent` or equivalent safe APIs.
8. Add loading, empty, missing-permission, unavailable-adapter, and generic failure states.
9. Run `check --json` until it exits successfully.
10. Run local preview at narrow and wide viewport sizes.
11. Leave signing to a human or protected CI process.
12. Package the finished project only after a human reviews the source and validation output.
## Hard constraints for agents
- Do not use `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, beacons, or remote imports.
- Do not use cookies, local storage, session storage, IndexedDB, service workers, workers, or application tokens.
- Do not read `window.parent`, `window.top`, `window.opener`, or the parent DOM.
- Do not use inline scripts, inline event handlers, `eval`, or generated executable code.
- Do not place JSON, Markdown, source maps, tests, or package-manager files in `runtime/`.
- Do not add secrets or customer information to the manifest or settings.
- Do not claim that a declared capability is granted.
- Do not change the namespace after the first release.
- Do not edit generated submission files.
## Agent completion report
Require the agent to return:
```text
Project path:
Namespace and version:
Visible behavior:
Capabilities requested:
- capability: reason it is necessary
Properties exposed:
Failure states implemented:
Accessibility checks:
BRT check command and exit status:
Manual preview sizes tested:
Known limitations:
Signing performed: no
```
Reject an agent result that omits command output, invents an API, requests a private key, or says a validation error can be ignored.
## Human approval boundary
An LLM can scaffold, implement, test, preview, and prepare a release. It must not receive the publisher private key. A developer or protected CI job signs the reviewed source, verifies the five release files, and uploads them. BRT validation and operator approval remain required after that upload.
# Troubleshooting
## The manifest has an unknown field
Schema version 1 is closed. Remove the field. This includes `$schema`, build metadata, icons, author records, and arbitrary configuration. Put build-only metadata outside the release manifest.
## The bundle contains unsupported files
Only HTML, CSS, JavaScript modules, supported web images, and WOFF fonts are accepted. Do not package source maps, TypeScript, JSON, Markdown, licenses, lockfiles, tests, or hidden operating-system files. Keep those in the project but outside `runtime/`.
## The bundle has more than one HTML file
An extension has exactly one entrypoint. Build additional views inside that document instead of packaging more HTML files.
## JavaScript must live in an external module
Move all executable code into `.js` or `.mjs` files and load it with a local module script:
```html
```
Remove inline scripts, inline event handlers, `javascript:` URLs, and dynamically generated executable strings.
## A resource works locally but not after installation
The production runtime blocks network connections and remote resources. Package scripts, styles, images, and fonts under `runtime/` and use relative paths. Replace direct API calls with an approved `window.BRT` method.
## `CAPABILITY_NOT_GRANTED`
The extension either did not declare the capability or the store user did not approve it. Do not loop or pressure the user. Keep the section useful without optional access and explain which feature is unavailable.
## `ADAPTER_UNAVAILABLE`
The current site does not have the related BRT feature configured. Show a neutral fallback. For example, hide an SMS action when the store has no text-message number.
## The signature does not verify
Package again from unchanged source. Common causes are editing `manifest.json`, `bundle.zip`, or `submission.json` after packaging; signing with a different key; or uploading files through a system that changes line endings.
The signature covers the exact SHA-256 hashes of `manifest.json` and `bundle.zip`. Even harmless reformatting changes the manifest hash.
## The publisher fingerprint changed
Stop. Confirm that the expected private key was used and that no key file was replaced. BRT must not silently accept a new key under an existing publisher identity.
## The iframe is blank
Check the browser console in local preview, then verify:
1. The entry module awaits `BRT.ready`.
2. Every imported module is packaged under `runtime/`.
3. Resource paths are relative and match case exactly.
4. Startup errors are caught and rendered inside the section.
5. The extension does not use blocked globals such as `fetch`, `XMLHttpRequest`, `WebSocket`, storage, workers, or parent DOM access.
## BRT review passes but product review fails
The automated review checks the release contract and signature. It cannot prove that the interface is useful, accessible, truthful, or appropriate for the requested permissions. Address the review notes, sign the corrected package, and resubmit it. You may keep the same version until BRT approves it. After approval, increase the version for every change.
# Machine-readable source: contract.json
{
"contract_version": 1,
"manifest": {
"schema_version": 1,
"protocol_version": 1,
"maximum_bytes": 65536,
"namespace_pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
"semantic_version_pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$",
"allowed_fields": [
"schema_version",
"protocol_version",
"namespace",
"version",
"name",
"description",
"entrypoint",
"capabilities",
"properties"
],
"maximum_capabilities": 32,
"maximum_properties": 64,
"property_types": [
"boolean",
"color",
"image",
"number",
"select",
"text",
"textarea",
"url"
],
"property_value_maximum_bytes": {
"boolean": null,
"color": 9,
"image": 2048,
"number": null,
"select": 500,
"text": 500,
"textarea": 4096,
"url": 2048
},
"maximum_settings_bytes": 16384
},
"artifact": {
"format": "zip",
"maximum_compressed_bytes": 5242880,
"maximum_expanded_bytes": 20971520,
"maximum_file_bytes": 5242880,
"maximum_files": 200,
"maximum_compression_ratio": 100,
"allowed_extensions": [
"avif",
"css",
"gif",
"html",
"jpeg",
"jpg",
"js",
"mjs",
"png",
"webp",
"woff",
"woff2"
],
"html_file_count": 1,
"javascript": "external-local-modules-only",
"remote_resources": false,
"symlinks": false,
"canonical_path": "extensions/{namespace}/{version}/bundle.zip"
},
"runtime": {
"global": "window.BRT",
"request_timeout_milliseconds": 10000,
"initialization_timeout_milliseconds": 10000,
"maximum_pending_requests": 64,
"maximum_message_bytes": 65536,
"maximum_payload_depth": 12,
"maximum_payload_nodes": 2000,
"maximum_collection_entries": 500,
"maximum_object_entries": 200,
"network_access": false,
"application_session_access": false,
"parent_dom_access": false,
"storage_access": false
},
"capabilities": {
"analytics.events.write": {
"risk": "low",
"api": "BRT.analytics.track(event, properties)",
"actions": ["track"],
"request": {
"event": "non-empty string, maximum 120 characters",
"properties.label": "optional string, maximum 160 characters",
"properties.value": "optional finite number"
},
"response": {"tracked": "boolean"}
},
"booking.availability.read": {
"risk": "low",
"api": "BRT.booking.availability(query)",
"actions": ["list"],
"request": {
"laneId": "positive integer",
"date": "YYYY-MM-DD",
"durationMinutes": "integer from 5 through 720"
},
"response": "public BRT range-availability response"
},
"booking.create": {
"risk": "medium",
"api": "BRT.booking.start(details)",
"actions": ["start"],
"request": {
"laneId": "optional positive integer",
"startsAt": "optional string, maximum 40 characters",
"endsAt": "optional string, maximum 40 characters"
},
"response": {"opened": true, "url": "string"}
},
"cart.manage": {
"risk": "medium",
"api": "BRT.cart.manage(action, details)",
"actions": ["add", "remove", "update"],
"request": {
"productId": "positive integer",
"quantity": "optional integer from 1 through 999; defaults to 1"
},
"response": {"ok": "boolean", "itemCount": "number", "total": "string"}
},
"catalog.products.read": {
"risk": "low",
"api": "BRT.catalog.products(query)",
"actions": ["list"],
"request": {
"search": "optional string, maximum 200 characters",
"page": "optional integer from 1 through 1000; defaults to 1",
"limit": "12 or 24; defaults to 12",
"includeOutOfStock": "optional boolean; defaults to true"
},
"response": {
"products": [{"id": "integer", "name": "string", "url": "string", "image": "string", "price": "string", "inStock": "boolean"}],
"total": "number",
"page": "number",
"lastPage": "number"
}
},
"communications.sms.open": {
"risk": "low",
"api": "BRT.communications.text(message)",
"actions": ["open"],
"request": {"message": "optional string, maximum 500 characters"},
"response": {"opened": true}
},
"forms.submit": {
"risk": "medium",
"api": "BRT.forms.submit(form, fields)",
"actions": ["submit"],
"request": {
"form": ["contact", "newsletter"],
"newsletter_fields": {"email": "string, maximum 255 characters"},
"contact_fields": {
"questionType": "optional integer from 1 through 9; defaults to 9",
"name": "string, maximum 255 characters",
"email": "string, maximum 255 characters",
"phone": "string, maximum 50 characters",
"subject": "string, maximum 255 characters",
"message": "string, maximum 5000 characters"
}
},
"response": {"ok": "boolean"}
},
"site.theme.read": {
"risk": "low",
"api": "BRT.theme.read()",
"actions": ["read"],
"request": {},
"response": {
"primary": "string",
"secondary": "string",
"background": "string",
"surface": "string",
"text": "string",
"mutedText": "string",
"link": "string",
"button": "string",
"headingFont": "string",
"bodyFont": "string",
"sectionGap": "string",
"buttonRadius": "string"
}
},
"waivers.start": {
"risk": "medium",
"api": "BRT.waivers.start(waiver)",
"actions": ["start"],
"request": {"waiver": ["range", "rental", "class", null]},
"response": {"opened": true, "url": "string"}
}
},
"submission": {
"format": "brt-extension-submission-v1",
"files": [
"bundle.zip",
"manifest.json",
"publisher-public-key.txt",
"signature.txt",
"submission.json"
],
"signature_algorithm": "Ed25519",
"hash_algorithm": "SHA-256",
"signing_payload_lines": [
"brt-extension-release-v1",
"namespace={namespace}",
"version={version}",
"manifest-sha256={sha256-of-exact-manifest-bytes}",
"artifact-sha256={sha256-of-exact-bundle-bytes}",
""
]
}
}
# Machine-readable source: manifest.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://developers.battlereadytech.com/extensions/v1/manifest.schema.json",
"title": "BRT website extension manifest",
"description": "Closed release manifest for BRT website extension schema version 1.",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"protocol_version",
"namespace",
"version",
"name",
"entrypoint",
"capabilities",
"properties"
],
"properties": {
"schema_version": {"const": 1},
"protocol_version": {"const": 1},
"namespace": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
},
"version": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 120,
"x-brt-minBytes": 1,
"x-brt-maxBytes": 120
},
"description": {
"type": "string",
"maxLength": 500,
"x-brt-maxBytes": 500
},
"entrypoint": {
"type": "string",
"minLength": 1,
"maxLength": 255,
"pattern": "^(?!/)(?!.*\\\\)(?!.*[?#])(?!(?:.*\\/)?\\.{1,2}(?:\\/|$))(?:[A-Za-z0-9._-]+/)*[A-Za-z0-9._-]+\\.[Hh][Tt][Mm][Ll]$",
"x-brt-maxBytes": 255
},
"capabilities": {
"type": "array",
"maxItems": 32,
"uniqueItems": true,
"items": {
"enum": [
"analytics.events.write",
"booking.availability.read",
"booking.create",
"cart.manage",
"catalog.products.read",
"communications.sms.open",
"forms.submit",
"site.theme.read",
"waivers.start"
]
}
},
"properties": {
"type": "object",
"maxProperties": 64,
"propertyNames": {"pattern": "^[a-z][A-Za-z0-9_]{0,63}$"},
"additionalProperties": {"$ref": "#/$defs/property"}
}
},
"$defs": {
"property": {
"type": "object",
"additionalProperties": false,
"required": ["type", "label"],
"properties": {
"type": {
"enum": ["boolean", "color", "image", "number", "select", "text", "textarea", "url"]
},
"label": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"x-brt-minBytes": 1,
"x-brt-maxBytes": 100
},
"default": {},
"required": {"type": "boolean"},
"help": {
"type": "string",
"maxLength": 300,
"x-brt-maxBytes": 300
},
"options": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["label", "value"],
"properties": {
"label": {"type": "string", "minLength": 1, "maxLength": 100},
"value": {"type": "string", "minLength": 1, "maxLength": 100}
}
}
},
"min": {"type": "number"},
"max": {"type": "number"}
},
"allOf": [
{
"if": {"properties": {"type": {"const": "boolean"}}, "required": ["type"]},
"then": {
"properties": {
"default": {"type": "boolean"},
"options": false,
"min": false,
"max": false
}
}
},
{
"if": {"properties": {"type": {"const": "number"}}, "required": ["type"]},
"then": {
"properties": {
"default": {"type": "number"},
"options": false
}
}
},
{
"if": {"properties": {"type": {"const": "select"}}, "required": ["type"]},
"then": {
"required": ["options"],
"properties": {
"default": {"type": "string", "maxLength": 500},
"min": false,
"max": false
}
}
},
{
"if": {
"properties": {"type": {"const": "color"}},
"required": ["type"]
},
"then": {
"properties": {
"default": {
"type": "string",
"maxLength": 9,
"pattern": "^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$"
},
"options": false,
"min": false,
"max": false
}
}
},
{
"if": {"properties": {"type": {"const": "text"}}, "required": ["type"]},
"then": {
"properties": {
"default": {"type": "string", "maxLength": 500},
"options": false,
"min": false,
"max": false
}
}
},
{
"if": {"properties": {"type": {"const": "textarea"}}, "required": ["type"]},
"then": {
"properties": {
"default": {"type": "string", "maxLength": 4096},
"options": false,
"min": false,
"max": false
}
}
},
{
"if": {
"properties": {"type": {"enum": ["image", "url"]}},
"required": ["type"]
},
"then": {
"properties": {
"default": {
"type": "string",
"maxLength": 2048,
"pattern": "^(?:|/(?!/)[^\\s\\\\]*|https?://(?:\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*)(?::[0-9]{1,5})?(?:[/?#][^\\s\\\\]*)?)$"
},
"options": false,
"min": false,
"max": false
}
}
},
{
"if": {"properties": {"required": {"const": true}}, "required": ["required"]},
"then": {"properties": {"default": {"pattern": "\\S"}}}
}
]
}
},
"x-brt-maxBytes": 65536
}
# Machine-readable source: submission.schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://developers.battlereadytech.com/extensions/v1/submission.schema.json",
"title": "BRT website extension submission",
"type": "object",
"additionalProperties": false,
"required": [
"format",
"namespace",
"version",
"artifact_path",
"manifest_sha256",
"artifact_sha256",
"publisher_public_key_id"
],
"properties": {
"format": {"const": "brt-extension-submission-v1"},
"namespace": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"
},
"version": {
"type": "string",
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$"
},
"artifact_path": {
"type": "string",
"pattern": "^extensions/[a-z][a-z0-9]*(?:-[a-z0-9]+)*/[a-z][a-z0-9]*(?:-[a-z0-9]+)*/[^/]+/bundle\\.zip$"
},
"manifest_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"artifact_sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"publisher_public_key_id": {"type": "string", "pattern": "^[a-f0-9]{64}$"}
}
}
# Machine-readable source: brt-extension.d.ts
export {};
declare global {
type BRTCapability =
| 'analytics.events.write'
| 'booking.availability.read'
| 'booking.create'
| 'cart.manage'
| 'catalog.products.read'
| 'communications.sms.open'
| 'forms.submit'
| 'site.theme.read'
| 'waivers.start';
type BRTSettingValue = boolean | number | string;
interface BRTReadyState {
readonly capabilities: readonly BRTCapability[];
readonly settings: Readonly>;
}
interface BRTTheme {
readonly primary: string;
readonly secondary: string;
readonly background: string;
readonly surface: string;
readonly text: string;
readonly mutedText: string;
readonly link: string;
readonly button: string;
readonly headingFont: string;
readonly bodyFont: string;
readonly sectionGap: string;
readonly buttonRadius: string;
}
interface BRTCatalogQuery {
readonly search?: string;
readonly page?: number;
readonly limit?: 12 | 24;
readonly includeOutOfStock?: boolean;
}
interface BRTCatalogProduct {
readonly id: number;
readonly name: string;
readonly url: string;
readonly image: string;
readonly price: string;
readonly inStock: boolean;
}
interface BRTCatalogResult {
readonly products: readonly BRTCatalogProduct[];
readonly total: number;
readonly page: number;
readonly lastPage: number;
}
interface BRTCartDetails {
readonly productId: number;
readonly quantity?: number;
}
interface BRTCartResult {
readonly ok: boolean;
readonly itemCount: number;
readonly total: string;
}
interface BRTAvailabilityQuery {
readonly laneId: number;
readonly date: string;
readonly durationMinutes: number;
}
interface BRTBookingDetails {
readonly laneId?: number;
readonly startsAt?: string;
readonly endsAt?: string;
}
interface BRTOpenResult {
readonly opened: true;
readonly url: string;
}
interface BRTNewsletterFields {
readonly email: string;
}
interface BRTContactFields {
readonly questionType?: number;
readonly name?: string;
readonly email?: string;
readonly phone?: string;
readonly subject?: string;
readonly message?: string;
}
interface BRTExtensionError extends Error {
readonly code: string;
}
interface BRTExtensionAPI {
readonly ready: Promise;
request(capability: BRTCapability, action: string, payload?: Record): Promise;
capabilities(): readonly BRTCapability[];
settings(): Readonly>;
readonly analytics: {
track(event: string, properties?: {readonly label?: string; readonly value?: number}): Promise<{readonly tracked: boolean}>;
};
readonly booking: {
availability(query: BRTAvailabilityQuery): Promise;
start(details?: BRTBookingDetails): Promise;
};
readonly cart: {
manage(action: 'add' | 'remove' | 'update', details: BRTCartDetails): Promise;
};
readonly catalog: {
products(query?: BRTCatalogQuery): Promise;
};
readonly communications: {
text(message?: string): Promise<{readonly opened: true}>;
};
readonly forms: {
submit(form: 'newsletter', fields: BRTNewsletterFields): Promise<{readonly ok: boolean}>;
submit(form: 'contact', fields: BRTContactFields): Promise<{readonly ok: boolean}>;
};
readonly theme: {
read(): Promise;
};
readonly waivers: {
start(waiver?: 'range' | 'rental' | 'class' | null): Promise;
};
}
interface Window {
readonly BRT: BRTExtensionAPI;
}
}