@hanzo/build 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/claims.d.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  /** The token's claims, or null when it is not a JWT. */
6
6
  export declare function claims(token: string | null): Record<string, unknown> | null;
7
7
  /**
8
- * Whether the token says its person administers `org`. Org admin, read off the
9
- * token's own `orgs` claim — never the reserved `admin` org, which is platform
10
- * authority and not what a builder decides anything by. The platform enforces
11
- * it again; this only decides which publish path is offered.
8
+ * Whether the token says its person administers `org`: owner or admin, read off
9
+ * the token's own `orgs` claim — never the reserved `admin` org, which is
10
+ * platform authority and not what a builder decides anything by. The platform
11
+ * enforces it again; this only decides which controls are offered.
12
12
  */
13
13
  export declare function administers(token: string | null, org: string | null): boolean;
package/lib/claims.js CHANGED
@@ -16,15 +16,20 @@ export function claims(token) {
16
16
  return null;
17
17
  }
18
18
  }
19
+ /** The coarse IAM roles that administer an org: its owner and its admins. */
20
+ const ADMIN = ['owner', 'admin'];
19
21
  /**
20
- * Whether the token says its person administers `org`. Org admin, read off the
21
- * token's own `orgs` claim — never the reserved `admin` org, which is platform
22
- * authority and not what a builder decides anything by. The platform enforces
23
- * it again; this only decides which publish path is offered.
22
+ * Whether the token says its person administers `org`: owner or admin, read off
23
+ * the token's own `orgs` claim — never the reserved `admin` org, which is
24
+ * platform authority and not what a builder decides anything by. The platform
25
+ * enforces it again; this only decides which controls are offered.
24
26
  */
25
27
  export function administers(token, org) {
26
28
  if (!org)
27
29
  return false;
28
30
  const set = claims(token)?.orgs;
29
- return (Array.isArray(set) ? set : []).some((r) => r?.org === org && r.role === 'admin');
31
+ return (Array.isArray(set) ? set : []).some((r) => {
32
+ const m = r;
33
+ return m?.org === org && ADMIN.includes(m.role);
34
+ });
30
35
  }
package/lib/host.d.ts CHANGED
@@ -37,7 +37,7 @@ export interface Host {
37
37
  chooseTheme?: (theme: Theme) => void;
38
38
  /** Who is signed in, or null. */
39
39
  person: Person | null;
40
- /** Whether the person administers `org`: an admin publishes to main, anyone else opens a review. */
40
+ /** Whether the person administers `org` (its owner or an admin): they publish to main and change its money; anyone else opens a review and reads. */
41
41
  admin: boolean;
42
42
  /** The address under the mount: '' | a session id | a slug | '-/codebases' | '-/projects' | '-/issues' | '-/artifacts' | '-/templates'. */
43
43
  path: string;
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  */
7
7
  import { SizableText, XStack, YStack } from '@hanzo/gui';
8
8
  import { CreditCard, Download, Plus, X } from '@hanzogui/lucide-icons-2';
9
- import { Button, Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
9
+ import { Button, Dialog, DialogContent, DialogTitle, toast } from '@hanzo/ui';
10
10
  import { useState } from 'react';
11
11
  import { cancel, current, detach, invoices, label, methods, money, pdf, reactivate, subscriptions, } from '../api/billing.js';
12
12
  import { useRead } from '../data.js';
@@ -30,13 +30,15 @@ export function Billing() {
30
30
  return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Billing" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s billing." })] }));
31
31
  }
32
32
  const plan = current(subs.value);
