xorm auto-maps CamelCase to snake_case by splitting on each
uppercase letter. MaintainerURL became maintainer_u_r_l instead
of maintainer_url, causing DB reads to return empty values.
Added explicit column name tags to all multi-word fields:
SupportURL, KeyPrefix, ExtensionName, DisplayName, ExtensionType,
MaintainerURL, InfoURL, TargetVersion, PHPMinimum, LicensingEnabled,
RequireKey, FeedVisibility, DownloadGating, StreamMode, CustomStreams.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add IsHidden field to Repository model. Three visibility modes:
- Public: visible to everyone (green label)
- Private: members only, non-members see 403 Access Denied (orange)
- Hidden: members only, non-members see 404 Not Found (red)
Private mode is for commercial repos — customers know the repo
exists and see a styled 403 page with sign-in button. Licensed
update feeds and key-gated downloads still work.
Hidden mode is for internal/secret repos — complete stealth, as
if the repo doesn't exist.
Settings UI: radio button selector in danger zone replaces the
old binary toggle. Each option shows a colored label with
description.
Migration v342: adds is_hidden column to repository table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#406: Add KeyPrefix field to UpdateStreamConfig. GenerateKeyString
now accepts a prefix parameter, looked up from org config. Default
remains MOKO if not set. Auto-uppercased, max 20 chars.
#408: Move "New Package" button into the packages header bar,
right-aligned. Uses details/summary pattern — clicking the button
expands the create form below. Cleaner layout on both repo and org.
#409: Add open-in-new-tab button (external link icon) next to every
copy button on feed URLs. All four feeds: Joomla XML, Dolibarr JSON,
WordPress JSON, Changelog XML.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AI migration 339: replaced with noopMigration placeholder
- feed/file.go: add missing comma in struct literal
- license_key.go: remove unused org_model import
These were being applied as server-side hotfixes on every deploy.
Now committed to dev so they persist through merges.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ParentOrgID field to User model for org hierarchy. Parent orgs
can have child orgs, enabling enterprise structures like
MokoConsulting → client orgs.
Model changes:
- ParentOrgID int64 on User (INDEX, DEFAULT 0)
- GetChildOrgs, GetAncestorOrgIDs, GetParentOrg helpers
- Max 10 hierarchy levels with cycle detection
License integration:
- ListLicensePackagesWithAncestors — shows packages from parent orgs
- ListLicenseKeysWithAncestors — shows keys from parent orgs
- SearchLicenseKeysWithAncestors — searches across hierarchy
- Master keys from parent orgs validate for child org repos
UI:
- Parent org dropdown in org settings (owners/admins only)
- Shows all orgs user owns except self
Migration v341: adds parent_org_id column to user table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add release_stream_map table for explicitly assigning releases to
update streams. When a mapping exists, it overrides automatic tag
detection. When absent, falls back to tag name/suffix matching.
New model: ReleaseStreamMap with SetReleaseStream, GetReleaseStream,
ResolveReleaseStream (manual first, auto fallback).
UI: stream selector dropdown on release create/edit page, shown when
licensing is enabled. Options: auto-detect (default) or any
configured stream (stable, release-candidate, beta, etc.).
All three feed generators (Joomla, Dolibarr, WordPress) now use
ResolveReleaseStream instead of MatchStreamFromTag.
Migration v340 updated with release_stream_map table creation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MatchStreamFromTag now checks if the tag name directly matches a
stream name (e.g. "stable", "release-candidate", "development")
before falling back to suffix matching. Supports both conventions:
1. Stream-name tags: tag IS the stream (MokoWaaS style)
2. Version tags: tag has version + suffix (v1.0.0-rc1 style)
When a stream-name tag is detected, the version number is extracted
from the release title instead of the tag. Falls back to tag name
if no version found in title.
Applied across all feed generators: Joomla XML, Dolibarr JSON,
WordPress JSON, and Changelog XML.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PaymentRef UNIQUE constraint causes Error 1062 when creating keys
without a payment reference — empty strings collide. Remove the
DB constraint; idempotency is enforced in code via
GetLicenseKeyByPaymentRef which already filters empty strings.
Also replace inline style with tw-max-w-lg class on search box.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SECURITY: Add verifyPackageOwnership/verifyKeyOwnership checks to
all API handlers that accept ID parameters. Prevents cross-org
access where an admin of org A could modify org B's license data.
FIX: RepoScope validation now properly parses JSON arrays using
json.Unmarshal instead of strings.Contains. The old approach matched
substrings (repo ID "2" matched inside "12"). Now uses typed int64
comparison.
FIX: Add {{$.CsrfTokenHtml}} to both delete confirmation modal
forms (package and key) in repo and org templates. Without CSRF
tokens, the form-fetch-action POST requests would be rejected.
FIX: HTML-escape release notes in WordPress changelog to prevent
XSS via malicious release note content reaching WP admin dashboards.
FIX: Parse AllowedChannels JSON format before comma-split fallback
to avoid garbage values from splitting JSON arrays by comma.
FIX: Add missing third return value (false) on error path in
validateUpdateKey to prevent compile error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add FeedVisibility field to UpdateStreamConfig with three modes:
- public: full feed with download URLs (default)
- no-download: version info visible but download URLs stripped
- hidden: empty feed returned without a valid license key
The "no-download" mode is the key commercial pattern — customers
see updates exist (motivating purchase/renewal) but cannot download
without a valid key. Joomla shows "update available" in admin.
Applied consistently across all update feed endpoints (Joomla XML,
Dolibarr JSON, WordPress JSON) via the shared validateUpdateKey()
which now returns a stripDownloads flag.
Also: when licensing is enabled, the release listing page requires
login. Anonymous users are redirected to the login page. This
prevents browsing release notes and download links without auth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SECURITY: ValidateLicenseKeyForRepo() now checks the package's
RepoScope field against the requesting repo ID. A package scoped
to repo A will reject keys when accessed from repo B's update feed.
Update server and download gating both use the new function.
Master/internal keys bypass repo scope checks.
RepoScope supports: "all" (any repo), single repo ID string,
or JSON array of repo IDs like ["1","5","12"].
Also adds POST /license-keys/{id}/revoke API endpoint that was
missing from the API but existed in web handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Domain lock timer: add DomainLockHours to LicensePackage and
FirstUsedUnix to LicenseKey. During the grace period after first
use, any domain is accepted and auto-added to the restriction list.
After the grace period, only listed domains are allowed. Set 0 for
immediate lock-on-first-use (default).
Fix infourl: default to /releases listing page instead of specific
tag page. Falls back to SupportURL or InfoURL if configured.
Match Akeeba Backup Pro XML format: downloadkey prefix is "dlid="
(not "&dlid="), matching how Joomla stores extra_query. Verified
against production Akeeba/JCE/AdminTools manifests via SSH.
Update migration v340 with FirstUsedUnix and DomainLockHours columns.
Add DomainLockHours field to create/edit package forms for both
repo and org levels with help text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migration v340: sync all missing columns (key_raw, payment_ref,
last_heartbeat_unix, is_archived, licensing_enabled, download_gating,
support_url, and all extension metadata fields).
Package archiving (#384): add IsArchived field with archive/unarchive
handlers and collapsible "Archived Packages" section in templates.
Existing keys from archived packages continue to work.
Expanded delete permissions (#385): org owners and site admins can
permanently delete packages and keys (previously site admin only).
Search (#392): server-side search across key_prefix, key_raw,
licensee_name, licensee_email, domain_restriction, and payment_ref
via ?q= query parameter on both repo and org licenses pages.
Sortable tables (#390): Fomantic UI sortable class on keys table
with new Domain column showing DomainRestriction per key.
Download gating (#347): three modes — none, prerelease-only, and
all downloads. CheckDownloadGating() intercepts both release
attachment and git archive download handlers.
Support URL (#393): configurable SupportURL field on
UpdateStreamConfig for wiki or external site links.
Changelog XML (#343): ServeChangelogXML endpoint at /changelog.xml
generates Joomla-compatible changelog from release notes. Parses
Keep-a-Changelog markdown sections into <security>, <fix>,
<addition>, <change>, <remove>, <note> XML elements.
API renew (#387): POST /license-keys/{id}/renew endpoint extends
key expiration by package duration.
Closes#384, #385, #386, #387, #389, #390, #392, #393
Refs #343, #346, #347
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove TODO from OAuth2 regenerate secret template — the
functionality was already fully implemented
- Remove stub TODO test comments for TestMerge, TestNewPullRequest,
TestAddTestPullRequestTask — stale for years
- Remove stale GetProjectsMode TODO — method is needed by templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In Go 1.23+, maps.Values returns iter.Seq not a slice.
Use slices.Collect() to materialize the iterator.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fix(ts): parseIssueHref now uses URL pathname and trims appSubUrl
for correct issue link parsing with sub-path deployments
- fix(actions): enforce MaxJobNumPerRun (256) limit when creating jobs,
rejecting workflows that exceed the GitHub-compatible limit
- chore: remove stale TODO comment on OAuth redirect route
Refs #325, #334
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add configurable fields: element name, display name, description,
extension type, maintainer, maintainer URL, info URL, target version,
PHP minimum
- Add platform dropdown: joomla, dolibarr, wordpress, prestashop,
drupal, composer, both
- Update Joomla XML generator to use metadata from config (falls back
to repo-derived values when not set)
- Add GetEffectiveConfig() for resolving repo → org → nil config chain
- Add locale keys for all new settings fields
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fix(templates): add required validation to issue dropdown fields
- refactor(ts): remove redundant `handled` field from MarkdownHandleIndentionResult
- refactor(go): rename HasOrgOrUserVisible to IsOwnerVisibleToDoer for clarity
Refs #311, #334
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full namespace migration: update the Go module path and all import
statements from git.mokoconsulting.tech to code.mokoconsulting.tech.
Also updates all URL references in templates, workflows, configs,
tests, and documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
License keys are domain-locked so hashing is unnecessary. Store the
full key in KeyRaw column for permanent visibility. Keys table now
shows the complete key with a clipboard copy button per row.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Block update feed endpoints based on repo platform setting:
Joomla-only repos return 404 on /updates/dolibarr.json and vice versa
- Show feed URLs section only when licensing is enabled
- Add delete button for license keys (site admin only)
- Add weekly cron job to purge expired keys older than 1 year
- Add DeleteLicenseKey and DeleteExpiredKeys model functions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admin-level teams (team.authorize >= Admin) now implicitly get admin
access to all unit types in UnitMaxAccess(), even without explicit
TeamUnit records. This resolves the long-standing TEAM-UNIT-PERMISSION
issue where adding new units (like TypeLicenses) left existing admin
teams without access.
Resolves: #304
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace confirm() with Gitea modal system (link-action + data-modal-confirm)
- Add confirmation modal to revoke key action
- Fix clipboard copy to use data-clipboard-target with tooltip feedback
- Localize all hardcoded English strings (feed labels, "unlimited", "Master")
- Improve key creation flash with security-focused message + copy button
- Add count badge to org licenses nav tab
- Add icon to org settings navbar for update streams
- Add help text to "Active" checkboxes explaining deactivation impact
- Fix empty state message to reference UI creation (not just API)
- Compact tables for denser license data display
- Add orange "Master" label to master package rows
- Conditional feed buttons on release page (only when licensing enabled)
- Add TypeLicenses unit type with Read/Write/Admin team permissions
- Route-level permission enforcement via RequireUnitReader/Writer
- Add "Renew" action for license keys (extends by package duration)
- Auto-associate domain on first heartbeat (lock-on-first-use)
- Enforce max_sites limit during domain auto-association
- Allow site admins and org owners to set custom license key values
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add key editing, domain enforcement, purchase webhooks, public
validation API, channels multiselect, Joomla downloadkey element,
licensing feature toggle, unified update system, release tag
enforcement, heartbeat tracking, and improved settings UX.
Phase 1: Full key display with AbsoluteShort dates, master package
protection (hide edit/delete in UI, reject in handlers).
Phase 2: Key edit page with template, handlers, and routes for both
repo and org levels. Master keys redirect away.
Phase 3: Domain restriction checking against CSV allowlist,
MaxSites enforcement via CountUniqueDomainsByKey and
IsDomainKnownForKey, dlid query param support for Joomla.
Phase 4: Purchase webhook (POST /license-keys/purchase) with
PaymentRef idempotency. Public validation endpoint
(POST /license-keys/validate) outside auth middleware.
PATCH /license-keys/{id} for API key editing.
Phase 5: Channels multiselect using org UpdateStreamConfig streams
rendered as checkboxes, stored as JSON arrays.
Additional: downloadkey XML element, LicensingEnabled toggle on
UpdateStreamConfig, Dolibarr endpoint unified with key validation,
release tag suffix enforcement, LastHeartbeatUnix field with
TouchHeartbeat, and cleaned-up settings pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an admin first visits the Licenses page, a master license package
and key are automatically created:
- Master package: lifetime, unlimited, all channels, all repos
- Master key: IsInternal=true, never expires
- Raw key shown once with copy instructions
- If master key is revoked, a new one is created on next visit
The master key is always present — revoking it and revisiting the page
generates a fresh one.
Ref #239
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a Licenses tab in the repo header that shows when license packages
exist for the repo's owner. The tab displays:
- License packages with name, duration, allowed channels, key count
- Issued keys with prefix, licensee, expiry, and status
Also includes:
- Org-level default update streams with per-repo override (#265)
- Full Joomla channel names in update feeds
- Update Feed button on releases page
- DB migration v336 for update_stream_config table
The Licenses tab appears after Packages in the repo header, gated by
whether any license packages exist for the owner.
Ref #239, #265
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add license key data model and Dolibarr update feed endpoint:
License key system:
- license_package table: subscription tiers with duration, max sites,
repo scope (org-wide or specific repos), and allowed update channels
- license_key table: individual keys with SHA-256 hashed storage,
domain restriction, custom start/end dates, internal/master key flag
- license_key_usage table: tracks update check activity per key
- DB migration v335 creates all three tables
Update server enhancements:
- Dolibarr JSON endpoint at /{owner}/{repo}/updates/dolibarr.json
- License key validation on update endpoints via ?key=MOKO-XXXX param
- Channel filtering: packages restrict which update streams keys access
- Invalid keys get empty XML response (Joomla-compatible "no updates")
- Usage tracking records domain, IP, user agent, version on each check
Key design decisions:
- Org-level master keys: IsInternal=true, package RepoScope="all"
- Keys stored as SHA-256 hashes, raw key only shown at creation
- Packages define allowed channels (e.g. ["stable","rc"] for Pro tier)
- MOKO-XXXX-XXXX-XXXX-XXXX format for license keys
Ref #239
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix three gaps that prevented per-unit public access from working on
private repositories:
1. Git HTTP handler (githttp.go): allow anonymous git pull for private
repos when the target unit (code or wiki) has AnonymousAccessMode
set to read. Previously only checked repo.IsPrivate.
2. Permission engine (repo_permission.go): call
finalProcessRepoUnitPermission for anonymous users on private repos
so that unit-level anonymous access modes are populated. Previously
returned early with AccessModeNone, skipping anonymous mode setup.
3. Search/explore (repo_list.go): include private repos that have at
least one unit with anonymous_access_mode > 0 in search results,
so anonymous users can discover repos with public sections.
The existing settings UI at /settings/public_access already allows
configuring per-unit visibility. The home page redirect to the first
readable unit (e.g. wiki) also already works via checkHomeCodeViewable.
Closes#238
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebrand the built-in actions bot user from upstream Gitea naming to
MokoGitea branding:
- Name: gitea-actions → mokogitea-actions
- FullName: Gitea Actions → MokoGitea Actions
- Email: teabot@gitea.io → mokogitea-actions[bot]@mokoconsulting.tech
Add backward-compatible name recognition so all three bot name variants
(mokogitea-actions, gitea-actions, github-actions) with optional [bot]
suffix resolve to the same system user.
Add WhitelistActionsUser, MergeWhitelistActionsUser, and
ForcePushAllowlistActionsUser toggles to branch protection rules,
allowing CI/CD workflows to push to protected branches when explicitly
enabled. Previously the actions bot (virtual user ID -2) could never be
added to whitelist because updateUserWhitelist() only validates real
database users.
Closes#233
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When multiple workflows are triggered by a single event (e.g. a
pull_request with several matching workflow files), each InsertRun
transaction acquires an X-lock on the repository row via
UpdateRepoRunsNumbers and an index lock on action_run. Two concurrent
transactions can deadlock when each holds one lock and waits for the
other. InnoDB kills the lighter transaction, but handleWorkflows only
logged the error and silently dropped the workflow run — making it
appear as though pull_request events were never fired.
This was the root cause of API-created PRs appearing to not trigger
Actions workflows: the notification pipeline was correct, but the DB
insert was lost to an unretried deadlock.
The fix wraps PrepareRunAndInsert in a retry loop (up to 3 attempts
with exponential backoff) that detects deadlock errors across MySQL,
PostgreSQL, and SQLite. On deadlock, the rolled-back run fields are
reset before the next attempt.
Also adds db.IsErrDeadlock() for cross-engine deadlock detection and
unit tests for the same.
Closes#220
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a Require2FA toggle to organization settings. When enabled,
org members without 2FA are redirected to the security settings
page with a warning flash message.
Changes:
- New Require2FA field on User model (migration v333)
- Org settings UI checkbox with shield-lock icon
- Check2FARequirement middleware on member-required org routes
- UpdateOptions extended with Require2FA field
Closes#208
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename the Go module path from code.gitea.io/gitea to
git.mokoconsulting.tech/MokoConsulting/MokoGitea across the entire
codebase.
Scope:
- go.mod module declaration
- 2,235 Go source files (import paths)
- Dockerfile WORKDIR and COPY paths
- Swagger API templates
- golangci.yml linter config
External dependencies (code.gitea.io/gitea-vet, code.gitea.io/sdk/gitea,
gitea.com/gitea/act, etc.) are intentionally NOT renamed — they are
separate upstream modules.
Closes#132
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
backport #37118
This PR closes remaining `public-only` token gaps in the API by making
the restriction apply consistently across repository, organization,
activity, notification, and authenticated `/api/v1/user/...` routes.
Previously, `public-only` tokens were still able to:
- receive private results from some list/search/self endpoints,
- access repository data through ID-based lookups,
- and reach several authenticated self routes that should remain
unavailable for public-only access.
This change treats `public-only` as a cross-cutting visibility boundary:
- list/search endpoints now filter private resources consistently,
- repository lookups enforce the same restriction even when addressed
indirectly,
- and self routes that inherently expose or mutate private account state
now reject `public-only` tokens.
---
Generated by a coding agent with Codex 5.2
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Backport #37706
This PR tightens several OAuth validation paths related to PKCE
handling, redirect URI normalization, and refresh-token replay safety.
What it changes:
- switch redirect URI comparison to ASCII-only normalization for
exact-match checks, avoiding Unicode case-folding surprises
- harden PKCE verification by:
- allowing PKCE omission only when no challenge data was stored
- rejecting exchanges with a missing verifier when PKCE was used
- rejecting malformed challenge state where a challenge exists without a
valid method
- comparing derived challenges with constant-time string matching
- make refresh-token invalidation counter updates conditional on the
previously observed counter value, so stale refresh state cannot be
accepted after the grant changes
Why:
These checks close gaps where:
- redirect URI comparisons could rely on broader Unicode normalization
than intended
- malformed or incomplete PKCE state could be treated too permissively
- concurrent or stale refresh-token use could advance the same grant
more than once
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Extend the existing /metrics endpoint with 3 new application metrics:
- gitea_active_users_30d: users active in last 30 days
- gitea_actions_queue_length: pending action jobs
- gitea_actions_running_jobs: currently running jobs
No new dependencies — extends existing collector and statistic model.
Closes#42
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix all 9 remaining files with CurrentRefSubURL field name
- Fix unused 'attempt' variable in action.go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add ConcurrencyGroup and ConcurrencyCancel fields to ActionRun
- Add GetConcurrentRunsAndJobs query function
- Fix PrepareToStartJobWithConcurrency 3-value return
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merges 356 commits from upstream Gitea v1.26.1 (bugfix release).
Resolved conflicts in templates by keeping our HelpURL changes,
all other conflicts resolved by taking upstream.
Closes#70
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Repositories with names starting with "." are now treated as system
repositories that are always private and cannot be made public. This is
enforced at every code path: API create, web create, migrate, template
create, push-to-create, API edit, web settings, and public access
settings. On creation paths, privacy is silently forced. On edit paths,
a clear error is returned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add organization-scoped branch protection rules that cascade to all
repos within the org. Repo-level rules take precedence; org rules
serve as the fallback when no repo rule matches a branch.
- New table: org_protected_branch (migration v332)
- OrgProtectedBranch model with full CRUD operations
- API endpoints: GET/POST/PATCH/DELETE /api/v1/orgs/{org}/branch_protections
- Inheritance via GetFirstMatchProtectedBranchRule() fallback
- InheritedFrom field added to BranchProtection API response
- Org rules use team-based whitelists (no per-user IDs at org level)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>