feedback-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 4a4c54cd95c6
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 54b9c9d7fdc2b68029ce2e57
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
{
  "auth": {
    "anonymous": true
  },
  "defs": {
    "joined": "@user.id != null && get(/boards/$boardId/joins/@user.id) != null",
    "isOwner": "@user.id != null && get(/boards/$boardId/owners/@user.id) != null",
    "boardText": "@StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 80 && @StringUtils.length(@newData.intro) <= 1000 && @StringUtils.length(@newData.tags) >= 2 && @StringUtils.length(@newData.tags) <= 3000",
    "postText": "@StringUtils.length(@newData.title) >= 1 && @StringUtils.length(@newData.title) <= 140 && @StringUtils.length(@newData.details) <= 5000 && @StringUtils.length(@newData.tags) >= 2 && @StringUtils.length(@newData.tags) <= 400 && @StringUtils.length(@newData.name) <= 60",
    "postStatus": "(@newData.status == 'new' || @newData.status == 'review' || @newData.status == 'planned' || @newData.status == 'progress' || @newData.status == 'done' || @newData.status == 'closed')",
    "postSame": "@newData.title == @data.title && @newData.details == @data.details && @newData.tags == @data.tags && @newData.status == @data.status && @newData.statusAt == @data.statusAt && @newData.pinned == @data.pinned && @newData.hidden == @data.hidden && @newData.merged == @data.merged && @newData.name == @data.name",
    "entryText": "@StringUtils.length(@newData.title) >= 1 && @StringUtils.length(@newData.title) <= 140 && @StringUtils.length(@newData.body) <= 10000 && @StringUtils.length(@newData.posts) >= 2 && @StringUtils.length(@newData.posts) <= 1500 && @newData.at <= @time.now"
  },
  "boards/$boardId": {
    "description": "A feedback board: its name, intro and tags (a JSON list of {id, name}). Readable by anyone who opened its link (a join below); the random board id is the link's secret. Only owners can change it.",
    "fields": {
      "name": "String",
      "intro": "String",
      "tags": "String",
      "createdBy": "String!",
      "createdAt": "UInt!"
    },
    "rules": {
      "read": "@def.joined",
      "create": "@user.id != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @def.boardText && getAfter(/boards/$boardId/joins/@user.id) != null && getAfter(/boards/$boardId/owners/@user.id) != null && getAfter(/boardkeys/$boardId) != null",
      "update": "@def.isOwner && @def.boardText",
      "delete": "false"
    }
  },
  "boards/$boardId/joins/$userId": {
    "description": "Someone who opened the board's link on a device. Knowing the board id is what lets you join, and joining is what lets you read the board.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && @user.id == $userId",
      "create": "@user.id != null && @user.id == $userId && @newData.at <= @time.now && getAfter(/boards/$boardId) != null",
      "update": "false",
      "delete": "false"
    }
  },
  "boards/$boardId/owners/$userId": {
    "description": "Who manages the board: the creator, in the same batch that creates it, or anyone who presents the admin key. The key must exist, so a board without one cannot be claimed.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@def.joined",
      "create": "@user.id != null && @user.id == $userId && @newData.at <= @time.now && ((get(/boards/$boardId) == null && getAfter(/boards/$boardId).createdBy == @user.id) || (get(/boardkeys/$boardId) != null && getAfter(/boardclaims/$boardId/c/@user.id).key == get(/boardkeys/$boardId).key))",
      "update": "false",
      "delete": "false"
    }
  },
  "boardkeys/$boardId": {
    "description": "The board's admin key. Only the public API can read it, to check a key someone presents.",
    "fields": {
      "key": "String!"
    },
    "rules": {
      "read": "@user.id == @const.BOUNDED_PUBLIC_PRINCIPAL_API",
      "create": "@user.id != null && get(/boards/$boardId) == null && getAfter(/boards/$boardId).createdBy == @user.id && @StringUtils.length(@newData.key) == 24",
      "update": "false",
      "delete": "false"
    }
  },
  "boardclaims/$boardId/c/$userId": {
    "description": "An admin key someone presented to become an owner. Never readable.",
    "fields": {
      "key": "String"
    },
    "rules": {
      "read": "false",
      "create": "@user.id != null && @user.id == $userId && @StringUtils.length(@newData.key) == 24",
      "update": "@user.id != null && @user.id == $userId && @StringUtils.length(@newData.key) == 24",
      "delete": "false"
    }
  },
  "boards/$boardId/posts/$postId": {
    "description": "An idea or bug report. tags is a JSON list of tag ids. status is new, review, planned, progress, done or closed. votes goes up or down by exactly one with each person's vote record, and comments by one with each new comment (lastComment names it). Hidden posts are seen only by their author and the owners. merged names the post this one was merged into.",
    "fields": {
      "title": "String",
      "details": "String",
      "tags": "String",
      "status": "String",
      "statusAt": "UInt",
      "votes": "UInt",
      "comments": "UInt",
      "lastComment": "String?",
      "pinned": "Bool",
      "hidden": "Bool",
      "merged": "String?",
      "name": "String",
      "by": "String!",
      "createdAt": "UInt!"
    },
    "rules": {
      "read": "@def.joined && (@data.hidden == false || @data.by == @user.id || get(/boards/$boardId/owners/@user.id) != null)",
      "create": "@def.joined && @newData.by == @user.id && @newData.createdAt <= @time.now && @newData.createdAt >= @time.now - 300 && @newData.statusAt <= @time.now && @def.postText && @def.postStatus && (@newData.status == 'new' || get(/boards/$boardId/owners/@user.id) != null) && @newData.votes == 1 && @newData.comments == 0 && @newData.pinned == false && @newData.hidden == false && @newData.merged == null && @newData.lastComment == null && getAfter(/boards/$boardId/posts/$postId/votes/@user.id) != null",
      "update": "(@def.isOwner && @def.postText && @def.postStatus && @newData.statusAt <= @time.now) || (@def.joined && @def.postSame && @newData.comments == @data.comments && @newData.lastComment == @data.lastComment && ((@newData.votes == @data.votes + 1 && get(/boards/$boardId/posts/$postId/votes/@user.id) == null && getAfter(/boards/$boardId/posts/$postId/votes/@user.id) != null) || (@newData.votes + 1 == @data.votes && get(/boards/$boardId/posts/$postId/votes/@user.id) != null && getAfter(/boards/$boardId/posts/$postId/votes/@user.id) == null))) || (@def.joined && @def.postSame && @newData.votes == @data.votes && @newData.comments == @data.comments + 1 && @newData.lastComment != null && get(/boards/$boardId/posts/$postId/comments/@newData.lastComment) == null && getAfter(/boards/$boardId/posts/$postId/comments/@newData.lastComment) != null)",
      "delete": "@def.isOwner"
    }
  },
  "boards/$boardId/posts/$postId/votes/$userId": {
    "description": "One person's vote on one post: one document per person per post, written in the same batch that raises the post's count by one (and removed in the batch that lowers it). Only the voter and the board's owners can read it, so nobody else learns who voted for what; everyone sees the count on the post. from is set when an owner merges a duplicate and the vote moves here from that post.",
    "fields": {
      "at": "UInt!",
      "from": "String?"
    },
    "rules": {
      "read": "@user.id != null && (@user.id == $userId || get(/boards/$boardId/owners/@user.id) != null)",
      "create": "@def.joined && @newData.at <= @time.now && getAfter(/boards/$boardId/people/$userId/votes/$postId) != null && ((@user.id == $userId && @newData.from == null && ((get(/boards/$boardId/posts/$postId) == null && getAfter(/boards/$boardId/posts/$postId).votes == 1) || (get(/boards/$boardId/posts/$postId) != null && getAfter(/boards/$boardId/posts/$postId).votes == get(/boards/$boardId/posts/$postId).votes + 1))) || (get(/boards/$boardId/owners/@user.id) != null && @newData.from != null && get(/boards/$boardId/posts/@newData.from/votes/$userId) != null))",
      "update": "false",
      "delete": "@user.id != null && @user.id == $userId && getAfter(/boards/$boardId/people/$userId/votes/$postId) == null && getAfter(/boards/$boardId/posts/$postId).votes + 1 == get(/boards/$boardId/posts/$postId).votes"
    }
  },
  "boards/$boardId/people/$userId/votes/$postId": {
    "description": "The same vote, listed under the person, so a device can show which posts it voted for in one read. Exists exactly when the post's vote record does.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && @user.id == $userId",
      "create": "@user.id != null && (@user.id == $userId || get(/boards/$boardId/owners/@user.id) != null) && @newData.at <= @time.now && getAfter(/boards/$boardId/posts/$postId/votes/$userId) != null",
      "update": "false",
      "delete": "@user.id != null && @user.id == $userId && getAfter(/boards/$boardId/posts/$postId/votes/$userId) == null"
    }
  },
  "boards/$boardId/posts/$postId/comments/$commentId": {
    "description": "A comment on a post, written in the same batch that raises the post's comment count by one. team marks a reply from an owner. Owners can delete comments. A hidden post's comments are read only by owners and the post's author, like the post.",
    "fields": {
      "body": "String",
      "name": "String",
      "team": "Bool",
      "by": "String!",
      "at": "UInt!"
    },
    "rules": {
      "read": "@def.joined && (get(/boards/$boardId/posts/$postId).hidden == false || get(/boards/$boardId/posts/$postId).by == @user.id || get(/boards/$boardId/owners/@user.id) != null)",
      "create": "@def.joined && @newData.by == @user.id && @newData.at <= @time.now && @newData.at >= @time.now - 300 && @StringUtils.length(@newData.body) >= 1 && @StringUtils.length(@newData.body) <= 3000 && @StringUtils.length(@newData.name) <= 60 && (@newData.team == false || get(/boards/$boardId/owners/@user.id) != null) && getAfter(/boards/$boardId/posts/$postId).comments == get(/boards/$boardId/posts/$postId).comments + 1 && getAfter(/boards/$boardId/posts/$postId).lastComment == $commentId",
      "update": "false",
      "delete": "@def.isOwner"
    }
  },
  "boards/$boardId/changelog/$entryId": {
    "description": "A changelog entry the owners publish: a title, a Markdown body and the posts it shipped (a JSON list of post ids), so voters see what came of their votes.",
    "fields": {
      "title": "String",
      "body": "String",
      "posts": "String",
      "at": "UInt",
      "by": "String!"
    },
    "rules": {
      "read": "@def.joined",
      "create": "@def.isOwner && @newData.by == @user.id && @def.entryText",
      "update": "@def.isOwner && @def.entryText",
      "delete": "@def.isOwner"
    }
  },
  "functions": {
    "api": {
      "auth": "true",
      "entry": "functions/api.ts",
      "public": true,
      "methods": [
        "GET",
        "POST",
        "PATCH"
      ],
      "cors": "passthrough"
    }
  },
  "constants": {
    "AGENT_ADDRESS": "CNQSVDHikc1Dyn5FTsGyz2MmWym2kFEXvwUCcwkuPWXz",
    "AGENT_ADDRESS_BOUND": true
  }
}

Change history

Point store links to openapps.free
4a4c54cd95 · 18 hours ago
Release with preview
1d0728818d · 19 hours ago
Retry release
1abc843b3f · 19 hours ago
Bind app agent address in policy
00eb15f50d · 19 hours ago
Use OpenApps service URLs and add contribution strip
347a7b8ca0 · 19 hours ago
Bounded source sync
a0aaceae63 · 1 day ago
Bounded source sync
3125b8113f · 1 day ago
Bounded source sync
882a76ba95 · 1 day ago
Bounded source sync
852d9782ee · 1 day ago
Bounded source sync
a8ee705109 · 1 day ago