# MyronCMS Extension Authoring Guide

## 1. What an Extension Is

An extension is a local package that adds new `MyronAction` API verbs. Extensions do not modify existing core actions, public rendering, design tokens, snapshots, or core admin sections.

## 2. Directory Layout

Place extensions under `app/extensions/{vendor}/`. The vendor is a snake_case identifier such as `myron_sample`; a hyphenated directory name such as `myron-sample` is normalized for the reference extension. A typical package contains `manifest.json`, `bootstrap.php`, optional `actions/`, optional `admin/routes/`, optional `migrations/`, and its own `README.md`.

## 3. manifest.json Format

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `vendor` | string | yes | snake_case; must match the normalized extension directory name |
| `name` | string | yes | human-readable extension name |
| `version` | string | yes | extension version |
| `requiredScopes` | list<string> | yes | values from the six base scopes |
| `hasMigrations` | bool | yes | true when `migrations/*.php` should run |
| `adminRoutes` | list<object> | yes | each object has `key` and relative `path` |

## 4. Namespace Rules

Extension API URIs must start with `/api/v1/x/{vendor}/`. Action metadata domains must start with `x.{vendor}.`. `ActionRegistry::registerExtension()` rejects actions that violate those prefixes or declare empty input/output schemas.

## 5. Scope Model

The six base scopes are `design.read`, `design.write`, `content.read`, `content.write`, `administration.read`, and `administration.write`. Extensions declare required scopes in `manifest.json`; operators grant scopes from Extensions -> Register Extension. The loader skips unregistered, disabled, or under-granted extensions.

## 6. Writing an Action

Implement `MyronAction`, return complete `MyronActionMetadata`, and use `MyronExtensionSDK` for runtime context. Do not call raw core factories from extension code. `bootstrap.php` may return a list of `MyronAction` instances; the loader registers them through `registerExtension()`.

## 7. Admin Pages

Admin pages are optional. Put route files under the extension, list them in `manifest.json` as `{ "key": "sample-hello", "path": "admin/routes/hello.php" }`, and render with `myronRenderAdminShellStart()` using an `activeItem` that matches the route key.

## 8. Migrations

Set `hasMigrations` to true and return migrations from `migrations/*.php` as `version => list<string>|callable`. SQL migrations may only create, alter, drop, or index tables whose names begin with `{vendor}_`. Applied versions are recorded in `{vendor}_schema_migrations`.

## 9. Error Handling

The loader catches manifest, migration, and bootstrap failures. Core requests continue. Failures are logged to `extension_load_errors` and surfaced on Installed Extensions.

## 10. Terms of Engagement

The runtime enforces manifest parsing, vendor normalization, scope grants, enablement, action URI/domain namespace, non-empty action schemas, migration table-prefix isolation, load error capture, and extension admin route isolation.

## 11. What Extensions Cannot Do

Extensions cannot add PHP event hooks, install from a marketplace, add design token groups, hook public rendering, contribute snapshot sections, inject into core admin rail sections, rely on code signing or sandboxing, or communicate through dependency resolution. Extension v1 is local/manual install only.

## 12. Mic Element Types

Extensions may register Myron Insert Code element resolvers for richtext body fields. A mic token has the form `[mic-x.{vendor}.{slug}-mic_0123456789abcdef]`; core types use `[mic-{slug}-...]`. Implement `MicElementResolverInterface` with `resolve(string $id, array $context): ?string` and `describe(): array`. The interface is tagged `@apiVersion 1`; future evolution is by additive context keys, not new method parameters.

Return mic resolvers from `bootstrap.php` under `micElementTypes`:

```php
return [
    'actions' => [new MyExtensionAction($sdk)],
    'micElementTypes' => [
        'catalog' => new MyCatalogMicResolver($sdk),
    ],
];
```

The loader registers the resolver as `x.{vendor}.catalog`. Extension storage must stay under `{vendor}_*` tables, and the descriptor must declare any required scopes. The descriptor `bankSurface` controls Content Banks visibility:

- `editor`: full Content Banks authoring UI owns entries, as core Tables does.
- `picker`: Content Banks lists entries and links to `parentSurfaceUrl`; authoring lives elsewhere, as planned for Quizzes or LMS Lessons.
- `none`: discoverable from the toolbar only; no Content Banks tab.

Picker registrations must include `parentSurfaceUrl`. A pure toolbar resolver can use `none`. A sample descriptor:

```php
public function describe(): array
{
    return [
        'slug' => 'x.my_vendor.catalog',
        'label' => 'Catalog Item',
        'description' => 'Reusable catalog block.',
        'icon' => 'box',
        'scopes' => ['content.read'],
        'bankSurface' => 'picker',
        'parentSurfaceUrl' => $this->sdk->buildAdminUrl('x.my_vendor.catalog'),
    ];
}
```

See `app/extensions/myron-sample/` for the working reference extension.

## 13. Mail Transports

Extensions may register outbound mail transports for future provider-specific delivery needs. The v1 contract is `MailTransportInterface`:

```php
interface MailTransportInterface {
    /** @apiVersion 1 */
    public function send(MailEnvelope $envelope): MailDispatchResult;
    public function describe(): array;
}
```

`MailEnvelope` carries `to`, `subject`, `htmlBody`, `textBody`, `fromEmail`, `fromName`, optional `replyTo`, and an allowlisted `headers` array. `MailDispatchResult` returns `ok`, `transportId`, optional `providerMessageId`, and optional `error`.

Core registers `smtp` at boot. Extensions register under the fully-qualified namespace `x.{vendor}.{slug}` by returning transports from `bootstrap.php` in the same convention family as actions and mic types:

```php
return [
    'mailTransports' => [
        'resend' => new ResendMailTransport($sdk),
    ],
];
```

Transport descriptors should include `slug`, `label`, `description`, `scopes`, and `capabilities`. Common capabilities include `smtp`, `api`, `webhooks`, `tags`, `batch`, `html`, and `text`. Extension manifests must request the scopes needed for any admin configuration surface; a transport that only sends through already-saved extension settings should still describe the read/write scopes its setup actions require.

Illustrative outline:

```php
final class ResendMailTransport implements MailTransportInterface
{
    public function send(MailEnvelope $envelope): MailDispatchResult
    {
        // Build provider payload from the envelope and return provider id.
        return MailDispatchResult::ok('x.my_vendor.resend', 'provider-message-id');
    }

    public function describe(): array
    {
        return [
            'slug' => 'x.my_vendor.resend',
            'label' => 'Resend',
            'description' => 'API-based transactional mail transport.',
            'scopes' => ['administration.read'],
            'capabilities' => ['api', 'html', 'text', 'tags'],
        ];
    }
}
```

No provider transport ships in core v1 beyond SMTP; the substrate exists so provider extensions can be added without changing form notification code.
