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.
src/lib/session.ts
// The session and its workspace. A first visit creates a guest and a workspace; "Keep my time on all
// devices" moves the guest's workspace into an email account. Right before the hosted sign-in the guest
// makes a one-time hand-off code; right after it, the account claims the code and becomes an owner.
// A new account adopts the workspace as it is. An account that already has a workspace (signed in on
// another device first) gets this browser's projects, clients, tags and time copied into it.
import { useEffect, useState } from "react";
import { get, set, setMany, serverTimestamp } from "@bounded-sh/client";
import { CURRENCIES, localTz, randomId, rescale, type Entry, type Project } from "../../functions/lib/time";
import { me, signIn, type Me } from "./bounded";
export const NOW = () => serverTimestamp();
export type Write = { path: string; document: Record<string, unknown> | null };
export interface Session { me: Me; ws: string }
const KEY = "time:handoff";
interface Handoff { guest: string; ws: string; code: string; at: number }
/** Runs writes in batches small enough for one transaction each. */
export async function writeAll(writes: Write[], size = 100) {
for (let i = 0; i < writes.length; i += size) await setMany(writes.slice(i, i + size) as never);
}
export async function listAll<T>(path: string, filter?: Record<string, unknown>): Promise<(T & { id: string })[]> {
const rows: (T & { id: string })[] = [];
let cursor: string | undefined;
do {
const res = await get(path, { limit: 1000, ...(filter ? { filter } : {}), ...(cursor ? { cursor } : {}) });
rows.push(...((res?.data || []) as (T & { id: string })[]));
cursor = res?.nextCursor || undefined;
} while (cursor);
return rows;
}
export function guessCurrency(): string {
try {
const region = new Intl.Locale(navigator.language).maximize().region || "US";
const byRegion: Record<string, string> = { US: "USD", GB: "GBP", CA: "CAD", AU: "AUD", NZ: "NZD", JP: "JPY", CH: "CHF", SE: "SEK", NO: "NOK", DK: "DKK", PL: "PLN", IN: "INR", SG: "SGD", HK: "HKD", MX: "MXN", BR: "BRL", ZA: "ZAR", IL: "ILS", KR: "KRW", AE: "AED" };
const euro = ["DE", "FR", "ES", "IT", "NL", "BE", "AT", "PT", "IE", "FI", "GR", "LU", "SK", "SI", "EE", "LV", "LT", "MT", "CY", "HR"];
const c = byRegion[region] || (euro.includes(region) ? "EUR" : "USD");
return CURRENCIES.includes(c) ? c : "USD";
} catch {
return "USD";
}
}
function guessWeekStart(): number {
try {
const region = new Intl.Locale(navigator.language).maximize().region || "US";
return ["US", "CA", "MX", "BR", "JP", "KR", "IL", "PH", "IN", "ZA", "HK", "TW", "AU"].includes(region) ? 0 : 1;
} catch {
return 1;
}
}
async function createWorkspace(m: Me): Promise<string> {
const id = randomId(20);
await setMany([
{ path: `ws/${id}`, document: { name: "", currency: guessCurrency(), rate: 0, weekStart: guessWeekStart(), tz: localTz(), createdBy: m.id, createdAt: NOW() } },
{ path: `ws/${id}/members/${m.id}`, document: { role: "owner", invite: "created", at: NOW() } },
{ path: `users/${m.id}`, document: { ws: id, at: NOW() } },
] as never);
return id;
}
function readHandoff(): Handoff | null {
try {
const h = JSON.parse(localStorage.getItem(KEY) || "null") as Handoff | null;
return h && Date.now() - h.at < 3600_000 ? h : null;
} catch {
return null;
}
}
let moved: "adopted" | "copied" | null = null;
/** What happened to a guest's time at sign-in, once. */
export function takeMoved() {
const m = moved;
moved = null;
return m;
}
/** The account claims the hand-off code and becomes an owner of the guest's workspace. */
async function finishHandoff(m: Me): Promise<string | null> {
const h = readHandoff();
try { localStorage.removeItem(KEY); } catch { /* ignore */ }
if (!h) return null;
if (h.guest === m.id || m.guest) {
// Sign-in was cancelled: the code is not needed.
if (h.guest === m.id) await set(`ws/${h.ws}/handoffs/${h.code}`, null).catch(() => null);
return null;
}
const base = `ws/${h.ws}`;
if (!(await get(`${base}/members/${m.id}`).catch(() => null))) {
await setMany([
{ path: `${base}/handoffs/${h.code}`, document: { usedBy: m.id } },
{ path: `${base}/members/${m.id}`, document: { role: "owner", invite: h.code, at: NOW() } },
] as never);
}
const home = ((await get(`users/${m.id}`)) as { ws?: string } | null)?.ws;
const doc = (await get(base)) as { name?: string; currency?: string } | null;
const projects = await listAll<Project>(`${base}/projects`);
const name = doc?.name || "Teammate";
if (!home || home === h.ws) {
await set(`users/${m.id}`, { ws: h.ws, at: NOW() });
await relink(m, h.ws, h.ws, projects, name);
await set(`${base}/members/${h.guest}`, null).catch(() => null);
moved = "adopted";
return h.ws;
}
await copyInto(h.ws, home, doc?.currency || "USD", projects);
await relink(m, h.ws, home, projects, name);
await set(`${base}/members/${h.guest}`, null).catch(() => null);
moved = "copied";
return home;
}
/** The account takes the guest's place on shared projects: owner of the ones it shared, teammate on the ones it joined. */
async function relink(m: Me, from: string, home: string, projects: Project[], name: string) {
for (const p of projects) {
if (!p.share) continue;
const write = p.team
? { path: `shared/${p.share}/joins/${m.id}`, document: { name, ws: home, at: NOW() } }
: { path: `shared/${p.share}/owners/${m.id}`, document: { ws: from, at: NOW() } };
await setMany([write] as never).catch(() => null);
}
}
/** Copies a workspace's clients, projects, tags, time and running timer into another one (same ids, so it can run twice). */
async function copyInto(from: string, to: string, fromCurrency: string, projects: Project[]) {
const target = (await get(`ws/${to}`)) as { currency?: string } | null;
const toCurrency = target?.currency || fromCurrency;
const strip = <T extends { id?: string; _id?: string; pathId?: string }>(r: T) => {
const { id: _a, _id: _b, pathId: _c, ...rest } = r as Record<string, unknown>;
for (const k of Object.keys(rest)) if (k.startsWith("_")) delete rest[k];
return rest;
};
const writes: Write[] = [];
for (const c of await listAll<Record<string, unknown>>(`ws/${from}/clients`)) writes.push({ path: `ws/${to}/clients/${c.id}`, document: { ...strip(c), at: NOW() } });
for (const t of await listAll<Record<string, unknown>>(`ws/${from}/tags`)) writes.push({ path: `ws/${to}/tags/${t.id}`, document: { ...strip(t), at: NOW() } });
for (const p of projects) writes.push({ path: `ws/${to}/projects/${p.id}`, document: { ...strip(p), rate: rescale(p.rate || 0, fromCurrency, toCurrency), at: NOW() } });
for (const e of await listAll<Entry>(`ws/${from}/entries`)) writes.push({ path: `ws/${to}/entries/${e.id}`, document: { ...strip(e), at: NOW() } });
const timer = (await get(`ws/${from}/timer/now`)) as Record<string, unknown> | null;
const theirs = (await get(`ws/${to}/timer/now`)) as { start?: number } | null;
if (timer?.start && !theirs?.start) writes.push({ path: `ws/${to}/timer/now`, document: { ...strip(timer), at: NOW() } });
await writeAll(writes);
}
let ready: Promise<Session> | null = null;
export function session(): Promise<Session> {
ready ??= (async () => {
const m = await me();
const handed = await finishHandoff(m).catch(() => null);
if (handed) return { me: m, ws: handed };
const u = (await get(`users/${m.id}`)) as { ws?: string } | null;
return { me: m, ws: u?.ws || (await createWorkspace(m)) };
})().catch((e) => { ready = null; throw e; });
return ready;
}
export function useSession(): { session: Session | null; error: unknown } {
const [state, setState] = useState<{ session: Session | null; error: unknown }>({ session: null, error: null });
useEffect(() => {
let dead = false;
session().then((s) => { if (!dead) setState({ session: s, error: null }); }, (e) => { if (!dead) setState({ session: null, error: e }); });
return () => { dead = true; };
}, []);
return state;
}
/** "Keep my time on all devices": makes a one-time hand-off code, then goes to the hosted email sign-in. */
export async function keepOnAllDevices(s: Session) {
const code = randomId(20);
await set(`ws/${s.ws}/handoffs/${code}`, { by: s.me.id, createdAt: NOW(), usedBy: "" });
localStorage.setItem(KEY, JSON.stringify({ guest: s.me.id, ws: s.ws, code, at: Date.now() } satisfies Handoff));
await signIn();
}
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