polls-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 c06cea4889e9
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 334801de44cb0fe911d5a88b
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(/events/$eventId/joins/@user.id) != null",
    "isOwner": "@user.id != null && get(/events/$eventId/owners/@user.id) != null",
    "eventFields": "@StringUtils.length(@newData.title) >= 1 && @StringUtils.length(@newData.title) <= 120 && @StringUtils.length(@newData.current) <= 40",
    "pollFields": "(@newData.type == 'choice' || @newData.type == 'wordcloud' || @newData.type == 'rating' || @newData.type == 'open' || @newData.type == 'ranking') && @StringUtils.length(@newData.question) >= 1 && @StringUtils.length(@newData.question) <= 300 && @StringUtils.length(@newData.options) >= 2 && @StringUtils.length(@newData.options) <= 8000 && @newData.max <= 10 && (@newData.status == 'draft' || @newData.status == 'open' || @newData.status == 'closed') && @StringUtils.length(@newData.results) <= 50000 && (@newData.showResults == true || @newData.results == '')",
    "answerFields": "@newData.at <= @time.now && @StringUtils.length(@newData.answer) >= 1 && @StringUtils.length(@newData.answer) <= 2000"
  },
  "events/$eventId": {
    "description": "An event: its title, 6-digit join code and settings. Readable by anyone who joined it (knowing the event id is enough to join); only owners can change it. current is the poll id the presenter shows on screen, 'qa' for the questions, or empty.",
    "fields": {
      "title": "String",
      "code": "String!",
      "current": "String",
      "qa": "Bool",
      "moderate": "Bool",
      "allowChange": "Bool",
      "createdBy": "String!",
      "createdAt": "UInt!"
    },
    "rules": {
      "read": "@def.joined",
      "create": "@user.id != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @def.eventFields && getAfter(/events/$eventId/owners/@user.id) != null && getAfter(/eventkeys/$eventId) != null && getAfter(/codes/@newData.code).event == $eventId",
      "update": "@def.isOwner && @def.eventFields",
      "delete": "false"
    }
  },
  "events/$eventId/joins/$userId": {
    "description": "Someone who opened the event on a device. Knowing the event id is what lets you join, and joining is what lets you read it.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && @user.id == $userId",
      "create": "@user.id != null && @user.id == $userId && @newData.at <= @time.now && getAfter(/events/$eventId) != null",
      "update": "false",
      "delete": "false"
    }
  },
  "events/$eventId/owners/$userId": {
    "description": "Someone who runs the event: the creator, in the same batch that creates it, or anyone who presents the admin key.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@def.joined",
      "create": "@user.id != null && @user.id == $userId && @newData.at <= @time.now && ((get(/events/$eventId) == null && getAfter(/events/$eventId).createdBy == @user.id) || (get(/eventkeys/$eventId) != null && getAfter(/eventclaims/$eventId/c/@user.id).key == get(/eventkeys/$eventId).key))",
      "update": "false",
      "delete": "false"
    }
  },
  "eventkeys/$eventId": {
    "description": "The event'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(/events/$eventId) == null && getAfter(/events/$eventId).createdBy == @user.id && @StringUtils.length(@newData.key) == 24",
      "update": "false",
      "delete": "false"
    }
  },
  "eventclaims/$eventId/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",
      "update": "@user.id != null && @user.id == $userId",
      "delete": "false"
    }
  },
  "codes/$code": {
    "description": "A 6-digit join code, made with its event and never reused. Reading one needs a recorded try (codetries), and tries are rate limited per device, so the million possible codes cannot be listed or quickly scanned.",
    "fields": {
      "event": "String!",
      "at": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && get(/codetries/@user.id/t/$code) != null",
      "create": "@user.id != null && @newData.at <= @time.now && @StringUtils.length($code) == 6 && $code * 1 >= 100000 && $code * 1 <= 999999 && get(/events/@newData.event) == null && getAfter(/events/@newData.event).createdBy == @user.id && getAfter(/events/@newData.event).code == $code",
      "update": "false",
      "delete": "false"
    }
  },
  "codetries/$userId/t/$code": {
    "description": "One device trying one join code. Each device may try 10 different codes in 10 minutes.",
    "tier": "durable",
    "fields": {
      "n": "UInt"
    },
    "rules": {
      "read": "@user.id != null && @user.id == $userId",
      "create": "@user.id != null && @user.id == $userId && @newData.n == 1",
      "update": "false",
      "delete": "false"
    },
    "invariants": [
      {
        "type": "rollingSum",
        "name": "code_tries_per_device",
        "field": "n",
        "windowSeconds": 600,
        "limit": 10,
        "scopeVariable": "$userId"
      }
    ]
  },
  "events/$eventId/polls/$pollId": {
    "description": "One poll. options is a JSON list of strings (choice and ranking). max is the top of a rating scale or how many words each person may give a word cloud. status is draft (hidden from the audience), open (taking votes) or closed. results is the summary the presenter's device publishes for the audience while showResults is on.",
    "fields": {
      "type": "String!",
      "question": "String",
      "options": "String",
      "multi": "Bool",
      "max": "UInt",
      "pos": "Int",
      "status": "String",
      "showResults": "Bool",
      "results": "String",
      "createdAt": "UInt!"
    },
    "rules": {
      "read": "@def.joined && (@data.status != 'draft' || get(/events/$eventId/owners/@user.id) != null)",
      "create": "@user.id != null && getAfter(/events/$eventId/owners/@user.id) != null && @newData.createdAt <= @time.now && @def.pollFields",
      "update": "@def.isOwner && @def.pollFields",
      "delete": "@def.isOwner"
    }
  },
  "events/$eventId/polls/$pollId/votes/$userId": {
    "description": "One person's answer to one poll: one document per person per poll, so each person votes once. answer is JSON. It can be changed only while the poll is open and only if the event lets people change answers. Only the voter and the owners can read it; owners can clear votes.",
    "fields": {
      "answer": "String",
      "at": "UInt"
    },
    "rules": {
      "read": "@user.id != null && (@user.id == $userId || get(/events/$eventId/owners/@user.id) != null)",
      "create": "@user.id != null && @user.id == $userId && get(/events/$eventId/joins/@user.id) != null && get(/events/$eventId/polls/$pollId).status == 'open' && @def.answerFields",
      "update": "@user.id != null && @user.id == $userId && get(/events/$eventId/polls/$pollId).status == 'open' && get(/events/$eventId).allowChange == true && @def.answerFields",
      "delete": "@def.isOwner"
    }
  },
  "events/$eventId/questions/$questionId": {
    "description": "An audience question. status is pending (waiting for approval), live, answered or hidden; the audience sees live and answered ones, their own, and nothing else. upvotes goes up by exactly one with each person's upvote record. Only the public API writes questions, so the account stored as their creator is the API's, never the asker's (see asked), and a question without a name stays anonymous to the host and the audience.",
    "fields": {
      "text": "String!",
      "name": "String!",
      "status": "String",
      "upvotes": "UInt",
      "createdAt": "UInt!"
    },
    "rules": {
      "read": "@def.joined && (@data.status == 'live' || @data.status == 'answered' || get(/events/$eventId/asked/@user.id/q/$questionId) != null || get(/events/$eventId/owners/@user.id) != null)",
      "create": "@user.id == @const.BOUNDED_PUBLIC_PRINCIPAL_API && get(/events/$eventId/joins/@user.id) != null && get(/events/$eventId).qa == true && @newData.upvotes == 0 && @newData.createdAt <= @time.now && @StringUtils.length(@newData.text) >= 1 && @StringUtils.length(@newData.text) <= 300 && @StringUtils.length(@newData.name) <= 40 && ((get(/events/$eventId).moderate == true && @newData.status == 'pending') || (get(/events/$eventId).moderate == false && @newData.status == 'live'))",
      "update": "(@def.isOwner && @newData.upvotes == @data.upvotes && (@newData.status == 'pending' || @newData.status == 'live' || @newData.status == 'answered' || @newData.status == 'hidden')) || (@user.id != null && @data.status == 'live' && @newData.status == @data.status && @newData.upvotes == @data.upvotes + 1 && get(/events/$eventId/upvotes/@user.id/q/$questionId) == null && getAfter(/events/$eventId/upvotes/@user.id/q/$questionId) != null)",
      "delete": "false"
    }
  },
  "events/$eventId/asked/$userId/q/$questionId": {
    "description": "Who asked a question, written by the asker just before the public API stores the question with this id. Only the asker can read it, so people see their own pending questions and nobody else learns who asked what.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && @user.id == $userId",
      "create": "@user.id != null && @user.id == $userId && @newData.at <= @time.now && get(/events/$eventId/joins/@user.id) != null && get(/events/$eventId/questions/$questionId) == null",
      "update": "false",
      "delete": "false"
    }
  },
  "events/$eventId/upvotes/$userId/q/$questionId": {
    "description": "One person's upvote on one question. One per person per question, written in the same batch that raises the question's count by one.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && @user.id == $userId",
      "create": "@user.id != null && @user.id == $userId && get(/events/$eventId/joins/@user.id) != null && @newData.at <= @time.now && getAfter(/events/$eventId/questions/$questionId).upvotes == get(/events/$eventId/questions/$questionId).upvotes + 1",
      "update": "false",
      "delete": "false"
    }
  },
  "functions": {
    "api": {
      "auth": "true",
      "entry": "functions/api.ts",
      "public": true,
      "methods": [
        "GET",
        "POST"
      ],
      "cors": "passthrough"
    }
  },
  "constants": {
    "AGENT_ADDRESS": "6Q8EEeRg5zDpXtj1rP9PtoDatuLRREfBLnHDVUiN2UhH",
    "AGENT_ADDRESS_BOUND": true
  }
}

Change history

Point OpenApps links to openapps.free
c06cea4889 · 1 day ago
Bind agent address in policy constants
a7de116d52 · 1 day ago
Use OpenApps service URLs and add contribution strip
e8ce0b0bf0 · 1 day ago
Bounded source sync
dcdc4d56e6 · 2 days ago
Bounded source sync
7acdace1e1 · 2 days ago
Bounded source sync
2140a66ea0 · 2 days ago
Bounded source sync
0db2fe3ca0 · 2 days ago
Bounded source sync
996f633a3c · 2 days ago