Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
742f4434b1 | ||
|
|
accb305fa3 |
43
AGENTS.md
43
AGENTS.md
@ -1,9 +1,44 @@
|
||||
# Core SDK
|
||||
|
||||
Go module `git.dev.alexdunmow.com/block/core`. Shared Go code between the **CMS and the orchestrator** — template engine, block registry, shared types, proto definitions.
|
||||
Go module `git.dev.alexdunmow.com/block/core`.
|
||||
|
||||
**Purpose: core exists ONLY for code that must be shared between first-party
|
||||
BlockNinja entities** — the **CMS**, the **orchestrator**, the **ninja CLI**,
|
||||
and any future entity that genuinely needs to share code with the others.
|
||||
|
||||
## Admission test
|
||||
|
||||
Before adding anything here, ask: **do two or more first-party entities need
|
||||
this code?**
|
||||
|
||||
- Needed by only one entity → it belongs in that entity's repo, not core.
|
||||
- Needed by plugins → it does NOT belong here (see below); plugins build
|
||||
against the wasm ABI, whose contract the CMS owns (`cms/backend/abi/`,
|
||||
`cms/docs/abi/`).
|
||||
|
||||
The standing direction (Captain, 2026-07-07) is to **shrink core**: prefer
|
||||
moving code out to its single consumer over adding more in. When a package's
|
||||
consumer count drops to one, migrate it out.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- **Core is NOT for plugins.** Its only consumers are the CMS and the orchestrator; it exists purely to share code between those two. Plugins must not import `block/core` — in the wasm-plugin era they are standalone artifacts built against the wasm ABI, not this module. (Core was previously the plugin SDK; that role is gone.)
|
||||
- **NEVER use `replace` directives in go.mod** — not in this repo, not in any consumer. All module resolution goes through the Gitea module proxy. If you need to test local changes, tag and push a version.
|
||||
- All consumers are in-house — no backwards compatibility shims needed. Just change the API and update consumers.
|
||||
- **Core is NOT for plugins.** Plugins are standalone wasm artifacts built
|
||||
against the wasm ABI, not this module. (Core was previously the plugin SDK;
|
||||
that role is gone, and remaining plugin-era surface is legacy to be worked
|
||||
off, not a precedent.)
|
||||
- **NEVER use `replace` directives in go.mod** — not in this repo, not in any
|
||||
consumer. All module resolution goes through the Gitea module proxy. If you
|
||||
need to test local changes, tag and push a version.
|
||||
- All consumers are in-house — no backwards compatibility shims needed. Just
|
||||
change the API and update consumers.
|
||||
|
||||
## Special case: `templates/bn/` is a synced copy, do not author here
|
||||
|
||||
The bn page chrome (head / toolbar / engagement / asset_hooks) is **authored
|
||||
in `cms/backend/templates/bn`** and mechanically copied here (`make
|
||||
sync-templates` in cms) solely so guest-side plugin templates can compile it
|
||||
into their wasm. check-safety check 31 fails cms commits while the copies
|
||||
drift. Never edit these files here directly — change them in cms and sync.
|
||||
`templates/bn/validation.go` is intentionally divergent (cms's version bridges
|
||||
the SDK ValidationTracker under both context keys) and is NOT part of the sync
|
||||
set. Spec: cms `docs/superpowers/specs/2026-07-07-bn-chrome-single-source-design.md`.
|
||||
|
||||
33
README.md
33
README.md
@ -1,8 +1,19 @@
|
||||
# BlockNinja Core
|
||||
|
||||
Shared Go code between the BlockNinja CMS and the orchestrator: types, interfaces, and utilities both need.
|
||||
Go code shared between first-party BlockNinja entities: the **CMS**, the
|
||||
**orchestrator**, the **ninja CLI**, and any future entity that genuinely
|
||||
needs to share code with the others.
|
||||
|
||||
> **Not a plugin SDK.** Core is purely for sharing code between the CMS and the orchestrator. Plugins must **not** import `block/core` — in the wasm-plugin era they are standalone artifacts built against the wasm ABI. (Core previously served as the plugin SDK; that role is gone.)
|
||||
> **Scope rule:** core is ONLY for code needed by **two or more** of those
|
||||
> entities. Code with a single consumer belongs in that consumer's repo. The
|
||||
> standing direction is to shrink core — when a package's consumer count drops
|
||||
> to one, it gets migrated out.
|
||||
|
||||
> **Not a plugin SDK.** Plugins must **not** treat `block/core` as their SDK —
|
||||
> in the wasm-plugin era they are standalone artifacts built against the wasm
|
||||
> ABI (owned by the CMS: `cms/backend/abi/`, `cms/docs/abi/`). Core previously
|
||||
> served as the plugin SDK; that role is gone, and remaining plugin-era
|
||||
> surface here is legacy to be worked off, not a precedent.
|
||||
|
||||
## Package Structure
|
||||
|
||||
@ -12,7 +23,7 @@ Shared Go code between the BlockNinja CMS and the orchestrator: types, interface
|
||||
| `blocks/` | BlockMeta, BlockFunc, BlockRegistry interface, BlockContext |
|
||||
| `blocks/builtin/` | Reusable block implementations (HTMLBlock) |
|
||||
| `templates/` | TemplateRegistry interface |
|
||||
| `templates/bn/` | Shared templ components (head, engagement, toolbar) |
|
||||
| `templates/bn/` | Synced copy of the cms-authored bn chrome (see below) |
|
||||
| `auth/` | Claims types, context extractors |
|
||||
| `content/` | Content access interface |
|
||||
| `settings/` | Settings access interface |
|
||||
@ -23,10 +34,20 @@ Shared Go code between the BlockNinja CMS and the orchestrator: types, interface
|
||||
| `ai/` | AI tool registry interface and types |
|
||||
| `rbac/` | Role type definition |
|
||||
|
||||
### `templates/bn/` is a synced copy — do not edit here
|
||||
|
||||
The bn page chrome (head / toolbar / engagement / asset_hooks) is authored in
|
||||
`cms/backend/templates/bn` and copied here via the cms `make sync-templates`
|
||||
target, solely so guest-side plugin templates can compile it into their wasm.
|
||||
check-safety (check 31) fails cms commits while the copies drift. Change the
|
||||
chrome in cms, sync, then commit + tag + push here.
|
||||
|
||||
## Usage
|
||||
|
||||
Consumers are the CMS and the orchestrator only.
|
||||
|
||||
```go
|
||||
import "git.dev.alexdunmow.com/block/core/plugin"
|
||||
import "git.dev.alexdunmow.com/block/core/blocks"
|
||||
```
|
||||
|
||||
Versioned via git tags through the Gitea module proxy — never `replace`
|
||||
directives. All consumers are in-house; no backwards-compatibility shims —
|
||||
change the API and update the consumers.
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -34,7 +35,7 @@ type RedeemResponse struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
secret []byte
|
||||
secret atomic.Pointer[[]byte]
|
||||
count, size, d int
|
||||
challengeTTL time.Duration
|
||||
tokenTTL time.Duration
|
||||
@ -59,7 +60,6 @@ func WithUsedTokenStore(ns NonceStore) Option { return func(sv *Server) { sv.use
|
||||
// 32-char salts, difficulty 4, 10-min challenge / 5-min token expiry.
|
||||
func New(secret []byte, opts ...Option) *Server {
|
||||
s := &Server{
|
||||
secret: secret,
|
||||
count: 50,
|
||||
size: 32,
|
||||
d: 4,
|
||||
@ -68,12 +68,20 @@ func New(secret []byte, opts ...Option) *Server {
|
||||
nonces: NewMemoryNonceStore(),
|
||||
usedTokens: NewMemoryNonceStore(),
|
||||
}
|
||||
s.secret.Store(&secret)
|
||||
for _, o := range opts {
|
||||
o(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) secretBytes() []byte { return *s.secret.Load() }
|
||||
|
||||
// SetSecret swaps the HMAC secret at runtime. Outstanding challenge and
|
||||
// verification tokens signed with the old secret immediately fail
|
||||
// verification, so callers can use this to invalidate all issued tokens.
|
||||
func (s *Server) SetSecret(secret []byte) { s.secret.Store(&secret) }
|
||||
|
||||
func (s *Server) CreateChallenge() (ChallengeResponse, error) {
|
||||
buf := make([]byte, 25)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
@ -81,7 +89,7 @@ func (s *Server) CreateChallenge() (ChallengeResponse, error) {
|
||||
}
|
||||
nonce := hex.EncodeToString(buf)
|
||||
expires := nowMs() + s.challengeTTL.Milliseconds()
|
||||
token := makeChallengeToken(s.secret, nonce, expires, s.count, s.size, s.d)
|
||||
token := makeChallengeToken(s.secretBytes(), nonce, expires, s.count, s.size, s.d)
|
||||
return ChallengeResponse{
|
||||
Challenge: Challenge{C: s.count, S: s.size, D: s.d},
|
||||
Token: token,
|
||||
@ -90,7 +98,7 @@ func (s *Server) CreateChallenge() (ChallengeResponse, error) {
|
||||
}
|
||||
|
||||
func (s *Server) Redeem(token string, solutions []string) RedeemResponse {
|
||||
claims := verifyChallengeToken(s.secret, token)
|
||||
claims := verifyChallengeToken(s.secretBytes(), token)
|
||||
if claims == nil {
|
||||
return RedeemResponse{Success: false}
|
||||
}
|
||||
@ -111,7 +119,7 @@ func (s *Server) Redeem(token string, solutions []string) RedeemResponse {
|
||||
}
|
||||
}
|
||||
expires := nowMs() + s.tokenTTL.Milliseconds()
|
||||
vt, err := makeVerificationToken(s.secret, expires)
|
||||
vt, err := makeVerificationToken(s.secretBytes(), expires)
|
||||
if err != nil {
|
||||
return RedeemResponse{Success: false}
|
||||
}
|
||||
@ -132,7 +140,7 @@ func (s *Server) Redeem(token string, solutions []string) RedeemResponse {
|
||||
// fragment that still carries the widget (with its reset flow) lets the visitor
|
||||
// re-solve and retry — callers MUST keep the widget present on rejection paths.
|
||||
func (s *Server) VerifyToken(token string) bool {
|
||||
claims := parseVerificationToken(s.secret, token)
|
||||
claims := parseVerificationToken(s.secretBytes(), token)
|
||||
if claims == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user