Skip to content

Latest commit

 

History

History
131 lines (112 loc) · 13.2 KB

File metadata and controls

131 lines (112 loc) · 13.2 KB

AGENTS.md

Operating mode

  • Act as a senior implementation partner
  • Default to implementation-first execution
  • Optimize for production correctness and smallest viable fix
  • Avoid speculative rewrites and preserve existing architecture unless explicitly requested

Shared Medusa Skills

  • This file is canonical for this repository; read it first.
  • Then read this repo's root SKILL.md.
  • When this repo is checked out inside a multi-plugin Medusa workspace, that workspace's shared skills and policy apply on top of this file. Standalone checkouts need nothing beyond this repository.

Repository Scope

  • This repository is the standalone source for @uhlhosting/medusa-notification-postal
  • Treat this repo as the canonical source for the postal plugin package
  • Keep changes scoped to the postal plugin unless the user explicitly asks for broader repo or ecosystem work
  • Do not import instructions, invariants, or paths from other Medusa repositories

Working Rules

  • Keep mutation logic in workflows, not routes
  • Keep route handlers thin and typed
  • Prefer AuthenticatedMedusaRequest for protected admin endpoints and enforce auth in src/api/middlewares.ts
  • Keep workflow composition in src/workflows/*.ts and import workflows statically from routes and handlers
  • Use Medusa SDK clients where applicable instead of raw fetch
  • Preserve the compiled .medusa/server bundle as the package publish surface
  • Do not add npm tokens, automation tokens, or .npmrc auth entries
  • Use glab for GitLab CI and variable management, and gh for GitHub mirror checks when needed

Postal Plugin Invariants

  1. Provider auth mode is smtp-api
  2. provider_data must carry email content and workflow metadata such as subject, html, text, workflow_event, and workflow_run_id
  3. The admin settings route under /admin/plugin-settings/postal is a configuration visibility surface and must not expose secrets
  4. Postal admin routes must require authenticated Medusa admin users through route-local middleware
  5. Postal debug or test sends must use the plugin workflow path so trace metadata is preserved
  6. Secrets (POSTAL_API_KEY, POSTAL_WEBHOOK_TOKEN) are sourced from provider options/environment at boot only — never persisted by the plugin and read-only in the admin UI. Non-secret settings (from, base_url, auth_type, test_to) persist in the postal_setting DML model via the module service; the plugin never writes to .env or mutates process.env on a request path. A boot loader reconciles the persisted row into process.env in memory. That loader must build the module service from its local container cradle, never by resolving the module key: Medusa passes a loader the module's local container and registers the service in the outer container only after every loader has run, so container.resolve(POSTAL_PLUGIN_MODULE) there always throws. It must also keep catching its own errors — an uncaught loader error makes Medusa register the module as undefined — and must interpolate the cause into the warning, because the logger drops extra arguments.
  7. Postal HTTP calls must fail fast with a bounded timeout, configurable via POSTAL_REQUEST_TIMEOUT_MS and clamped to 1–60s
  8. Postal webhook callbacks must use a tokenized store route, and the exact tokenized URL should be surfaced from an admin-only view rather than the settings surface
  9. Persistence goes through Medusa data primitives: the postal_setting and postal_webhook_events DML models + module service (no raw SQL, no PG-connection probing), with tables created by migrations (never on request paths)
  10. The admin webhook URL endpoint should return the tokenized path plus an absolute callback URL when the request origin can be resolved
  11. The provider must reject CR/LF characters in the sender address, subject, and recipients, and require an http/https base_url
  12. The public webhook route must validate its body and enforce a bounded body-size cap
  13. Admin message-inspection must delegate to the resolved provider service, not a duplicated Postal HTTP client
  14. The build must emit TypeScript declarations so every types/exports target advertised in package.json resolves for consumers
  15. Recording a Postal webhook is idempotent (a replayed message + event type must not duplicate a row) and emits a best-effort postal.<status> event on the event bus for subscribers
  16. Sends carry an idempotency_key derived from the workflow run id + template + recipient when a run id is present, so workflow retries do not duplicate emails
  17. Postal admin UI requests use the Medusa dashboard session through the shared JS SDK client; do not switch the plugin client to standalone JWT storage
  18. Provider-backed admin routes resolve the configured postal provider through Medusa's Notification module provider registry, and health must report unavailable when that provider cannot be resolved
  19. Notification creation uses Medusa's typed CreateNotificationDTO contract and selects the registered provider through the email channel; do not add the unsupported provider_id field to create payloads

Publish and CI Rules

  1. Versioning and releases are automated with semantic-release on the default branch, per GitLab's documented example (docs.gitlab.com/ci/examples/semantic-release/). Commits MUST follow Conventional Commits (fix: → patch, feat: → minor, feat!:/BREAKING CHANGE: → major) — the commit type drives the version bump; never hand-edit package.json version.

  2. The release:semantic job (stage deploy, default branch only) runs pnpm exec semantic-release, which computes the next version, publishes to the GitLab npm registry (authenticated with the ephemeral CI_JOB_TOKEN via a generated .npmrc), creates a GitLab Release with generated notes, and commits the bumped package.json + v* tag back to the default branch.

  3. semantic-release requires a masked project CI/CD variable GITLAB_TOKEN (scopes api + write_repository) allowed to push to the protected default branch and create protected v* tags. The npm publish uses CI_JOB_TOKEN, not a static npm token. The GitLab package registry does not support provenance, so the release job sets NPM_CONFIG_PROVENANCE=false.

  4. The semantic-release plugin chain and options live in .releaserc.json; keep it aligned with the plugins declared in devDependencies.

  5. Keep release validation in the repo: release:verify (pnpm release:check) must pass before release:semantic runs (needs).

  6. Onboarding/reconciliation: semantic-release derives the last release from git tags, so every published version must have a matching v<version> tag reachable from the default branch (e.g. the v0.1.17 baseline tag added when adopting semantic-release).

  7. GitHub npm publishing (public npmjs) uses OIDC Trusted Publishing and must verify protected refs and tag/version alignment before publishing. The npm-publish.yml job targets the npm-production environment, which requires a manual reviewer approval; an unapproved deployment expires after 30 days and the run is recorded as failed — no package is published. Approve or reject each publish run deliberately; never leave it pending.

  8. The GitLab mirror job (mirror:github) mirrors to GitHub, uses a masked/protected token, and pushes tag refs specifically (refs/tags/...) to avoid conflicts with GitLab's background mirroring. A project-level GitLab push mirror also syncs branches and tags to GitHub on its own schedule; because GitHub does not create workflow runs for a push that carries more than three tags, a mirrored tag must not be relied on to trigger the npmjs publish. After a release tag reaches GitHub, start the publish explicitly with gh workflow run npm-publish.yml --ref v<version> (the workflow's workflow_dispatch trigger applies the same tag/version and main-ancestry guards as a tag push), then approve the npm-production deployment.

  9. Security scanning uses the native Jobs/SAST.gitlab-ci.yml and Jobs/Secret-Detection.gitlab-ci.yml templates and runs on merge-request and default-branch pipelines (AST_ENABLE_MR_PIPELINES: "true"). The security findings merge-request widget is Premium/Ultimate-only and this instance is Community Edition (Free), so the security:report job surfaces findings in the job log and exposes the raw reports as a downloadable MR artifact (artifacts:expose_as) — tokenless and Free-tier-safe. SECURITY_FAIL_ON_FINDINGS=true turns it into a gate.

  10. pnpm's supply-chain verification (minimumReleaseAge, default 1440 minutes since v11) must stay enabled in every CI job, including the npm publish job — that job holds the OIDC trusted-publishing token and must never trust the lockfile on faith. Do not reach for --trust-lockfile to work around registry throttling. The one known throttling source is @medusajs/*, whose packuments carry thousands of preview/snapshot versions so npmjs rate-limits requests for them (medusajs/medusa#16294); that is handled by the narrow minimumReleaseAgeExclude entry in pnpm-workspace.yaml, which exempts only that scope while all other dependencies keep the quarantine. GitLab additionally caches pnpm's cache-dir (PNPM_CACHE_DIR, keyed on pnpm-lock.yaml) so the verification result is reused across jobs. Keep the pnpm version in .gitlab-ci.yml (PNPM_VERSION) and in npm-publish.yml (corepack prepare) in sync with the packageManager field in package.json. Never add npm auth to raise a rate limit — invariant: no npm tokens in this repo.

Validation Checklist

  1. pnpm release:check passes (includes admin typecheck via typecheck:admin)
  2. npm pack --dry-run includes the compiled .medusa/server bundle and the emitted .d.ts type targets
  3. GitHub Actions publish workflow runs without npm tokens
  4. GitLab CI validates, builds, and mirrors to GitHub on the allowed pipeline sources

Maintenance Rule

  • Update this file in the same commit whenever you change plugin architecture, auth behavior, route contracts, publish behavior, or CI/CD mirror behavior

Release assets

  • Use GitLab release assets as generic packages for distributable artifacts
  • Keep release assets aligned with the published package version and tag
  • Prefer Free-tier-safe release automation: avoid Ultimate-only security or release features unless explicitly requested

Secrets — never let a value reach the transcript

Anything printed is permanent. It lands in the agent transcript, the shell history and any CI log at the same instant, and there is no unprinting it. Treat an accidental print as a live incident requiring rotation, not a typo. It has happened twice on this platform: a Proxmox CSI token (2026-07-08, base64 -d to stdout) and GitLab's incoming email password (2026-07-16, a grep over a config that matched the value along with the key). The second one happened despite the rule being written down — because it was written somewhere the agent never read, and only covered writing secrets, not reading a file that contains one.

Redact in the same command that reads — never afterwards. Output is captured the moment it is emitted. When grepping anything that could hold a credential:

grep -nE "API_KEY|TOKEN|SECRET|PASSWORD" .env | sed -E "s/=.*/= <redacted>/"

