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}