Kind Realm (renderable)
Name home
Namespace g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm
Exported functions 30
Module gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home
gno 0.9 home.gno gno
⧉
1 // Package home is the realm behind https://gno.land/u/clockwork. 2 // 3 // gnoweb builds a user profile page by calling Render("") on the realm at the 4 // exact path /r/<username>/home (gno.land/pkg/gnoweb/handler_http.go, 5 // GetUserView). That lookup does no version resolution, so this path is the 6 // interface with gnoweb and can never carry a /vN suffix. Everything that 7 Accept (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, pkgPath string)
Append (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, body string)
Authority () string
Delete (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string)
Each (fn func(string, gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home.Slot) bool)
Fallback (path string) string
Forget (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Freeze (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Frozen () bool
Get (slug string) string
Has (slug string) bool
HistoryPaths () []string
LastHeight () int64
Layout () string
Link (sub string) string
LivePath () string
Lookup (slug string) (struct{Body string; Rev int; Height int64}, bool)
Manifest () string
NewFiller () *gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home.Filler
Path () string
PendingPaths () []string
Register (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, t interface {Render func(string) string})
Render (path string) string
Revision () int
Rollback (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Set (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, body string)
Slugs () []string
Style (key string, fallback string) string
TransferAuthority (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string)
Withdraw (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, pkgPath string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
// might want to change therefore lives behind the path, in four layers:
8 //
9 // 1. CONTENT is data. The page is assembled from SLOTS: named markdown
10 // fragments in an avl tree, each written by its own Set call. Updating
11 // one paragraph is one small transaction.
12 // 2. LAYOUT is data. The slot named "layout" is the page template; every
13 // :slug: placeholder in it is filled from the slot of that name.
14 // 3. STYLE is data. Slots under the "style." prefix are knobs (colors,
15 // header mode) that the theme reads. Changing the accent color is a Set.
16 // 4. RENDERING is code, but replaceable. A THEME is a separate realm nested
17 // under this one that implements Theme and registers itself from its own
18 // init. The authority accepts it through p/clockwork/upgradeable and can
19 // roll it back. The slots never move: an upgrade replaces the code that
20 // reads them, not the data. When no theme is live the built-in renderer
21 // in render.gno serves the page, and the operator views (system, slots,
22 // edit, manifest) are always served by this realm, so a theme that
23 // panics can be rolled back from the page it cannot break.
24 //
25 // This realm holds state and forwards. It is deliberately small, because it
26 // is the one piece that can never be redeployed: it is importable by its
27 // themes, so it cannot be private, so AddPackage refuses a second deploy.
28 package home
29
30 import (
31 "crypto/sha256"
32 "encoding/hex"
33 "strconv"
34 "strings"
35
36 "chain/runtime"
37
38 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/upgradeable/v0"
39 "gno.land/p/nt/avl/v0"
40 )
41
42 // Admin is the initial authority: the only address that may write slots and
43 // accept, roll back or freeze a theme. It is a constant so a reviewer can read
44 // who controls the page without decoding chain state; TransferAuthority moves
45 // it afterwards.
46 //
47 // The value here is the standard test1 account, so the gnodev walkthrough in
48 // the README works unedited. `make build ADMIN=g1...` rewrites it for a real
49 // deployment.
50 const Admin = address( "g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm" )
51
52 const (
53 // LayoutSlug names the slot used as the page template.
54 LayoutSlug = "layout"
55
56 // StylePrefix marks slots that are style knobs rather than content.
57 // Style("accent", ...) reads the slot "style.accent".
58 StylePrefix = "style."
59
60 // maxSlugLen bounds a slot name so an index row stays readable.
61 maxSlugLen = 64
62 )
63
64 // Theme is what a renderer realm promises. It is declared here, in the file
65 // that can never change, so every theme version agrees on one type identity.
66 //
67 // Render receives the render path ("" is the profile page) and returns
68 // markdown. The operator views (system, slots, edit, manifest) never reach a
69 // theme; for any other path it does not draw itself, a theme should return
70 // Fallback(path). A panic here aborts the query: there is no recovering a
71 // foreign realm's panic, so a broken theme shows an error until Rollback.
72 type Theme interface {
73 Render(path string ) string
74 }
75
76 // Slot is one named fragment of the page.
77 type Slot struct {
78 Body string
79 Rev int // Revision() at the time of the last write to this slot
80 Height int64 // block height of that write
81 }
82
83 var (
84 proxy *upgradeable.Proxy
85 selfPath string
86
87 slots = avl.NewTree() // slug -> *Slot
88 rev int // total writes accepted; bumps on every mutation
89 lastHeight int64 // height of the most recent write
90 )
91
92 func init(cur realm) {
93 // Captured rather than written as a constant, so links and the nesting
94 // rule follow the path this code was actually deployed at.
95 selfPath = cur.PkgPath()
96 proxy = upgradeable.New(upgradeable.NewAddrAuthority(Admin))
97 }
98
99 // ---------------------------------------------------------------------------
100 // Authority
101
102 // assertAuthority panics unless the caller of the crossing function that owns
103 // cur is the current authority. It asks the proxy's Authority directly rather
104 // than going through proxy.AssertAuthorized, so content writes keep working
105 // after Freeze: freezing ends code upgrades, not editing.
106 func assertAuthority(cur realm) {
107 caller := cur.Previous()
108 if !proxy.Authority().Authorized(caller.Address(), caller.PkgPath()) {
109 panic( "home: restricted to the authority" )
110 }
111 }
112
113 // ---------------------------------------------------------------------------
114 // Slugs
115
116 // reservedSlugs are placeholders computed at render time (see render.gno). A
117 // slot may not take one of these names, so the page never depends on which
118 // of the two would win.
119 var reservedSlugs = [] string { "chainid" , "height" , "owner" , "realm" , "rev" , "slots" , "theme" , "updated" }
120
121 // validSlug reports whether slug is a legal slot name: 1..maxSlugLen bytes of
122 // [a-z0-9._-]. No ':' so a slug can never break out of its own :slug:
123 // placeholder, and no uppercase so a slot has exactly one name.
124 func validSlug(slug string ) bool {
125 if len(slug) == 0 || len(slug) > maxSlugLen {
126 return false
127 }
128 for i := 0 ; i < len(slug); i++ {
129 c := slug[i]
130 switch {
131 case c >= 'a' && c <= 'z' , c >= '0' && c <= '9' :
132 case c == '-' , c == '_' , c == '.' :
133 default :
134 return false
135 }
136 }
137 return true
138 }
139
140 func assertWritableSlug(slug string ) {
141 if !validSlug(slug) {
142 panic( "home: invalid slug " + strconv.Quote(slug) +
143 ": want 1-" + strconv.Itoa(maxSlugLen) + " bytes of [a-z0-9._-]" )
144 }
145 for _, r := range reservedSlugs {
146 if slug == r {
147 panic( "home: reserved slug " + slug + " is computed at render time" )
148 }
149 }
150 }
151
152 // ---------------------------------------------------------------------------
153 // Writes (authority only)
154
155 // Set creates or replaces the slot named slug. This is the ordinary update:
156 // one slot, one transaction. An empty body keeps the slot but empties it; use
157 // Delete to remove it.
158 func Set(cur realm, slug, body string ) {
159 assertAuthority(cur)
160 assertWritableSlug(slug)
161 write(slug, body)
162 }
163
164 // Append adds to the end of a slot, creating it when absent. It exists for a
165 // body too large for one transaction: Set the first chunk, Append the rest.
166 func Append(cur realm, slug, body string ) {
167 assertAuthority(cur)
168 assertWritableSlug(slug)
169 write(slug, Get(slug)+body)
170 }
171
172 // Delete removes a slot. Deleting the layout slot restores the theme's
173 // default layout.
174 func Delete(cur realm, slug string ) {
175 assertAuthority(cur)
176 if _, removed := slots.Remove(slug); !removed {
177 panic( "home: no such slot: " + slug)
178 }
179 bump()
180 }
181
182 func write(slug, body string ) {
183 bump()
184 slots.Set(slug, &Slot{Body: body, Rev: rev, Height: lastHeight})
185 }
186
187 func bump() {
188 rev++
189 lastHeight = runtime.ChainHeight()
190 }
191
192 // ---------------------------------------------------------------------------
193 // Reads (anyone, including themes)
194
195 // Get returns a slot body, or "" when the slot does not exist.
196 func Get(slug string ) string {
197 v := slots.Get(slug)
198 if v == nil {
199 return ""
200 }
201 return v.(*Slot).Body
202 }
203
204 // Has reports whether a slot exists, empty or not.
205 func Has(slug string ) bool { return slots.Has(slug) }
206
207 // Lookup returns a copy of a slot and whether it exists.
208 func Lookup(slug string ) (Slot, bool ) {
209 v := slots.Get(slug)
210 if v == nil {
211 return Slot{}, false
212 }
213 return *v.(*Slot), true
214 }
215
216 // Slugs returns every slot name, sorted.
217 func Slugs() [] string {
218 out := make([] string , 0 , slots.Size())
219 slots.Iterate( "" , "" , func (key string , _ any ) bool {
220 out = append(out, key)
221 return false
222 })
223 return out
224 }
225
226 // Each visits every slot in name order until fn returns true.
227 func Each(fn func (slug string , s Slot) bool ) {
228 slots.Iterate( "" , "" , func (key string , value any ) bool {
229 return fn(key, *value.(*Slot))
230 })
231 }
232
233 // Style returns the style knob named key (the slot "style."+key), or fallback
234 // when it is absent or empty.
235 func Style(key, fallback string ) string {
236 if v := Get(StylePrefix + key); v != "" {
237 return v
238 }
239 return fallback
240 }
241
242 // Layout returns the layout slot, or "" when none has been set. Themes
243 // substitute their own default in that case.
244 func Layout() string { return Get(LayoutSlug) }
245
246 // Revision returns the total number of writes this realm has accepted. It
247 // changes on every mutation, so a client can cheaply tell "nothing moved".
248 func Revision() int { return rev }
249
250 // LastHeight returns the block height of the most recent write, or 0.
251 func LastHeight() int64 { return lastHeight }
252
253 // Manifest returns one tab-separated line per slot:
254 //
255 // <slug>\t<rev>\t<bytes>\t<sha256 of body, hex>
256 //
257 // It is the diff surface scripts/sync.sh reads: one query tells it exactly
258 // which local file is out of date, without downloading any body.
259 func Manifest() string {
260 var b strings.Builder
261 slots.Iterate( "" , "" , func (key string , value any ) bool {
262 s := value.(*Slot)
263 sum := sha256.Sum256([] byte (s.Body))
264 b.WriteString(key)
265 b.WriteString( "\t" )
266 b.WriteString(strconv.Itoa(s.Rev))
267 b.WriteString( "\t" )
268 b.WriteString(strconv.Itoa(len(s.Body)))
269 b.WriteString( "\t" )
270 b.WriteString(hex.EncodeToString(sum[:]))
271 b.WriteString( "\n" )
272 return false
273 })
274 return b.String()
275 }
276
277 // Path returns this realm's package path, e.g. "gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home".
278 func Path() string { return selfPath }
279
280 // Link returns the gnoweb path of a render path on this realm:
281 // Link("") is "/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home", Link("slots") is "/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/home:slots".
282 func Link(sub string ) string {
283 p := selfPath
284 if i := strings.Index(p, "/" ); i >= 0 {
285 p = p[i:]
286 }
287 if sub == "" {
288 return p
289 }
290 return p + ":" + sub
291 }
292
293 // ---------------------------------------------------------------------------
294 // Themes: the upgradeable part.
295 //
296 // A theme realm nested under this path (…/home/theme/vN) calls Register from
297 // its own init, so deploying it is what nominates it. Nothing serves until the
298 // authority runs Accept with that path, which is a separate transaction naming
299 // code anyone can go and read. Rollback puts the previous theme back.
300
301 // Register records the calling realm's theme as a candidate. The path filed
302 // is read off the crossing frame, never taken as an argument, so a candidate
303 // cannot claim to live somewhere it does not. Only realms nested under this
304 // one are admitted; a direct user call is refused.
305 func Register(cur realm, t Theme) { proxy.Propose( 0 , cur, t) }
306
307 // Accept makes the candidate at pkgPath the live theme. Authority only.
308 func Accept(cur realm, pkgPath string ) { proxy.Accept( 0 , cur, pkgPath) }
309
310 // Withdraw drops a candidate. Authority only, except that a theme realm may
311 // always drop its own.
312 func Withdraw(cur realm, pkgPath string ) { proxy.Withdraw( 0 , cur, pkgPath) }
313
314 // Rollback restores the previous theme and returns the current one to the
315 // pending set. Authority only.
316 func Rollback(cur realm) { proxy.Rollback( 0 , cur) }
317
318 // Forget drops the rollback history and the storage it holds. Authority only.
319 func Forget(cur realm) { proxy.Forget( 0 , cur) }
320
321 // Freeze ends theme upgrades permanently. Slots stay editable. Authority
322 // only, and there is no way back.
323 func Freeze(cur realm) { proxy.Freeze( 0 , cur) }
324
325 // TransferAuthority hands both editing and upgrading to another address.
326 func TransferAuthority(cur realm, to address) {
327 proxy.TransferAuthority( 0 , cur, upgradeable.NewAddrAuthority(to))
328 }
329
330 // LivePath returns the realm path of the theme currently serving, or "".
331 func LivePath() string { return proxy.LivePath() }
332
333 // PendingPaths returns the candidate theme paths awaiting acceptance.
334 func PendingPaths() [] string {
335 out := [] string {}
336 for _, r := range proxy.Pending() {
337 out = append(out, r.PkgPath())
338 }
339 return out
340 }
341
342 // HistoryPaths returns the themes this realm has already served, oldest
343 // first, excluding the live one.
344 func HistoryPaths() [] string {
345 out := [] string {}
346 for _, r := range proxy.History() {
347 out = append(out, r.PkgPath())
348 }
349 return out
350 }
351
352 // Frozen reports whether theme upgrades have ended.
353 func Frozen() bool { return proxy.Frozen() }
354
355 // Authority returns a human-readable description of who may write and
356 // upgrade.
357 func Authority() string { return proxy.Authority().String() }
358
359 // liveTheme returns the accepted theme, if any.
360 func liveTheme() (Theme, bool ) {
361 v, ok := proxy.TryImpl()
362 if !ok {
363 return nil , false
364 }
365 t, ok := v.(Theme)
366 return t, ok
367 }
368
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.