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 c7891873549c9ca1004894b3
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.
src/lib/data.ts
// Reads and writes shared by the pages: joining an exchange, opening a person's page with their key,
// and writing new people. Every rule behind these lives in policy.json.
import { get, set, setMany, serverTimestamp } from "@bounded-sh/client";
import { me, explain } from "./bounded";
import { randomId } from "../../functions/lib/santa";
export interface NewPerson { name: string; contact: string; mine?: boolean }
type Write = { path: string; document: Record<string, unknown> | null };
/** Signs in as a guest and joins the exchange on this device (knowing its id is the permission). False when there is no such exchange. */
export async function openExchange(id: string): Promise<{ uid: string; found: boolean }> {
const uid = await me();
if (await get(`exchanges/${id}/joins/${uid}`)) return { uid, found: true };
try {
await set(`exchanges/${id}/joins/${uid}`, { at: serverTimestamp() });
return { uid, found: true };
} catch (err) {
// The join is refused only when there is no exchange with that id.
if (/offline/.test(explain(err))) throw err;
return { uid, found: false };
}
}
/** The claim and its open record, written together, so every device that opens a page is listed on it. */
export function claimWrites(id: string, pid: string, key: string, uid: string): Write[] {
const openId = `${pid}_${uid}`;
return [
{ path: `personclaims/${pid}/c/${uid}`, document: { key, exchangeId: id, openId } },
{ path: `exchanges/${id}/opens/${openId}`, document: { pid, by: uid, via: "web", at: serverTimestamp() } },
];
}
/** A person, their key and their contact. `self` people joined from the link and hold their own key. */
export function personWrites(id: string, uid: string, p: { pid: string; key: string; name: string; contact: string; self: boolean }): Write[] {
return [
{ path: `exchanges/${id}/people/${p.pid}`, document: { name: p.name, addedBy: uid, self: p.self, createdAt: serverTimestamp() } },
{ path: `personkeys/${p.pid}`, document: { key: p.key, exchangeId: id } },
...(p.contact ? [{ path: `exchanges/${id}/contacts/${p.pid}`, document: { contact: p.contact, by: uid } }] : []),
];
}
/** Writes groups in batches the data plane takes comfortably, never splitting a group (a person and their key go together). */
export async function writeGroups(groups: Write[][]) {
let batch: Write[] = [];
for (const g of groups) {
if (batch.length && batch.length + g.length > 90) { await setMany(batch); batch = []; }
batch.push(...g);
}
if (batch.length) await setMany(batch);
}
/** Makes an exchange owned by this device, with its people; the organizer's own person opens here. */
export async function createExchange(o: { name: string; budget: string; date: string; people: NewPerson[] }) {
const uid = await me();
const id = randomId(20);
const key = randomId(24);
const people = o.people.map((p) => ({ ...p, pid: randomId(20), key: randomId(24), self: false }));
const mine = people.find((p) => p.mine);
await writeGroups([[
{ path: `exchanges/${id}`, document: { name: o.name, budget: o.budget, date: o.date, notes: "", allowJoin: true, organizerSees: false, revealAt: 0, status: "open", round: 0, drawnAt: 0, createdBy: uid, createdAt: serverTimestamp() } },
{ path: `exchanges/${id}/joins/${uid}`, document: { at: serverTimestamp() } },
{ path: `exchanges/${id}/owners/${uid}`, document: { at: serverTimestamp() } },
{ path: `exchangekeys/${id}`, document: { key } },
{ path: `keepapart/${id}`, document: { pairs: "[]" } },
], ...people.map((p) => [...personWrites(id, uid, p), ...(p.mine ? claimWrites(id, p.pid, p.key, uid) : [])])]);
return { id, key, mine: mine ? { pid: mine.pid, key: mine.key, name: mine.name } : null };
}
Change history
Point OpenApps links to openapps.free
Bind agent address in policy constants
Use functions.openapps.xyz alias, add OpenApp contribution strip
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync