···6565 }
6666 // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type."
6767 switch s := def.Inner.(type) {
6868- case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription:
6868+ case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription, SchemaPermissionSet:
6969 if frag != "main" {
7070 return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag)
7171 }
+120
atproto/lexicon/language.go
···3636 return v.CheckSchema()
3737 case SchemaSubscription:
3838 return v.CheckSchema()
3939+ case SchemaPermissionSet:
4040+ return v.CheckSchema()
4141+ case SchemaPermission:
4242+ return v.CheckSchema()
3943 case SchemaNull:
4044 return v.CheckSchema()
4145 case SchemaBoolean:
···178182 return nil
179183 case "subscription":
180184 v := new(SchemaSubscription)
185185+ if err = json.Unmarshal(b, v); err != nil {
186186+ return err
187187+ }
188188+ s.Inner = *v
189189+ return nil
190190+ case "permission-set":
191191+ v := new(SchemaPermissionSet)
192192+ if err = json.Unmarshal(b, v); err != nil {
193193+ return err
194194+ }
195195+ s.Inner = *v
196196+ return nil
197197+ case "permission":
198198+ v := new(SchemaPermission)
181199 if err = json.Unmarshal(b, v); err != nil {
182200 return err
183201 }
···371389 return s.Parameters.CheckSchema()
372390}
373391392392+type SchemaPermissionSet struct {
393393+ Type string `json:"type"` // "permission-set"
394394+ Description *string `json:"description,omitempty"`
395395+ Permissions []SchemaPermission `json:"permissions"`
396396+}
397397+398398+func (s *SchemaPermissionSet) CheckSchema() error {
399399+ for _, p := range s.Permissions {
400400+ if err := p.CheckSchema(); err != nil {
401401+ return err
402402+ }
403403+ }
404404+ return nil
405405+}
406406+407407+type SchemaPermission struct {
408408+ Type string `json:"type"` // "permission"
409409+ Description *string `json:"description,omitempty"`
410410+411411+ Resource string `json:"resource"`
412412+ Accept []string `json:"accept,omitempty"`
413413+ Collection []string `json:"collection,omitempty"`
414414+ Action []string `json:"action,omitempty"`
415415+ LXM []string `json:"lxm,omitempty"`
416416+ Audience string `json:"aud,omitempty"`
417417+ InheritAud bool `json:"inheritAud,omitempty"`
418418+}
419419+420420+func (s *SchemaPermission) CheckSchema() error {
421421+ if s.Type != "permission" {
422422+ return fmt.Errorf("expected 'permission'")
423423+ }
424424+ switch s.Resource {
425425+ case "blob":
426426+ if len(s.Accept) == 0 {
427427+ return fmt.Errorf("blob permission requires 'accept'")
428428+ }
429429+ for _, acc := range s.Accept {
430430+ // TODO: more complete MIME pattern parsing
431431+ parts := strings.SplitN(acc, "/", 3)
432432+ if len(parts) != 2 || parts[0] == "*" || parts[0] == "" || parts[1] == "" {
433433+ return fmt.Errorf("invalid blob 'accept' pattern: %s", acc)
434434+ }
435435+ }
436436+ case "repo":
437437+ if len(s.Collection) == 0 {
438438+ return fmt.Errorf("repo permission requires 'collection'")
439439+ }
440440+ for _, coll := range s.Collection {
441441+ if coll == "*" {
442442+ continue
443443+ }
444444+ _, err := syntax.ParseNSID(coll)
445445+ if err != nil {
446446+ return fmt.Errorf("repo permission: %w", err)
447447+ }
448448+ }
449449+ for _, act := range s.Action {
450450+ if act != "create" && act != "update" && act != "delete" {
451451+ return fmt.Errorf("unsupported repo action: %s", act)
452452+ }
453453+ }
454454+ case "rpc":
455455+ if len(s.LXM) == 0 {
456456+ return fmt.Errorf("rpc permission requires 'lxm'")
457457+ }
458458+ for _, lxm := range s.LXM {
459459+ if lxm == "*" {
460460+ if s.Audience == "*" {
461461+ // TODO: is this necessary here?
462462+ return fmt.Errorf("can't have both 'lxm' and 'aud' be '*'")
463463+ }
464464+ continue
465465+ }
466466+ _, err := syntax.ParseNSID(lxm)
467467+ if err != nil {
468468+ return fmt.Errorf("rpc permission: %w", err)
469469+ }
470470+ }
471471+ if (s.InheritAud == true && s.Audience != "") || (s.InheritAud == false && s.Audience == "") {
472472+ return fmt.Errorf("rpc permission must have eith 'aud' or 'inheritAud' defined")
473473+ }
474474+ if s.Audience != "" {
475475+ // TODO: helper for service refs
476476+ parts := strings.SplitN(s.Audience, "#", 3)
477477+ if len(parts) != 2 || parts[1] == "" {
478478+ return fmt.Errorf("rpc 'aud' must be a service ref")
479479+ }
480480+ _, err := syntax.ParseDID(parts[0])
481481+ if err != nil {
482482+ return fmt.Errorf("rpc 'aud' must be a service ref: %w", err)
483483+ }
484484+ }
485485+ default:
486486+ return fmt.Errorf("unsupported permission resource: %s", s.Resource)
487487+ }
488488+ return nil
489489+}
490490+374491type SchemaBody struct {
375492 Description *string `json:"description,omitempty"`
376493 Encoding string `json:"encoding"` // required, mimetype
···803920}
804921805922func (s *SchemaParams) CheckSchema() error {
923923+ if s.Type != "params" {
924924+ return fmt.Errorf("expected 'params'")
925925+ }
806926 // TODO: check for set uniqueness of required
807927 for _, k := range s.Required {
808928 if _, ok := s.Properties[k]; !ok {