split-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 9ed962174401
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 20ace674a35b849ffda63b30
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
  },
  "groups/$groupId": {
    "description": "A group of people sharing costs. Readable by anyone who has opened its link (a join below); the random group id is the link's secret.",
    "fields": {
      "name": "String",
      "currency": "String",
      "createdBy": "String!",
      "createdAt": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && get(/groups/$groupId/joins/@user.id) != null",
      "create": "@user.id != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 80 && @StringUtils.length(@newData.currency) == 3 && getAfter(/groups/$groupId/joins/@user.id) != null",
      "update": "@user.id != null && get(/groups/$groupId/joins/@user.id) != null && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 80 && @StringUtils.length(@newData.currency) == 3",
      "delete": "false"
    }
  },
  "groups/$groupId/joins/$userId": {
    "description": "Someone who opened the group's link on a device. Knowing the group id is what lets you join.",
    "fields": {
      "at": "UInt!"
    },
    "rules": {
      "read": "@user.id != null && @user.id == $userId",
      "create": "@user.id != null && @user.id == $userId && @newData.at <= @time.now && getAfter(/groups/$groupId) != null",
      "update": "false",
      "delete": "@user.id != null && @user.id == $userId"
    }
  },
  "groups/$groupId/people/$personId": {
    "description": "A person in the split. People are names, not accounts: anyone in the group can add or rename them.",
    "fields": {
      "name": "String",
      "createdAt": "UInt!",
      "removed": "Bool?"
    },
    "rules": {
      "read": "@user.id != null && get(/groups/$groupId/joins/@user.id) != null",
      "create": "@user.id != null && getAfter(/groups/$groupId/joins/@user.id) != null && @newData.createdAt <= @time.now && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 40",
      "update": "@user.id != null && get(/groups/$groupId/joins/@user.id) != null && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 40",
      "delete": "false"
    }
  },
  "groups/$groupId/expenses/$expenseId": {
    "description": "An expense or a payment between two people. amount is in the currency's minor unit; shares is a JSON object of personId to weight (equal, shares) or minor units (exact).",
    "fields": {
      "title": "String",
      "amount": "UInt",
      "paidBy": "String",
      "splitMode": "String",
      "shares": "String",
      "kind": "String",
      "date": "UInt",
      "createdAt": "UInt!",
      "createdBy": "String!",
      "deleted": "Bool?"
    },
    "rules": {
      "read": "@user.id != null && get(/groups/$groupId/joins/@user.id) != null",
      "create": "@user.id != null && getAfter(/groups/$groupId/joins/@user.id) != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @newData.amount > 0 && (@newData.kind == 'expense' || @newData.kind == 'payment') && (@newData.splitMode == 'equal' || @newData.splitMode == 'shares' || @newData.splitMode == 'exact') && @StringUtils.length(@newData.title) <= 120 && @StringUtils.length(@newData.shares) <= 4000 && getAfter(/groups/$groupId/people/@newData.paidBy) != null",
      "update": "@user.id != null && get(/groups/$groupId/joins/@user.id) != null && @newData.amount > 0 && (@newData.kind == 'expense' || @newData.kind == 'payment') && (@newData.splitMode == 'equal' || @newData.splitMode == 'shares' || @newData.splitMode == 'exact') && @StringUtils.length(@newData.title) <= 120 && @StringUtils.length(@newData.shares) <= 4000 && getAfter(/groups/$groupId/people/@newData.paidBy) != null",
      "delete": "false"
    }
  },
  "functions": {
    "api": {
      "auth": "true",
      "entry": "functions/api.ts",
      "public": true,
      "methods": [
        "GET",
        "POST",
        "PATCH",
        "DELETE"
      ],
      "cors": "passthrough"
    }
  },
  "constants": {
    "AGENT_ADDRESS": "8P6e5ZitNyQpRqYMEBUdjbUYKwMc211NndcoZTsC5r31",
    "AGENT_ADDRESS_BOUND": true
  }
}

Change history

Publish quality pass from owner chat
9ed9621744 · 5 hours ago
Bind agent address in policy constants
7effc1cbbd · 5 hours ago
Quality pass: cleaner group page, phone polish, fuller API, sourced comparison
5345219781 · 5 hours ago
Bounded source sync
277fb9ad96 · 1 day ago
Bounded source sync
e944552325 · 1 day ago