Rules:

  • Never print a .env, and never cat one. List keys, not values: grep -oE '^[A-Z_]+' .env
  • Inject, never read. infisical run --env <env> -- <cmd> passes values to the child process without them crossing your terminal. Secrets for this platform live in Infisical (cerberus.uhl.cloud), not in the repo — see SECURITY.md in the Talos repo for the project layout and rotation procedure.
  • infisical secrets set ... >/dev/null — it echoes the value back in a confirmation table. The redirect is not optional.
  • Never kubectl get secret -o yaml, never | base64 -d. Keys only: kubectl -n <ns> get secret <name> -o jsonpath='{.data}' | jq 'keys'
  • Never print a GitLab CI variable's value (glab variable get, masked or not), and never echo one inside a job — a masked variable is masked in job logs, not in yours.
  • Pass secrets on stdin, never argvargv shows up in ps and shell history.
  • Never ask a human to paste a secret into chat. Their paste is transcript too. Hand them a command that reads from stdin instead.

If a value does escape: say so immediately, name exactly what leaked, and rotate it. A quiet fix leaves a live credential in a transcript nobody knows to purge.

Unified commit and release policy

  • Use Conventional Commits: type(scope): description. The scope is optional, lowercase, and may contain letters, numbers, dots, slashes, underscores, or hyphens.
  • fix and perf release a patch, feat releases a minor, and a ! or BREAKING CHANGE: footer releases a major.
  • chore(deps) releases a patch. This is the workspace-wide pattern for dependency updates that must reach the published package.
  • Other build, chore, ci, docs, refactor, style, and test commits do not release.
  • Do not use fix or feat only to force a release. The subject must describe the actual change.
  • Run pnpm commitlint before pushing. It validates the local commit range; the release gate validates the current pipeline commit from GitLab CI.