33
+ const empty = !cards.error && !cards.loading && cards.value.length === 0;
34
+ // A done act is a toast; a refused one stays on the page, beside what was tried.
33
35
  const act = async (what, done, after) => {
34
36
  setWorking(true);
35
37
  setNote('');
36
38
  try {
37
39
  await what();
38
40
  after();
39
- setNote(done);
41
+ toast.success(done);
40
42
  }
41
43
  catch (e) {
42
44
  setNote(e.message);
@@ -49,9 +51,9 @@ export function Billing() {
49
51
  ? subs.error.message
50
52
  : plan
51
53
  ? terms(plan)
52
- : 'No paid plan. Usage is paid from the balance.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsx(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : cards.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No card on file." }) })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [m.expires ? `Expires ${m.expires}` : '', m.default ? 'Default' : ''].filter(Boolean).join(' · ') || undefined, trailing: host.admin ? (_jsx(XStack, { render: "button", "aria-label": `Remove ${label(m)}`, p: "$1", onPress: () => void act(() => detach(t, m.id), `${label(m)} is removed`, cards.reload), children: _jsx(X, { size: 14 }) })) : undefined }, m.id))) })) }), _jsx(Group, { title: "Invoices", children: bills.error ? (_jsx(Soft, { children: bills.error.message })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: "Reading invoices\u2026" })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No invoices yet." }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Date" }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", style: { textAlign: 'right' }, children: "Total" }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: "Status" }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", style: { textAlign: 'right' }, children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `View invoice ${b.number || day(b.date)}`, onPress: () => setOpen(b), children: "View" }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: "Cancellation", children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? `Your plan ends on ${day(plan.ends)}` : 'Cancel plan', detail: plan.ending ? 'Keep it, and it renews as before.' : 'It runs to the end of the period you paid for, then ends.', trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(t, plan.id), 'Your plan renews as before', subs.reload), children: "Keep plan" })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: "Cancel" })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
54
+ : 'No paid plan. Usage is paid from the balance.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsxs(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "No card on file." }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? 'Add one to pay for a plan or a top-up. Adding it charges nothing.' : 'An org admin adds cards.' })] }), host.admin ? (_jsxs(Button, { onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " Add card"] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [m.expires ? `Expires ${m.expires}` : '', m.default ? 'Default' : ''].filter(Boolean).join(' · ') || undefined, trailing: host.admin ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Remove ${label(m)}`, disabled: working, onPress: () => void act(() => detach(t, m.id), `${label(m)} is removed`, cards.reload), children: "Remove" })) : undefined }, m.id))) })), !host.admin && cards.value.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin adds and removes cards." })) : null] }), _jsx(Group, { title: "Invoices", children: bills.error ? (_jsx(Soft, { children: bills.error.message })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: "Reading invoices\u2026" })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No invoices yet." }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Date" }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", style: { textAlign: 'right' }, children: "Total" }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: "Status" }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", style: { textAlign: 'right' }, children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `View invoice ${b.number || day(b.date)}`, onPress: () => setOpen(b), children: "View" }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: "Cancellation", children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? `Your plan ends on ${day(plan.ends)}` : 'Cancel plan', detail: plan.ending ? 'Keep it, and it renews as before.' : 'It runs to the end of the period you paid for, then ends.', trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(t, plan.id), 'Your plan renews as before', subs.reload), children: "Keep plan" })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: "Cancel" })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
53
55
  cards.reload();
54
- setNote(`${label(m)} is saved`);
56
+ toast.success(`${label(m)} is saved`);
55
57
  } }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(t, plan.id), `Your plan ends on ${day(plan.ends)}`, () => {
56
58
  setEnding(false);
57
59
  subs.reload();
@@ -101,5 +101,5 @@ export function AddCard({ open, onOpenChange, onSaved }) {
101
101
  const why = cfg.error ? cfg.error.message : !cfg.loading && !ready(p) ? 'This deployment takes no cards yet.' : '';
102
102
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Add a card" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [why ? (_jsx(SizableText, { size: "$2", color: "$soft", children: why })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { minH: 96, "aria-label": "Card details", children: [_jsx("div", { ref: setBox, style: { minHeight: 96 } }), !mounted && !note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Loading the card form\u2026" })) : null] }), _jsx(SizableText, { size: "$1", color: "$soft", children: p && p.environment !== 'production'
103
103
  ? 'Sandbox: use a test card. Nothing is charged.'
104
- : 'The card number goes to the payment processor, never to Hanzo.' })] })), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: !mounted || working, onPress: () => void add(), children: "Save card" })] })] })] }) }));
104
+ : 'The card number goes to the payment processor, never to Hanzo. Saving it charges nothing.' })] })), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: !mounted || working, onPress: () => void add(), children: "Save card" })] })] })] }) }));
105
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",