{"id":"kentiers/anvil","name":"anvil","scope":"kentiers","platform":"roblox","description":"Server-authoritative action, schema, and lifecycle foundation for Roblox","version":"0.4.0","latest":"0.4.0","versions":["0.1.0","0.1.1","0.1.2","0.1.3","0.2.0","0.3.0","0.4.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"7be9f8e68489190d883f15fab049a413000945f4b792644476820acfdcc80ea8","likes":0,"downloads":0,"install":"forest install kentiers/anvil","url":"https://forest.dev/p/roblox/kentiers/anvil","files":"https://api.forest.dev/ai/package/roblox/kentiers/anvil/files","readme":"# Anvil\n\n[![Verify](https://github.com/kentiers/anvil/actions/workflows/verify.yml/badge.svg)](https://github.com/kentiers/anvil/actions/workflows/verify.yml)\n[![Release](https://img.shields.io/github/v/release/kentiers/anvil?display_name=tag&label=release)](https://github.com/kentiers/anvil/releases)\n[![License](https://img.shields.io/github/license/kentiers/anvil)](LICENSE)\n[![Wally](https://img.shields.io/badge/Wally-kentiers%2Fanvil-ff6a8b)](https://wally.run/package/kentiers/anvil)\n\nServer-authoritative Luau foundation for Roblox actions, runtime schemas, resource scopes, and safe remote boundaries.\n\nAnvil gives server code one explicit path for untrusted input:\n\n```text\nremote payload\n  -> schema validation\n  -> rate limit\n  -> cooldown\n  -> authorization\n  -> domain execution\n  -> output validation\n  -> safe client response\n```\n\n## Why Anvil\n\n- **Trust boundaries are explicit.** Raw client values do not reach an action executor before its input schema passes.\n- **Failure is typed.** `Result` and stable error codes model expected gameplay failures without Promise allocation.\n- **Resource ownership is visible.** `Scope` owns connections, Instances, callbacks, and cancellable work; destruction is idempotent.\n- **Runtime cost is predictable.** Core has no required third-party runtime dependency, polling loop, or hidden remote creation.\n\n## Non-goals\n\nAnvil is not an anti-cheat system, DataStore wrapper, UI framework, networking replacement, or game-rule engine. Game code still owns prices, inventory, ownership, damage, permissions, and external-effect compensation.\n\n## Install\n\nAnvil is a server-realm Wally package. Pin an exact version:\n\n```toml\n[server-dependencies]\nAnvil = \"kentiers/anvil@0.4.0\"\n```\n```bash\nwally install\n```\n\nMap Wally's server dependencies into `ServerScriptService` with Rojo:\n\n```json\n{\n  \"name\": \"MyGame\",\n  \"tree\": {\n    \"$className\": \"DataModel\",\n    \"ServerScriptService\": {\n      \"ServerPackages\": { \"$path\": \"ServerPackages\" }\n    }\n  }\n}\n```\n\nNever map Anvil's `Action` or `Transport` modules into `ReplicatedStorage`.\n\n## Use case: server-authoritative purchase request\n\nThis example accepts only a bounded item identifier. Price, ownership, inventory mutation, and reward remain server decisions inside `execute`.\n\n```lua\n--!strict\n\nlocal ServerScriptService = game:GetService(\"ServerScriptService\")\nlocal AnvilModule = ServerScriptService.ServerPackages.Anvil\nlocal Anvil = require(AnvilModule)\nlocal RobloxRemote = require(AnvilModule.Transport.RobloxRemote)\n\nlocal purchase = Anvil.Action.new(\"Purchase\", {\n    input = Anvil.Schema.object({\n        ItemId = Anvil.Schema.string():minLength(1):maxLength(64),\n    }),\n    output = Anvil.Schema.object({ Accepted = Anvil.Schema.boolean() }),\n    cooldown = require(AnvilModule.Action.Cooldown).new(0.25, os.clock),\n    rateLimit = require(AnvilModule.Action.RateLimit).new(10, 1, os.clock),\n    authorize = function()\n        return Anvil.Result.ok(nil)\n    end,\n    execute = function(context)\n        local input = context.input :: { ItemId: string }\n\n        -- Read catalog, price, balance, and ownership from server-owned state.\n        if input.ItemId == \"\" then\n            return Anvil.Result.err(\"PURCHASE_NOT_ALLOWED\")\n        end\n        return Anvil.Result.ok({ Accepted = true })\n    end,\n})\n\nlocal remotes = ServerScriptService:WaitForChild(\"Remotes\")\nlocal purchaseRemote = remotes:WaitForChild(\"Purchase\") :: RemoteEvent\nRobloxRemote.new():bindEvent(purchaseRemote, purchase, {})\n```\n\nThe caller creates and owns `purchaseRemote`; Anvil does not create remotes implicitly.\n\n## Runtime schemas\n\nUse schemas at every untrusted boundary. Roblox datatypes are explicit, and Instance references require both class and ancestry constraints:\n\n```lua\nlocal target = Anvil.Schema.instance({\n    classNames = { \"BasePart\" },\n    ancestor = workspace:WaitForChild(\"BuildArea\"),\n})\n```\n\nAvailable Roblox validators: `Schema.vector3()`, `Schema.cframe()`, `Schema.color3()`, and `Schema.enumItem(expectedEnum?)`.\n\nPassing `Schema.instance` only proves reference shape and location. It does **not** prove player ownership, entitlement, placement validity, or permission.\n\n## Security and lifecycle\n\n- Input order is validation, rate limit, cooldown, authorization, execution, then output validation.\n- Unknown object fields, non-finite numbers, unsupported datatypes, and unconfigured Instances are rejected.\n- Client failures expose stable codes, not stack traces or server state.\n- Each request transport dispatch owns a `Scope` and destroys it after completion.\n- `Scope` lifecycle audit is opt-in, server-only, and has no default telemetry or polling.\n\nRead [Security model](docs/SECURITY.md) before binding production remotes. Read [Architecture](docs/ARCHITECTURE.md) for contracts, constraints, and cost model.\n\n## Roadmap\n\n| Phase | Focus | Status |\n| --- | --- | --- |\n| 0.1 | Core: Result, Schema, Scope, Action, transport | Released |\n| 0.2 | Reliability: lifecycle helpers, fakes, diagnostics | Released (`0.2.0`) |\n| 0.3 | Optional integration adapters | Released (`0.3.0`) |\n| 0.4 | Transactions and replay | Released (`0.4.0`) |\n\nFull scope and exit gates: [ROADMAP.md](docs/ROADMAP.md).\n\n## Verification\n\n```powershell\nwally install\npowershell -ExecutionPolicy Bypass -File scripts/test.ps1\npowershell -ExecutionPolicy Bypass -File scripts/test-consumer.ps1\n```\n\nThe consumer smoke test downloads exact public Wally package version, maps it server-only through Rojo, and exercises valid and invalid Action requests. TestEZ runs through local Roblox Studio; GitHub CI runs format, lint, and strict analysis.\n\n## Documentation\n\n- [Security model](docs/SECURITY.md)\n- [Architecture and API contracts](docs/ARCHITECTURE.md)\n- [Tooling and verification](docs/TOOLING.md)\n- [Optional adapter contracts](docs/ADAPTERS.md)\n- [Transactions and replay](docs/TRANSACTIONS.md)\n- [Changelog](CHANGELOG.md)\n- [0.2.0 migration notes](docs/MIGRATION-0.2.0.md)\n- [0.3.0 migration notes](docs/MIGRATION-0.3.0.md)\n- [0.4.0 migration notes](docs/MIGRATION-0.4.0.md)\n- [Release notes](https://github.com/kentiers/anvil/releases)\n\n## Contributing and support\n\nOpen a focused [issue](https://github.com/kentiers/anvil/issues) for bugs, design proposals, or documentation gaps. Report vulnerabilities privately; see [SECURITY.md](SECURITY.md).\n\n## Credits\n\nBuilt for Roblox with [Luau](https://luau.org/), [Wally](https://github.com/UpliftGames/wally), [Rojo](https://rojo.space/), and [TestEZ](https://github.com/Roblox/testez). Anvil is independent software; these projects are not bundled runtime dependencies.\n\n## License\n\n[MIT](LICENSE) © 2026 kentiers.\n","readmeTruncated":false}