lists-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev ebccaf0b13fe
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone ee59501e91bae6d2c23a8370
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
policy.json
{
  "constants": {
    "AGENT_ADDRESS": "GeQMXgfjSiK5owEBZ2GyS1NWQcHiFUZvBCLKSfn9qQDT",
    "AGENT_ADDRESS_BOUND": true
  },
  "auth": {
    "anonymous": true
  },
  "defs": {
    "member": "@user.id != null && get(/lists/$l/members/@user.id) != null",
    "memberAfter": "@user.id != null && getAfter(/lists/$l/members/@user.id) != null",
    "owner": "@user.id != null && get(/lists/$l/owners/@user.id) != null && get(/lists/$l/members/@user.id) != null",
    "creating": "get(/lists/$l) == null && getAfter(/lists/$l).createdBy == @user.id",
    "listOk": "@StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 180 && (@newData.kind == 'grocery' || @newData.kind == 'todo') && @StringUtils.length(@newData.order) <= 1000",
    "itemOk": "@StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 300 && @StringUtils.length(@newData.qty) <= 120 && @StringUtils.length(@newData.note) <= 900 && @StringUtils.length(@newData.section) <= 20"
  },
  "lists/$l": {
    "description": "A shared list. kind is grocery (items group by store section) or todo. order is the list's section order, a JSON list of section ids. Only members can read it. A list is deleted by an owner setting deleted, after which nobody can read or change its items.",
    "fields": {
      "name": "String",
      "kind": "String",
      "order": "String",
      "createdBy": "String",
      "createdAt": "UInt",
      "deleted": "Bool?"
    },
    "rules": {
      "read": "@def.member",
      "create": "@user.id != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @newData.deleted != true && @def.listOk && getAfter(/lists/$l/members/@user.id) != null && getAfter(/lists/$l/owners/@user.id) != null && getAfter(/listkeys/$l) != null && getAfter(/ownerkeys/$l) != null",
      "update": "@def.member && @data.deleted != true && @newData.createdBy == @data.createdBy && @newData.createdAt == @data.createdAt && @def.listOk && (@newData.deleted != true || @def.owner)",
      "delete": "false"
    }
  },
  "lists/$l/members/$u": {
    "description": "Someone who uses the list: a person's browser, or the assistant API. Joining needs the list key from the share link (a claim in the same write) or the owner key from the admin link, except for the person creating the list. Anyone can leave; an owner can remove anyone.",
    "fields": {
      "name": "String",
      "at": "UInt"
    },
    "rules": {
      "read": "@def.member",
      "create": "@user.id != null && @user.id == $u && @newData.at <= @time.now && @StringUtils.length(@newData.name) <= 120 && (@def.creating || (get(/lists/$l) != null && get(/lists/$l).deleted != true && ((get(/listkeys/$l) != null && getAfter(/lists/$l/claims/@user.id).key == get(/listkeys/$l).key) || (get(/ownerkeys/$l) != null && getAfter(/lists/$l/ownerclaims/@user.id).key == get(/ownerkeys/$l).key))))",
      "update": "@user.id != null && @user.id == $u && @StringUtils.length(@newData.name) <= 120",
      "delete": "@user.id != null && (@user.id == $u || @def.owner)"
    }
  },
  "lists/$l/claims/$u": {
    "description": "The list key a person last presented. The write only succeeds with the current key, so a successful write proves the caller holds it (the API checks every request this way). n is random so every claim is a real change.",
    "fields": {
      "key": "String",
      "n": "String"
    },
    "rules": {
      "read": "false",
      "create": "@user.id != null && @user.id == $u && get(/listkeys/$l) != null && @newData.key == get(/listkeys/$l).key && @StringUtils.length(@newData.n) <= 24",
      "update": "@user.id != null && @user.id == $u && get(/listkeys/$l) != null && @newData.key == get(/listkeys/$l).key && @StringUtils.length(@newData.n) <= 24",
      "delete": "false"
    }
  },
  "lists/$l/owners/$u": {
    "description": "A member who can remove people, change the share link and delete the list: the creator, and anyone who opened the admin link. Owner powers also need membership.",
    "fields": {
      "at": "UInt"
    },
    "rules": {
      "read": "@def.member",
      "create": "@user.id != null && @user.id == $u && @newData.at <= @time.now && getAfter(/lists/$l/members/$u) != null && (@def.creating || (get(/ownerkeys/$l) != null && getAfter(/lists/$l/ownerclaims/@user.id).key == get(/ownerkeys/$l).key))",
      "update": "false",
      "delete": "false"
    }
  },
  "lists/$l/ownerclaims/$u": {
    "description": "The owner key a person presented, read by the owners and members rules in the same write. Like a claim, it can only be written with the list's owner key, so nobody without the admin link can store anything here. Nobody can read it back.",
    "fields": {
      "key": "String",
      "n": "String"
    },
    "rules": {
      "read": "false",
      "create": "@user.id != null && @user.id == $u && get(/ownerkeys/$l) != null && @newData.key == get(/ownerkeys/$l).key && @StringUtils.length(@newData.n) <= 24",
      "update": "@user.id != null && @user.id == $u && get(/ownerkeys/$l) != null && @newData.key == get(/ownerkeys/$l).key && @StringUtils.length(@newData.n) <= 24",
      "delete": "false"
    }
  },
  "listkeys/$l": {
    "description": "The secret half of the list key, which the share link and assistants carry. Members read it to share the list. An owner can replace it, which turns off the old link and assistant key without removing anyone.",
    "fields": {
      "key": "String"
    },
    "rules": {
      "read": "@def.member",
      "create": "@user.id != null && @def.creating && @StringUtils.length(@newData.key) == 24",
      "update": "@def.owner && @StringUtils.length(@newData.key) == 24",
      "delete": "false"
    }
  },
  "ownerkeys/$l": {
    "description": "The owner key in the admin link, set once when the list is made. Nobody can read it; the owners rule compares against it.",
    "fields": {
      "key": "String"
    },
    "rules": {
      "read": "false",
      "create": "@user.id != null && @def.creating && @StringUtils.length(@newData.key) == 24",
      "update": "false",
      "delete": "false"
    }
  },
  "lists/$l/items/$i": {
    "description": "An item. The id comes from the item's name, so the same item added twice (or on two offline phones) is one item. section is a store section id. checkedAt (seconds) and addedAt (milliseconds) come from the device's clock and only order the list. Any member can add, change, check off or remove items. Length caps here count UTF-8 bytes, so they are three times the app's character limits: anything the app or the API accepts fits.",
    "fields": {
      "name": "String",
      "qty": "String",
      "note": "String",
      "section": "String",
      "checked": "Bool",
      "checkedAt": "UInt?",
      "addedAt": "UInt"
    },
    "rules": {
      "read": "@def.member && get(/lists/$l).deleted != true",
      "create": "@def.memberAfter && getAfter(/lists/$l).deleted != true && @def.itemOk",
      "update": "@def.member && get(/lists/$l).deleted != true && @def.itemOk",
      "delete": "@def.member"
    }
  },
  "lists/$l/known/$k": {
    "description": "Items this list has used, for suggestions and for remembering the section someone chose. The id is the item's id. uses counts how often it was added; at is when it was last added, by the device's clock.",
    "fields": {
      "name": "String",
      "section": "String",
      "uses": "UInt",
      "at": "UInt"
    },
    "rules": {
      "read": "@def.member && get(/lists/$l).deleted != true",
      "create": "@def.memberAfter && getAfter(/lists/$l).deleted != true && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 300 && @StringUtils.length(@newData.section) <= 20",
      "update": "@def.member && get(/lists/$l).deleted != true && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 300 && @StringUtils.length(@newData.section) <= 20",
      "delete": "@def.member"
    }
  },
  "functions": {
    "api": {
      "auth": "true",
      "entry": "functions/api.ts",
      "public": true,
      "methods": [
        "GET",
        "POST",
        "PATCH",
        "DELETE"
      ],
      "cors": "passthrough"
    }
  }
}

Change history

Point OpenApps links to openapps.free
ebccaf0b13 · 17 hours ago
Bind agent address in policy constants
519855687c · 19 hours ago
Use OpenApps service URLs and add contribution strip
2c353edfdc · 19 hours ago
Bounded source sync
58ac7e08ec · 1 day ago
Bounded source sync
150962f3d9 · 1 day ago
Bounded source sync
e52e4676b9 · 1 day ago