code wiki / _hdl_build / nx_connect_album.nx
nx_connect_album.nx source
↩ module page · 183 lines · 9230 B
1// nx_connect_album.nx -- the family suite's sharing API as a VERB DISPATCHER over nx_suite_share, which is
2// itself a facade over nx_rebac. One dispatcher, no parsing, no permission logic of its own.
3//
4// WHY THIS SHAPE. The daemon already owns request parsing (cd_req_path, cs_form_get) and nx_rebac already
5// owns the permission rule. If this file re-did either, it would be the third time this lane produced a
6// second copy of something that already existed -- so it does neither. It takes ALREADY-PARSED arguments,
7// asks the one plane, and serialises the answer. That also makes it gate-able standalone: a gate can drive
8// every verb without standing up a socket, which is why the teeth below are cheap enough to be exhaustive.
9//
10// The daemon's job at the call site is two lines: parse, then call al_do. Auth is NOT optional here --
11// `actor` must already be a resolved OPAQUE account id, and 0 means signed-out, which every mutating verb
12// refuses. A route that trusts a caller-supplied identity is not auth-gated, it is decorated.
13// license_tier: ORIGINAL
14import "nx_suite_share.nx"
15
16const AL_MAXOUT: i64 = 64
17const AL_UNAUTH: i64 = 0
18
19func al_cat(dst: *u8, off: i64, s: *u8) -> i64 { return rb_cat(dst, off, s) }
20func al_num(dst: *u8, off: i64, v: i64) -> i64 { return rb_catn(dst, off, v) }
21
22// {"v":1,"ok":0,"err":"<why>"} -- structured, machine-readable, and it always names the reason. A refusal
23// a caller cannot distinguish from a failure is a refusal nobody can act on.
24func al_err(out: *u8, why: *u8) -> i64 {
25 var o: i64 = 0
26 o = al_cat(out, o, "{\x22v\x22:1,\x22ok\x22:0,\x22err\x22:\x22" as *u8)
27 o = al_cat(out, o, why)
28 o = al_cat(out, o, "\x22}" as *u8)
29 out[o] = 0 as u8
30 return o
31}
32func al_ok_n(out: *u8, field: *u8, v: i64) -> i64 {
33 var o: i64 = 0
34 o = al_cat(out, o, "{\x22v\x22:1,\x22ok\x22:1,\x22" as *u8)
35 o = al_cat(out, o, field)
36 o = al_cat(out, o, "\x22:" as *u8)
37 o = al_num(out, o, v)
38 o = al_cat(out, o, "}" as *u8)
39 out[o] = 0 as u8
40 return o
41}
42// map a plane error code onto a stable wire name, so the browser and an agent read the SAME vocabulary
43func al_reason(rc: i64, out: *u8) -> i64 {
44 if rc == SG_E_NOTOWNER { return al_err(out, "not_owner" as *u8) }
45 if rc == SG_E_UNCLAIMED { return al_err(out, "unclaimed" as *u8) }
46 if rc == SG_E_SELF { return al_err(out, "self" as *u8) }
47 if rc == SG_E_BADARG { return al_err(out, "bad_arg" as *u8) }
48 return al_err(out, "refused" as *u8)
49}
50
51// THE DISPATCHER. Returns bytes written into `out`; every path writes something, so a caller never has to
52// invent a response for a verb it did not recognise.
53// membership: which items are in this album. Containment is a RELATION, and nx_rebac is the relation
54// plane -- so an album's contents are tuples (album:N, contains, item:<id>), not a new store. The items
55// themselves already exist as CID records in the media vault; nothing here re-implements either.
56const AL_R_CONTAINS: *u8 = "contains\x00"
57
58// ABSENT-ITEM SENTINEL. nx_cc cannot represent an empty string literal (seq907: a zero-length literal
59// returns a pointer to the NEXT literal in the pool), so "no item supplied" is carried as a single "-"
60// rather than as "". Both are treated as absent so a caller cannot smuggle a blank membership row in.
61func al_no_item(item: *u8) -> i64 {
62 if item[0] == (0 as u8) { return 1 }
63 if item[0] == (45 as u8) { if item[1] == (0 as u8) { return 1 } }
64 return 0
65}
66func al_item_obj(item: *u8, out: *u8) -> i64 {
67 var o: i64 = 0
68 o = al_cat(out, o, "item:" as *u8)
69 o = al_cat(out, o, item)
70 out[o] = 0 as u8
71 return o
72}
73func al_do(prefix: *u8, verb: *u8, actor: i64, subject: i64, kind: *u8, id: i64, item: *u8, out: *u8, cap: i64) -> i64 {
74 // READ verbs are allowed for a signed-out visitor only insofar as the plane allows -- and the plane
75 // denies by default, so a signed-out actor (0) simply sees nothing rather than being special-cased.
76 if rb_seq(verb, "may" as *u8) == 1 {
77 return al_ok_n(out, "may" as *u8, sg_may(prefix, actor, kind, id, SG_VIEW))
78 }
79 if rb_seq(verb, "viewers" as *u8) == 1 {
80 let vw: *i64 = sys_mmap(8*AL_MAXOUT) as *i64
81 let n: i64 = sg_viewers(prefix, kind, id, vw, AL_MAXOUT)
82 var o: i64 = 0
83 o = al_cat(out, o, "{\x22v\x22:1,\x22ok\x22:1,\x22viewers\x22:[" as *u8)
84 var i: i64 = 0
85 while i < n {
86 if i > 0 { o = al_cat(out, o, "," as *u8) }
87 o = al_cat(out, o, "\x22" as *u8); o = al_cat(out, o, vw[i] as *u8); o = al_cat(out, o, "\x22" as *u8)
88 i = i + 1
89 }
90 o = al_cat(out, o, "]}" as *u8)
91 out[o] = 0 as u8
92 return o
93 }
94 // LIST THE CONTENTS. Gated by VIEW, and the refusal is deliberately IDENTICAL for "this album does not
95 // exist" and "this album is not shared with you" -- if those two answers differ, the API becomes an
96 // enumeration oracle and a stranger can map the family's albums by probing ids.
97 if rb_seq(verb, "items" as *u8) == 1 {
98 if sg_may(prefix, actor, kind, id, SG_VIEW) == 0 { return al_err(out, "not_permitted" as *u8) }
99 let alb: *u8 = sys_mmap(256)
100 sg_obj(kind, id, alb)
101 let it: *i64 = sys_mmap(8*AL_MAXOUT) as *i64
102 let n: i64 = rb_list_subjects(prefix, alb, AL_R_CONTAINS, it, AL_MAXOUT)
103 var o: i64 = 0
104 o = al_cat(out, o, "{\x22v\x22:1,\x22ok\x22:1,\x22items\x22:[" as *u8)
105 var i: i64 = 0
106 while i < n {
107 if i > 0 { o = al_cat(out, o, "," as *u8) }
108 o = al_cat(out, o, "\x22" as *u8); o = al_cat(out, o, it[i] as *u8); o = al_cat(out, o, "\x22" as *u8)
109 i = i + 1
110 }
111 o = al_cat(out, o, "]}" as *u8)
112 out[o] = 0 as u8
113 return o
114 }
115 if rb_seq(verb, "provenance" as *u8) == 1 {
116 let ch: *i64 = sys_mmap(8*AL_MAXOUT) as *i64
117 let n: i64 = sg_provenance(prefix, kind, id, ch, AL_MAXOUT)
118 var o: i64 = 0
119 o = al_cat(out, o, "{\x22v\x22:1,\x22ok\x22:1,\x22chain\x22:[" as *u8)
120 var i: i64 = 0
121 while i < n {
122 if i > 0 { o = al_cat(out, o, "," as *u8) }
123 o = al_cat(out, o, "\x22" as *u8); o = al_cat(out, o, ch[i] as *u8); o = al_cat(out, o, "\x22" as *u8)
124 i = i + 1
125 }
126 o = al_cat(out, o, "]}" as *u8)
127 out[o] = 0 as u8
128 return o
129 }
130
131 // ---- everything below MUTATES, and a signed-out caller may not ----
132 if actor == AL_UNAUTH { return al_err(out, "sign_in_required" as *u8) }
133
134 if rb_seq(verb, "claim" as *u8) == 1 {
135 let rc: i64 = sg_claim(prefix, actor, kind, id)
136 if rc < 0 { return al_reason(rc, out) }
137 return al_ok_n(out, "claimed" as *u8, 1)
138 }
139 if rb_seq(verb, "share" as *u8) == 1 {
140 let rc: i64 = sg_grant(prefix, actor, subject, kind, id, SG_R_VIEWER)
141 if rc < 0 { return al_reason(rc, out) }
142 return al_ok_n(out, "shared" as *u8, 1)
143 }
144 // curate = share WITH the right to re-share. Kept as its own verb rather than a flag, so granting
145 // someone the power to widen an album is always a deliberate word in the request, never a default.
146 if rb_seq(verb, "curate" as *u8) == 1 {
147 let rc: i64 = sg_grant(prefix, actor, subject, kind, id, SG_R_CURATOR)
148 if rc < 0 { return al_reason(rc, out) }
149 return al_ok_n(out, "curator" as *u8, 1)
150 }
151 if rb_seq(verb, "unshare" as *u8) == 1 {
152 let rc: i64 = sg_revoke(prefix, actor, subject, kind, id, SG_R_VIEWER)
153 if rc < 0 { return al_reason(rc, out) }
154 return al_ok_n(out, "unshared" as *u8, 1)
155 }
156 // PUT AN ITEM IN. Requires ADD (curator|owner) -- a viewer may look at an album and may never change
157 // what is in it. Additive: adding the same item twice is idempotent at the plane (latest-wins).
158 if rb_seq(verb, "add" as *u8) == 1 {
159 if al_no_item(item) == 1 { return al_err(out, "bad_arg" as *u8) }
160 if sg_may(prefix, actor, kind, id, SG_ADD) == 0 { return al_err(out, "not_permitted" as *u8) }
161 let alb: *u8 = sys_mmap(256); sg_obj(kind, id, alb)
162 let iob: *u8 = sys_mmap(256); al_item_obj(item, iob)
163 let asub: *u8 = sys_mmap(64); sg_sub(actor, asub)
164 rb_put(prefix, alb, AL_R_CONTAINS, iob, asub, 1)
165 return al_ok_n(out, "added" as *u8, 1)
166 }
167 // TAKE AN ITEM OUT: a tombstone, so the record of it having been there survives (rule 13).
168 if rb_seq(verb, "remove" as *u8) == 1 {
169 if al_no_item(item) == 1 { return al_err(out, "bad_arg" as *u8) }
170 if sg_may(prefix, actor, kind, id, SG_ADD) == 0 { return al_err(out, "not_permitted" as *u8) }
171 let alb: *u8 = sys_mmap(256); sg_obj(kind, id, alb)
172 let iob: *u8 = sys_mmap(256); al_item_obj(item, iob)
173 let asub: *u8 = sys_mmap(64); sg_sub(actor, asub)
174 rb_put(prefix, alb, AL_R_CONTAINS, iob, asub, 0)
175 return al_ok_n(out, "removed" as *u8, 1)
176 }
177 if rb_seq(verb, "gift" as *u8) == 1 {
178 let rc: i64 = sg_gift(prefix, actor, subject, kind, id)
179 if rc < 0 { return al_reason(rc, out) }
180 return al_ok_n(out, "gifted" as *u8, 1)
181 }
182 return al_err(out, "unknown_verb" as *u8)
183}