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.
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.
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
Release with preview
Retry release
Bind app agent address in policy
Use OpenApps service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync