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 703ed60f2cc5ad3d98a5bf4a
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": {
"member": "@user.id != null && get(/ws/$w/members/@user.id) != null",
"memberAfter": "@user.id != null && getAfter(/ws/$w/members/@user.id) != null",
"owner": "@user.id != null && get(/ws/$w/members/@user.id).role == 'owner'",
"shareOwner": "@user.id != null && get(/shared/$s/owners/@user.id) != null"
},
"users/$u": {
"description": "Which workspace is this person's own. Only they can read or change it, and only to a workspace they own.",
"fields": {
"ws": "String",
"at": "UInt"
},
"rules": {
"read": "@user.id != null && @user.id == $u",
"create": "@user.id != null && @user.id == $u && getAfter(/ws/@newData.ws/members/@user.id).role == 'owner'",
"update": "@user.id != null && @user.id == $u && getAfter(/ws/@newData.ws/members/@user.id).role == 'owner'",
"delete": "false"
}
},
"ws/$w": {
"description": "A workspace: one person's time, projects, clients and tags. Only its members can read it or anything under it. name is the person's name (shown on shared projects and invoices); rate is the default hourly rate in the currency's minor unit; weekStart is 1 for Monday or 0 for Sunday; tz is the IANA time zone days are counted in.",
"fields": {
"name": "String",
"currency": "String",
"rate": "UInt",
"weekStart": "UInt",
"tz": "String",
"createdBy": "String!",
"createdAt": "UInt!"
},
"rules": {
"read": "@def.member",
"create": "@user.id != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @StringUtils.length($w) == 20 && @StringUtils.length(@newData.currency) == 3 && @StringUtils.length(@newData.name) <= 80 && @StringUtils.length(@newData.tz) <= 64 && @newData.rate <= 100000000 && @newData.weekStart <= 1 && getAfter(/ws/$w/members/@user.id).role == 'owner'",
"update": "@def.owner && @StringUtils.length(@newData.currency) == 3 && @StringUtils.length(@newData.name) <= 80 && @StringUtils.length(@newData.tz) <= 64 && @newData.rate <= 100000000 && @newData.weekStart <= 1",
"delete": "false"
}
},
"ws/$w/members/$u": {
"description": "Who can use the workspace. role is owner or api. The creator becomes an owner in the batch that creates the workspace; a person signing in with email becomes an owner by claiming an unused hand-off code an owner made in the last 2 hours (invite holds the code); the public api function joins as role api by presenting the hash of a workspace key an owner created (invite holds the hash). Anyone can leave; owners can remove anyone.",
"fields": {
"role": "String!",
"invite": "String!",
"at": "UInt!"
},
"rules": {
"read": "@def.member",
"create": "@user.id != null && @user.id == $u && @newData.at <= @time.now && ((get(/ws/$w) == null && getAfter(/ws/$w).createdBy == @user.id && @newData.role == 'owner' && @newData.invite == 'created') || (@newData.role == 'owner' && get(/ws/$w/handoffs/@newData.invite) != null && get(/ws/$w/handoffs/@newData.invite).usedBy == '' && getAfter(/ws/$w/handoffs/@newData.invite).usedBy == @user.id) || (@user.id == @const.BOUNDED_PUBLIC_PRINCIPAL_API && @newData.role == 'api' && get(/ws/$w/apikeys/@newData.invite) != null))",
"update": "false",
"delete": "@user.id != null && (@user.id == $u || get(/ws/$w/members/@user.id).role == 'owner')"
}
},
"ws/$w/handoffs/$code": {
"description": "A one-time code that moves a guest's workspace into their email account: the guest makes it right before signing in, and the account claims it right after (usedBy is set in the same batch that adds the account as an owner). It works for 2 hours, once, and only while the owner who made it is still an owner.",
"fields": {
"by": "String!",
"createdAt": "UInt!",
"usedBy": "String"
},
"rules": {
"read": "@def.owner",
"create": "@def.owner && @newData.by == @user.id && @newData.createdAt <= @time.now && @newData.usedBy == '' && @StringUtils.length($code) == 20",
"update": "@user.id != null && @data.usedBy == '' && @newData.usedBy == @user.id && @data.createdAt + 7200 > @time.now && get(/ws/$w/members/@data.by).role == 'owner' && getAfter(/ws/$w/members/@user.id).role == 'owner'",
"delete": "@def.owner"
}
},
"ws/$w/apikeys/$hash": {
"description": "A workspace key for AI assistants, created by an owner. Only the SHA-256 hash of the key is stored (it is the document id), so nobody can read a key back. The public api function looks a key up by its hash.",
"fields": {
"label": "String!",
"hint": "String!",
"by": "String!",
"createdAt": "UInt!"
},
"rules": {
"read": "@user.id != null && (@user.id == @const.BOUNDED_PUBLIC_PRINCIPAL_API || get(/ws/$w/members/@user.id) != null)",
"create": "@def.owner && @newData.by == @user.id && @newData.createdAt <= @time.now && @StringUtils.length($hash) == 64 && @StringUtils.length(@newData.label) >= 1 && @StringUtils.length(@newData.label) <= 40 && @StringUtils.length(@newData.hint) <= 8",
"update": "false",
"delete": "@def.owner"
}
},
"ws/$w/clients/$c": {
"description": "A client that projects belong to.",
"fields": {
"name": "String",
"archived": "Bool",
"at": "UInt"
},
"rules": {
"read": "@def.member",
"create": "@def.memberAfter && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 100",
"update": "@def.member && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 100",
"delete": "@def.member"
}
},
"ws/$w/projects/$p": {
"description": "A project. rate is the hourly rate in minor units (0 uses the workspace's default); billable is the default for new entries. share is the id of the shared project when this one is shared by link; team is true for someone else's project joined by link (owner is their name).",
"fields": {
"name": "String",
"color": "String",
"client": "String",
"rate": "UInt",
"billable": "Bool",
"archived": "Bool",
"share": "String",
"team": "Bool",
"owner": "String",
"at": "UInt"
},
"rules": {
"read": "@def.member",
"create": "@def.memberAfter && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 100 && @StringUtils.length(@newData.color) <= 9 && @StringUtils.length(@newData.client) <= 40 && @StringUtils.length(@newData.share) <= 40 && @StringUtils.length(@newData.owner) <= 80 && @newData.rate <= 100000000",
"update": "@def.member && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 100 && @StringUtils.length(@newData.color) <= 9 && @StringUtils.length(@newData.client) <= 40 && @StringUtils.length(@newData.share) <= 40 && @StringUtils.length(@newData.owner) <= 80 && @newData.rate <= 100000000",
"delete": "@def.member"
}
},
"ws/$w/tags/$t": {
"description": "A tag that entries can carry.",
"fields": {
"name": "String",
"at": "UInt"
},
"rules": {
"read": "@def.member",
"create": "@def.memberAfter && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 60",
"update": "@def.member && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 60",
"delete": "@def.member"
}
},
"ws/$w/entries/$e": {
"description": "A time entry. start and end are unix seconds (end is never before start, and one entry is at most 1,000 hours); project is a project id or ''; tags is a JSON list of tag ids.",
"fields": {
"description": "String",
"project": "String",
"tags": "String",
"billable": "Bool",
"start": "UInt",
"end": "UInt",
"at": "UInt"
},
"rules": {
"read": "@def.member",
"create": "@def.memberAfter && @newData.end >= @newData.start && @newData.end <= @newData.start + 3600000 && @StringUtils.length(@newData.description) <= 1000 && @StringUtils.length(@newData.project) <= 40 && @StringUtils.length(@newData.tags) <= 2000",
"update": "@def.member && @newData.end >= @newData.start && @newData.end <= @newData.start + 3600000 && @StringUtils.length(@newData.description) <= 1000 && @StringUtils.length(@newData.project) <= 40 && @StringUtils.length(@newData.tags) <= 2000",
"delete": "@def.member"
}
},
"ws/$w/timer/$t": {
"description": "The workspace's running timer (the only document is 'now'). start is unix seconds, or 0 when no timer runs; entry is the id the entry gets when the timer stops.",
"fields": {
"description": "String",
"project": "String",
"tags": "String",
"billable": "Bool",
"start": "UInt",
"entry": "String",
"at": "UInt"
},
"rules": {
"read": "@def.member",
"create": "@def.memberAfter && $t == 'now' && @StringUtils.length(@newData.description) <= 1000 && @StringUtils.length(@newData.project) <= 40 && @StringUtils.length(@newData.tags) <= 2000 && @StringUtils.length(@newData.entry) <= 40",
"update": "@def.member && @StringUtils.length(@newData.description) <= 1000 && @StringUtils.length(@newData.project) <= 40 && @StringUtils.length(@newData.tags) <= 2000 && @StringUtils.length(@newData.entry) <= 40",
"delete": "false"
}
},
"shared/$s": {
"description": "A project shared by link so teammates can log time to it. Anyone who knows the id can join (a join below); only its owners can change it or read the time logged to it. billable is the owner's default for new time on it; ws is the owner's workspace; closed stops new time.",
"fields": {
"name": "String",
"color": "String",
"client": "String",
"billable": "Bool",
"owner": "String",
"ws": "String!",
"closed": "Bool",
"createdBy": "String!",
"createdAt": "UInt!"
},
"rules": {
"read": "@user.id != null && (get(/shared/$s/joins/@user.id) != null || get(/shared/$s/owners/@user.id) != null)",
"create": "@user.id != null && @newData.createdBy == @user.id && @newData.createdAt <= @time.now && @StringUtils.length($s) == 20 && get(/ws/@newData.ws/members/@user.id).role == 'owner' && getAfter(/shared/$s/owners/@user.id) != null && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 100 && @StringUtils.length(@newData.color) <= 9 && @StringUtils.length(@newData.client) <= 100 && @StringUtils.length(@newData.owner) <= 80",
"update": "@def.shareOwner && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 100 && @StringUtils.length(@newData.color) <= 9 && @StringUtils.length(@newData.client) <= 100 && @StringUtils.length(@newData.owner) <= 80",
"delete": "false"
}
},
"shared/$s/owners/$u": {
"description": "Someone who owns the shared project: a member of the workspace it belongs to (the person, or the api function acting for a workspace key).",
"fields": {
"ws": "String!",
"at": "UInt!"
},
"rules": {
"read": "@user.id != null && (@user.id == $u || get(/shared/$s/owners/@user.id) != null)",
"create": "@user.id != null && @user.id == $u && @newData.at <= @time.now && get(/ws/@newData.ws/members/@user.id) != null && getAfter(/shared/$s).ws == @newData.ws",
"update": "false",
"delete": "@def.shareOwner"
}
},
"shared/$s/joins/$u": {
"description": "A teammate who opened the project's link. name is shown to the owner; ws is the teammate's own workspace. The owner can remove a teammate (which also marks their workspace removed); a teammate can leave.",
"fields": {
"name": "String",
"ws": "String",
"at": "UInt!"
},
"rules": {
"read": "@user.id != null && (@user.id == $u || get(/shared/$s/owners/@user.id) != null)",
"create": "@user.id != null && @user.id == $u && @newData.at <= @time.now && getAfter(/shared/$s) != null && get(/shared/$s).closed != true && get(/ws/@newData.ws/members/@user.id) != null && get(/shared/$s/removed/@newData.ws) == null && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 80",
"update": "@user.id != null && @user.id == $u && get(/ws/@newData.ws/members/@user.id) != null && get(/shared/$s/removed/@newData.ws) == null && @StringUtils.length(@newData.name) >= 1 && @StringUtils.length(@newData.name) <= 80",
"delete": "@user.id != null && (@user.id == $u || get(/shared/$s/owners/@user.id) != null)"
}
},
"shared/$s/removed/$w": {
"description": "A teammate's workspace the owner removed from the shared project. Its people and its assistants cannot join again, add time, or change or delete the time it logged, until an owner lets them back in. Members of that workspace can read it, so the api function can tell.",
"fields": {
"name": "String",
"at": "UInt!"
},
"rules": {
"read": "@def.shareOwner || (@user.id != null && get(/ws/$w/members/@user.id) != null)",
"create": "@def.shareOwner && @newData.at <= @time.now && @StringUtils.length(@newData.name) <= 80",
"update": "false",
"delete": "@def.shareOwner"
}
},
"shared/$s/entries/$e": {
"description": "A teammate's time on the shared project: a copy of an entry in the teammate's own workspace (ws), written in the same batch. Only the project's owners can read it. who is the teammate's name; tags is a JSON list of tag names. Once the owner stops sharing or removes the teammate's workspace, what it logged stays: only owners can delete it.",
"fields": {
"ws": "String",
"who": "String",
"description": "String",
"tags": "String",
"billable": "Bool",
"start": "UInt",
"end": "UInt",
"at": "UInt"
},
"rules": {
"read": "@def.shareOwner",
"create": "@user.id != null && get(/shared/$s/joins/@user.id) != null && get(/shared/$s).closed != true && get(/ws/@newData.ws/members/@user.id) != null && get(/shared/$s/removed/@newData.ws) == null && @newData.end >= @newData.start && @newData.end <= @newData.start + 3600000 && @StringUtils.length(@newData.who) <= 80 && @StringUtils.length(@newData.description) <= 1000 && @StringUtils.length(@newData.tags) <= 2000",
"update": "@user.id != null && get(/shared/$s/joins/@user.id) != null && get(/shared/$s).closed != true && get(/ws/@data.ws/members/@user.id) != null && get(/shared/$s/removed/@data.ws) == null && get(/ws/@newData.ws/members/@user.id) != null && get(/shared/$s/removed/@newData.ws) == null && @newData.end >= @newData.start && @newData.end <= @newData.start + 3600000 && @StringUtils.length(@newData.who) <= 80 && @StringUtils.length(@newData.description) <= 1000 && @StringUtils.length(@newData.tags) <= 2000",
"delete": "@def.shareOwner || (@user.id != null && get(/ws/@data.ws/members/@user.id) != null && get(/shared/$s).closed != true && get(/shared/$s/removed/@data.ws) == null)"
}
},
"functions": {
"api": {
"auth": "true",
"entry": "functions/api.ts",
"public": true,
"methods": [
"GET",
"POST",
"DELETE"
],
"cors": "passthrough"
}
},
"constants": {
"AGENT_ADDRESS": "jBNKzHpGmdV9K5BZUF99g2chhgm3Zx1EobxVgJisBtx",
"AGENT_ADDRESS_BOUND": true
}
}
Change history
Point OpenApps links to openapps.free
Bind agent address constants in policy
Use functions.openapps.xyz alias and add OpenApp contribution strip
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync