@hanzo/rooms 0.1.0

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.
Files changed (235) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +23 -0
  4. package/dist/Account.d.ts +8 -0
  5. package/dist/Account.js +58 -0
  6. package/dist/Beluga.d.ts +12 -0
  7. package/dist/Beluga.js +152 -0
  8. package/dist/Billing.d.ts +1 -0
  9. package/dist/Billing.js +31 -0
  10. package/dist/Board.d.ts +9 -0
  11. package/dist/Board.js +447 -0
  12. package/dist/Boxes.d.ts +1 -0
  13. package/dist/Boxes.js +173 -0
  14. package/dist/CalScheduler.d.ts +15 -0
  15. package/dist/CalScheduler.js +196 -0
  16. package/dist/Chat.d.ts +35 -0
  17. package/dist/Chat.js +1049 -0
  18. package/dist/Column.d.ts +15 -0
  19. package/dist/Column.js +88 -0
  20. package/dist/Contacts.d.ts +1 -0
  21. package/dist/Contacts.js +301 -0
  22. package/dist/Directory.d.ts +24 -0
  23. package/dist/Directory.js +356 -0
  24. package/dist/Drive.d.ts +2 -0
  25. package/dist/Drive.js +466 -0
  26. package/dist/Find.d.ts +7 -0
  27. package/dist/Find.js +82 -0
  28. package/dist/Guide.d.ts +6 -0
  29. package/dist/Guide.js +272 -0
  30. package/dist/Home.d.ts +39 -0
  31. package/dist/Home.js +306 -0
  32. package/dist/Inbox.d.ts +9 -0
  33. package/dist/Inbox.js +193 -0
  34. package/dist/Market.d.ts +3 -0
  35. package/dist/Market.js +149 -0
  36. package/dist/Me.d.ts +27 -0
  37. package/dist/Me.js +251 -0
  38. package/dist/Meet.d.ts +9 -0
  39. package/dist/Meet.js +370 -0
  40. package/dist/ModeMark.d.ts +8 -0
  41. package/dist/ModeMark.js +18 -0
  42. package/dist/OrgSwitcher.d.ts +31 -0
  43. package/dist/OrgSwitcher.js +120 -0
  44. package/dist/Orgs.d.ts +10 -0
  45. package/dist/Orgs.js +582 -0
  46. package/dist/Profile.d.ts +4 -0
  47. package/dist/Profile.js +67 -0
  48. package/dist/Prose.d.ts +5 -0
  49. package/dist/Prose.js +111 -0
  50. package/dist/ProviderMark.d.ts +17 -0
  51. package/dist/ProviderMark.js +277 -0
  52. package/dist/Recents.d.ts +10 -0
  53. package/dist/Recents.js +91 -0
  54. package/dist/RightPane.d.ts +13 -0
  55. package/dist/RightPane.js +426 -0
  56. package/dist/Room.d.ts +8 -0
  57. package/dist/Room.js +50 -0
  58. package/dist/Scene.d.ts +12 -0
  59. package/dist/Scene.js +105 -0
  60. package/dist/Settings.d.ts +46 -0
  61. package/dist/Settings.js +321 -0
  62. package/dist/Setup.d.ts +1 -0
  63. package/dist/Setup.js +36 -0
  64. package/dist/Share.d.ts +3 -0
  65. package/dist/Share.js +130 -0
  66. package/dist/Shared.d.ts +1 -0
  67. package/dist/Shared.js +113 -0
  68. package/dist/Shell.d.ts +63 -0
  69. package/dist/Shell.js +1225 -0
  70. package/dist/Start.d.ts +1 -0
  71. package/dist/Start.js +96 -0
  72. package/dist/Support.d.ts +14 -0
  73. package/dist/Support.js +122 -0
  74. package/dist/Talk.d.ts +4 -0
  75. package/dist/Talk.js +107 -0
  76. package/dist/Upgrade.d.ts +11 -0
  77. package/dist/Upgrade.js +75 -0
  78. package/dist/Usage.d.ts +1 -0
  79. package/dist/Usage.js +21 -0
  80. package/dist/Vibe.d.ts +2 -0
  81. package/dist/Vibe.js +188 -0
  82. package/dist/Work.d.ts +12 -0
  83. package/dist/Work.js +256 -0
  84. package/dist/bots/Bots.d.ts +2 -0
  85. package/dist/bots/Bots.js +456 -0
  86. package/dist/bots/Lights.d.ts +8 -0
  87. package/dist/bots/Lights.js +13 -0
  88. package/dist/bots/api.d.ts +120 -0
  89. package/dist/bots/api.js +75 -0
  90. package/dist/bots/face.d.ts +5 -0
  91. package/dist/bots/face.js +13 -0
  92. package/dist/bots/ink.d.ts +7 -0
  93. package/dist/bots/ink.js +29 -0
  94. package/dist/bots/kit.d.ts +113 -0
  95. package/dist/bots/kit.js +43 -0
  96. package/dist/bots/runtime.d.ts +4 -0
  97. package/dist/bots/runtime.js +326 -0
  98. package/dist/card.d.ts +52 -0
  99. package/dist/card.js +52 -0
  100. package/dist/cast.d.ts +143 -0
  101. package/dist/cast.js +251 -0
  102. package/dist/connect.d.ts +29 -0
  103. package/dist/connect.js +64 -0
  104. package/dist/contacts-store.d.ts +111 -0
  105. package/dist/contacts-store.js +276 -0
  106. package/dist/conversations.d.ts +54 -0
  107. package/dist/conversations.js +98 -0
  108. package/dist/copy.d.ts +23 -0
  109. package/dist/copy.js +39 -0
  110. package/dist/crew.d.ts +36 -0
  111. package/dist/crew.js +304 -0
  112. package/dist/dev.d.ts +3 -0
  113. package/dist/dev.js +102 -0
  114. package/dist/enso.d.ts +70 -0
  115. package/dist/enso.js +112 -0
  116. package/dist/failure.d.ts +57 -0
  117. package/dist/failure.js +143 -0
  118. package/dist/ground.d.ts +32 -0
  119. package/dist/ground.js +32 -0
  120. package/dist/host.d.ts +76 -0
  121. package/dist/host.js +76 -0
  122. package/dist/index.d.ts +14 -0
  123. package/dist/index.js +12 -0
  124. package/dist/kept.d.ts +5 -0
  125. package/dist/kept.js +27 -0
  126. package/dist/lib/account.d.ts +49 -0
  127. package/dist/lib/account.js +229 -0
  128. package/dist/lib/ai.d.ts +46 -0
  129. package/dist/lib/ai.js +140 -0
  130. package/dist/lib/api.d.ts +43 -0
  131. package/dist/lib/api.js +62 -0
  132. package/dist/lib/bare.d.ts +9 -0
  133. package/dist/lib/bare.js +7 -0
  134. package/dist/lib/coding.d.ts +105 -0
  135. package/dist/lib/coding.js +87 -0
  136. package/dist/lib/destination.d.ts +102 -0
  137. package/dist/lib/destination.js +260 -0
  138. package/dist/lib/durable.d.ts +66 -0
  139. package/dist/lib/durable.js +107 -0
  140. package/dist/lib/first.d.ts +11 -0
  141. package/dist/lib/first.js +23 -0
  142. package/dist/lib/home.d.ts +61 -0
  143. package/dist/lib/home.js +54 -0
  144. package/dist/lib/host.d.ts +104 -0
  145. package/dist/lib/host.js +132 -0
  146. package/dist/lib/hostname.d.ts +1 -0
  147. package/dist/lib/hostname.js +14 -0
  148. package/dist/lib/hydrated.d.ts +1 -0
  149. package/dist/lib/hydrated.js +20 -0
  150. package/dist/lib/integrations.d.ts +21 -0
  151. package/dist/lib/integrations.js +1 -0
  152. package/dist/lib/intent.d.ts +6 -0
  153. package/dist/lib/intent.js +71 -0
  154. package/dist/lib/invite.d.ts +155 -0
  155. package/dist/lib/invite.js +245 -0
  156. package/dist/lib/limits.d.ts +55 -0
  157. package/dist/lib/limits.js +119 -0
  158. package/dist/lib/list.d.ts +9 -0
  159. package/dist/lib/list.js +11 -0
  160. package/dist/lib/mark.d.ts +13 -0
  161. package/dist/lib/mark.js +85 -0
  162. package/dist/lib/mix.d.ts +22 -0
  163. package/dist/lib/mix.js +22 -0
  164. package/dist/lib/models.d.ts +27 -0
  165. package/dist/lib/models.js +106 -0
  166. package/dist/lib/org.d.ts +109 -0
  167. package/dist/lib/org.js +140 -0
  168. package/dist/lib/path.d.ts +9 -0
  169. package/dist/lib/path.js +29 -0
  170. package/dist/lib/plans.d.ts +193 -0
  171. package/dist/lib/plans.js +310 -0
  172. package/dist/lib/prose.d.ts +13 -0
  173. package/dist/lib/prose.js +62 -0
  174. package/dist/lib/reach.d.ts +17 -0
  175. package/dist/lib/reach.js +70 -0
  176. package/dist/lib/rooms.json +23 -0
  177. package/dist/lib/served.d.ts +20 -0
  178. package/dist/lib/served.js +92 -0
  179. package/dist/lib/session.d.ts +211 -0
  180. package/dist/lib/session.js +493 -0
  181. package/dist/lib/share.d.ts +131 -0
  182. package/dist/lib/share.js +216 -0
  183. package/dist/lib/signout.d.ts +14 -0
  184. package/dist/lib/signout.js +40 -0
  185. package/dist/lib/tags.d.ts +27 -0
  186. package/dist/lib/tags.js +46 -0
  187. package/dist/lib/tap.d.ts +17 -0
  188. package/dist/lib/tap.js +9 -0
  189. package/dist/lib/tier.d.ts +115 -0
  190. package/dist/lib/tier.js +207 -0
  191. package/dist/lib/tiers.d.ts +9 -0
  192. package/dist/lib/tiers.js +141 -0
  193. package/dist/lib/todo.d.ts +125 -0
  194. package/dist/lib/todo.js +379 -0
  195. package/dist/look.d.ts +1 -0
  196. package/dist/look.js +21 -0
  197. package/dist/meters.d.ts +8 -0
  198. package/dist/meters.js +31 -0
  199. package/dist/model.d.ts +4 -0
  200. package/dist/model.js +44 -0
  201. package/dist/network.d.ts +64 -0
  202. package/dist/network.js +216 -0
  203. package/dist/open.d.ts +81 -0
  204. package/dist/open.js +149 -0
  205. package/dist/pane.d.ts +92 -0
  206. package/dist/pane.js +231 -0
  207. package/dist/progress.d.ts +46 -0
  208. package/dist/progress.js +51 -0
  209. package/dist/recent.d.ts +67 -0
  210. package/dist/recent.js +161 -0
  211. package/dist/rooms.d.ts +131 -0
  212. package/dist/rooms.js +195 -0
  213. package/dist/roster.d.ts +28 -0
  214. package/dist/roster.js +70 -0
  215. package/dist/span.d.ts +14 -0
  216. package/dist/span.js +50 -0
  217. package/dist/speech.d.ts +44 -0
  218. package/dist/speech.js +229 -0
  219. package/dist/stars.d.ts +16 -0
  220. package/dist/stars.js +75 -0
  221. package/dist/tabs.d.ts +26 -0
  222. package/dist/tabs.js +25 -0
  223. package/dist/team.d.ts +11 -0
  224. package/dist/team.gen.d.ts +10 -0
  225. package/dist/team.gen.js +11 -0
  226. package/dist/team.js +30 -0
  227. package/dist/tools.d.ts +35 -0
  228. package/dist/tools.js +143 -0
  229. package/dist/tours.d.ts +24 -0
  230. package/dist/tours.js +311 -0
  231. package/dist/waiting.d.ts +15 -0
  232. package/dist/waiting.js +17 -0
  233. package/dist/where.d.ts +54 -0
  234. package/dist/where.js +14 -0
  235. package/package.json +108 -0
@@ -0,0 +1,207 @@
1
+ 'use client';
2
+ /**
3
+ * WHAT THIS ACCOUNT IS ON, AND WHAT IT MAY SPEND.
4
+ *
5
+ * Two reads, one question each. `GET /v1/billing/tier` names the plan and its
6
+ * term. `GET /v1/billing/balance` is the money: it reads the ledger the model
7
+ * gate admits against and every metered call is debited from, so its
8
+ * `available` is the one figure a person's spend is compared to. The tier's own
9
+ * `effectiveAvailable` is computed from commerce's store, which a top-up reaches
10
+ * and a usage debit does not, so it stays at the top-up after the ledger has
11
+ * spent it — and for the ecosystem orgs it is a $1,000 floor the gate never
12
+ * reads. The balance replaces it here; nothing adds the two.
13
+ *
14
+ * WHAT IT DOES NOT DO IS GUESS. The server answers 502 rather than 0 for a
15
+ * balance it could not read — "unknown is not broke" — and this keeps that
16
+ * distinction all the way to the screen: a failed read resolves to an error, an
17
+ * error renders as a sentence, and no path in this file produces a number that
18
+ * did not come from a response.
19
+ *
20
+ * The failure carries `status` because that is what `workspace/failure.ts`
21
+ * reads first, so a refusal here already speaks the vocabulary every room uses.
22
+ * A second Refusal class would be a second vocabulary for the same event.
23
+ */
24
+ import { useCallback, useEffect, useState } from 'react';
25
+ import { api } from './api.js';
26
+ import { bearer, org, orgs, scope } from './session.js';
27
+ import { list } from './list.js';
28
+ import { money } from './plans.js';
29
+ import { date } from './limits.js';
30
+ /** An error that names the status, for `say()` to turn into a sentence. */
31
+ const refusal = (status, message) => Object.assign(new Error(message), { status });
32
+ /** GETs `path` as whoever holds the bearer, in this browser's organization. */
33
+ async function read(path, signal) {
34
+ const res = await fetch(`${api()}${path}`, {
35
+ headers: scope(),
36
+ signal,
37
+ });
38
+ if (!res.ok) {
39
+ // The body is RFC 7807 here — `detail` is the server's own sentence, which
40
+ // is better than any rewording of it. Where there is none, the status is
41
+ // still a fact and `say()` knows what to do with it.
42
+ let detail = '';
43
+ try {
44
+ detail = (await res.json()).detail ?? '';
45
+ }
46
+ catch {
47
+ // an HTML error page proves nothing worth showing
48
+ }
49
+ throw refusal(res.status, detail || `Billing answered ${res.status}`);
50
+ }
51
+ return (await res.json());
52
+ }
53
+ /** Reads the tier and the balance for whoever holds the bearer. The requests
54
+ * name no subject — the credential is the scope — so there is no id to get wrong. */
55
+ export async function readTier(signal) {
56
+ const token = bearer();
57
+ if (!token)
58
+ throw refusal(401, 'Sign in to see your plan');
59
+ const [data, ledger] = await Promise.all([
60
+ read('/v1/billing/tier', signal),
61
+ read('/v1/billing/balance', signal),
62
+ ]);
63
+ data.balance = { ...data.balance, effectiveAvailable: ledger.available, account: ledger.account };
64
+ return data;
65
+ }
66
+ /**
67
+ * Whether a tier answer is a paid plan: billing names a paid plan the account's
68
+ * subscription is on, or serves it a paid tier — `pro`, which a plan recorded
69
+ * outside checkout confers, or `enterprise` (the platform org, a partner org).
70
+ * A trial nobody paid for is `starter` and names no plan.
71
+ */
72
+ export function paid(t) {
73
+ return Boolean((t?.plan && t.plan !== 'free') || t?.tier?.name === 'pro' || t?.tier?.name === 'enterprise');
74
+ }
75
+ /** The states in which a subscription is the plan the org is on. */
76
+ const LIVE = new Set(['active', 'trialing']);
77
+ /**
78
+ * The org's live subscription, or null where it holds none — being on no plan
79
+ * is an answer, and a refusal is not: that throws, the way the tier read does.
80
+ */
81
+ export async function readSubscription(signal) {
82
+ const got = await read('/v1/billing/subscriptions', signal);
83
+ return list(got?.subscriptions).find((s) => LIVE.has(s?.status) && typeof s?.plan?.price === 'number') ?? null;
84
+ }
85
+ /**
86
+ * The term as a sentence: "$200/month subscription, renews Oct 30, 2026". The
87
+ * price is per seat, so it is the seats times it; a cancelled term ends rather
88
+ * than renews.
89
+ */
90
+ export function renewal(s) {
91
+ const each = money((s.plan.price * Math.max(1, s.quantity || 1)) / 100);
92
+ const when = date(s.currentPeriodEnd);
93
+ return `${each}/${s.plan.interval || 'month'} subscription${when ? `, ${s.cancelAtPeriodEnd ? 'ends' : 'renews'} ${when}` : ''}`;
94
+ }
95
+ /** The live subscription, held: null while unknown, refused, or on no plan. */
96
+ export function useSubscription(enabled = true, org = null) {
97
+ const [sub, setSub] = useState(null);
98
+ useEffect(() => {
99
+ if (!enabled)
100
+ return;
101
+ const stop = new AbortController();
102
+ readSubscription(stop.signal)
103
+ .then((got) => {
104
+ if (!stop.signal.aborted)
105
+ setSub(got);
106
+ })
107
+ .catch(() => {
108
+ if (!stop.signal.aborted)
109
+ setSub(null);
110
+ });
111
+ return () => stop.abort();
112
+ }, [enabled, org]);
113
+ return sub;
114
+ }
115
+ /** How long a sign-in waits on billing before it offers the plans. */
116
+ const PATIENCE = 5000;
117
+ /** `signal`, cut off at PATIENCE, on a browser that can combine the two; else PATIENCE alone. */
118
+ function within(signal) {
119
+ const limit = typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(PATIENCE) : undefined;
120
+ if (!signal || !limit)
121
+ return limit ?? signal;
122
+ return typeof AbortSignal.any === 'function' ? AbortSignal.any([signal, limit]) : limit;
123
+ }
124
+ /**
125
+ * Whether this account pays for a plan where it works: in the organization it
126
+ * acts in, or, before it has picked one, in any organization its token names.
127
+ * A read that fails or takes longer than PATIENCE counts as no plan, so the
128
+ * plans are offered rather than the app withheld. At most ten organizations
129
+ * are asked.
130
+ */
131
+ export async function paying(signal) {
132
+ const token = bearer();
133
+ if (!token)
134
+ return false;
135
+ const acting = org();
136
+ const where = acting ? [acting] : orgs().slice(0, 10);
137
+ const reads = await Promise.allSettled((where.length ? where : [null]).map(async (o) => {
138
+ const res = await fetch(`${api()}/v1/billing/tier`, {
139
+ headers: { Authorization: `Bearer ${token}`, ...(o ? { 'X-Org-Id': o } : {}) },
140
+ signal: within(signal),
141
+ });
142
+ return res.ok ? (await res.json()) : null;
143
+ }));
144
+ return reads.some((r) => r.status === 'fulfilled' && paid(r.value));
145
+ }
146
+ /**
147
+ * The tier, held. `null` while unknown — which is BOTH "still asking" and
148
+ * "asked and was refused", so `wrong` is kept beside it rather than swallowed:
149
+ * a row cannot choose between saying nothing and saying why without it.
150
+ */
151
+ export function useTier(enabled = true, org = null) {
152
+ const [tier, setTier] = useState(null);
153
+ const [wrong, setWrong] = useState(null);
154
+ const [nonce, setNonce] = useState(0);
155
+ useEffect(() => {
156
+ if (!enabled)
157
+ return;
158
+ const stop = new AbortController();
159
+ setWrong(null);
160
+ readTier(stop.signal)
161
+ .then((got) => {
162
+ if (!stop.signal.aborted)
163
+ setTier(got);
164
+ })
165
+ .catch((e) => {
166
+ if (!stop.signal.aborted) {
167
+ setTier(null);
168
+ setWrong(e);
169
+ }
170
+ });
171
+ return () => stop.abort();
172
+ }, [enabled, nonce, org]);
173
+ return { tier, wrong, reload: useCallback(() => setNonce((n) => n + 1), []) };
174
+ }
175
+ /**
176
+ * Whether an organization is its owner's personal one, off IAM's own row.
177
+ *
178
+ * The token's `orgs` claim names an organization and the reader's role in it
179
+ * and says nothing of its kind; IAM keeps that on the row (`isPersonal`), and
180
+ * `GET /v1/iam/organizations` answers the rows the caller belongs to. `null`
181
+ * until it is answered for this organization. A refusal reads as `false`, so a
182
+ * room that needs a plan goes on asking for one.
183
+ */
184
+ export function usePersonal(org, enabled = true) {
185
+ const [read, setRead] = useState(null);
186
+ useEffect(() => {
187
+ if (!enabled || !org)
188
+ return;
189
+ const stop = new AbortController();
190
+ fetch(`${api()}/v1/iam/organizations?${new URLSearchParams({ q: org })}`, {
191
+ headers: scope(),
192
+ signal: stop.signal,
193
+ })
194
+ .then((res) => (res.ok ? res.json() : null))
195
+ .then((body) => {
196
+ const row = list(body?.organizations).find((o) => o?.name === org);
197
+ if (!stop.signal.aborted)
198
+ setRead({ org, personal: Boolean(row?.isPersonal) });
199
+ })
200
+ .catch(() => {
201
+ if (!stop.signal.aborted)
202
+ setRead({ org, personal: false });
203
+ });
204
+ return () => stop.abort();
205
+ }, [enabled, org]);
206
+ return read && read.org === org ? read.personal : null;
207
+ }
@@ -0,0 +1,9 @@
1
+ export type Tier = 'alpha' | 'beta';
2
+ export declare function useTiers(): Set<Tier>;
3
+ /** The tiers this reader has chosen to hide, and the toggle that sets them.
4
+ *
5
+ * Entitlement is one question (`useTiers`), what you want to see is another. A
6
+ * reader who may see a beta room can still put it away — the choice is theirs and
7
+ * lives with them, in this browser, never on the server. It starts empty: you see
8
+ * what you are entitled to until you say otherwise. */
9
+ export declare function useHidden(): [Set<Tier>, (tier: Tier) => void];
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+ // Which preview tiers this identity may see.
3
+ //
4
+ // The rail offers Chat and Dev to everyone. The rooms after them — Inbox,
5
+ // Agents, Tasks, Drive — are behind a tier, and a tier opens for exactly three
6
+ // reasons, asked here and nowhere else:
7
+ //
8
+ // 1. A SUPERADMIN sees every tier. That is IAM's answer, read off the signed
9
+ // membership set: its first entry is the org the account lives in, and a
10
+ // SuperAdmin lives in the reserved `admin` org (hanzoai/authz Claims.Sudo).
11
+ // A membership of `admin` held from another org confers nothing, and a
12
+ // per-org `isAdmin` is a different, org-scoped question.
13
+ //
14
+ // 2. An ALPHA or BETA org sees its own tier. Whether an org is in a cohort is
15
+ // a targeting decision that lives on the cloud, so it is asked of the cloud:
16
+ // POST /v1/flags answers the feature flags in force for this identity, and
17
+ // `tier.alpha` / `tier.beta` open their rooms.
18
+ //
19
+ // 3. A LOCALHOST BUILD sees every tier, because a room nobody can open is a
20
+ // room nobody can work on. The check is the ORIGIN the page was served
21
+ // from, not an env var and not a build flag: a deployed host can never
22
+ // satisfy it however it is configured, so this cannot leak an unfinished
23
+ // room into production by being set wrong somewhere.
24
+ //
25
+ // 4. Nothing else. Signed out, still loading, or on any error the answer is the
26
+ // empty set: the rail shows Chat and Dev and nothing more. Failing closed is
27
+ // the point — a stranger meets the product the landing page describes, and a
28
+ // flags outage cannot leak an unfinished room into it.
29
+ //
30
+ // The per-account SETTING that hides a tier the reader is entitled to lives with
31
+ // the reader, not here — see `useHidden`. This answers what you MAY see; that
32
+ // answers what you have chosen to.
33
+ import { useEffect, useState } from 'react';
34
+ import { useIam, useOrganizations } from '@hanzo/iam/react';
35
+ import { api } from './api.js';
36
+ import { superAdmin as isSuperAdmin } from './session.js';
37
+ /** Every tier, for the callers that are entitled to all of them. */
38
+ const ALL = ['alpha', 'beta'];
39
+ /**
40
+ * Whether this page is being served from a developer's own machine.
41
+ *
42
+ * The ORIGIN, because it is the one fact a deployment cannot accidentally
43
+ * acquire: no environment variable, no build flag and no misconfigured host
44
+ * turns api.hanzo.ai into localhost. Loopback by both names, and the private
45
+ * ranges a second machine on the same desk is reached by.
46
+ *
47
+ * READ AFTER MOUNT, never during the render that hydrates. The server has no
48
+ * origin to look at and answers false, so a client that answered true on its
49
+ * first pass rendered a different rail than the HTML it was hydrating and React
50
+ * threw the tree away. It is a fact about the browser, and a fact only the
51
+ * browser holds arrives in an effect.
52
+ */
53
+ const local = () => {
54
+ if (typeof window === 'undefined')
55
+ return false;
56
+ const host = window.location.hostname;
57
+ return (host === 'localhost' ||
58
+ host === '127.0.0.1' ||
59
+ host === '[::1]' ||
60
+ host.endsWith('.local') ||
61
+ /^10\./.test(host) ||
62
+ /^192\.168\./.test(host));
63
+ };
64
+ export function useTiers() {
65
+ const { user, accessToken } = useIam();
66
+ const { organizations } = useOrganizations();
67
+ const [flags, setFlags] = useState({});
68
+ const [here, setHere] = useState(false);
69
+ const [superAdmin, setSuperAdmin] = useState(false);
70
+ useEffect(() => setHere(local()), []);
71
+ // IAM's answer off the token's own `orgs` claim (lib/auth/session), read after
72
+ // mount like `local` so the first render matches the HTML it hydrates.
73
+ useEffect(() => setSuperAdmin(isSuperAdmin()), [accessToken]);
74
+ const id = user?.id ?? user?.name ?? '';
75
+ useEffect(() => {
76
+ // A SuperAdmin needs no answer from the server, and a stranger has none to
77
+ // ask for. Only a signed-in, non-admin identity consults /v1/flags.
78
+ if (!id || superAdmin) {
79
+ setFlags({});
80
+ return;
81
+ }
82
+ const ac = new AbortController();
83
+ // The PostHog-shaped evaluate: a distinct_id and the person's org, answered
84
+ // with `{ featureFlags: { … } }`. The session cookie carries the principal;
85
+ // with none the cloud answers 403 and this falls to the empty set, which is
86
+ // the signed-out room — the safe direction.
87
+ fetch(`${api()}/v1/flags`, {
88
+ method: 'POST',
89
+ credentials: 'include',
90
+ headers: { 'content-type': 'application/json' },
91
+ body: JSON.stringify({ distinct_id: id, person_properties: { org: organizations[0]?.name } }),
92
+ signal: ac.signal,
93
+ })
94
+ .then((r) => (r.ok ? r.json() : null))
95
+ .then((body) => setFlags(body?.featureFlags ?? {}))
96
+ .catch(() => setFlags({}));
97
+ return () => ac.abort();
98
+ }, [id, superAdmin, organizations]);
99
+ // A platform admin, or a page served off a developer's own machine. Both see
100
+ // every tier for the same reason: a room nobody can open is a room nobody can
101
+ // work on.
102
+ if (superAdmin || here)
103
+ return new Set(ALL);
104
+ const tiers = new Set();
105
+ if (flags['tier.alpha'] === true)
106
+ tiers.add('alpha');
107
+ if (flags['tier.beta'] === true)
108
+ tiers.add('beta');
109
+ return tiers;
110
+ }
111
+ const HIDDEN_KEY = 'hanzo.hidden-tiers';
112
+ /** The tiers this reader has chosen to hide, and the toggle that sets them.
113
+ *
114
+ * Entitlement is one question (`useTiers`), what you want to see is another. A
115
+ * reader who may see a beta room can still put it away — the choice is theirs and
116
+ * lives with them, in this browser, never on the server. It starts empty: you see
117
+ * what you are entitled to until you say otherwise. */
118
+ export function useHidden() {
119
+ const [hidden, setHidden] = useState(new Set());
120
+ useEffect(() => {
121
+ try {
122
+ const saved = JSON.parse(localStorage.getItem(HIDDEN_KEY) || '[]');
123
+ setHidden(new Set(saved));
124
+ }
125
+ catch {
126
+ setHidden(new Set());
127
+ }
128
+ }, []);
129
+ const toggle = (tier) => setHidden((was) => {
130
+ const next = new Set(was);
131
+ next.has(tier) ? next.delete(tier) : next.add(tier);
132
+ try {
133
+ localStorage.setItem(HIDDEN_KEY, JSON.stringify([...next]));
134
+ }
135
+ catch {
136
+ /* a browser that refuses storage keeps the choice for this session */
137
+ }
138
+ return next;
139
+ });
140
+ return [hidden, toggle];
141
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The board's store: `/v1/todo` with resilient local cache fallback.
3
+ *
4
+ * A card IS a forge issue — the platform's own words: "the column is a LABEL on
5
+ * the forge, so the board and the forge web UI are the same object seen twice:
6
+ * relabelling in either moves the card in both." So nothing here invents a
7
+ * parallel place to keep work; it reads and writes the one the org already has.
8
+ *
9
+ * WHAT THE STORE HOLDS, and therefore what this board can honestly draw: a
10
+ * title, a description, a column, a priority, an assignee, labels, and a start
11
+ * and due date. Checklists ride in the description as markdown task items,
12
+ * which is not a workaround — it is how a forge issue has always spelled a
13
+ * checklist, so one written here renders in the forge web UI and one written
14
+ * there arrives back on the card.
15
+ */
16
+ import type { AiClient } from '@hanzo/ai';
17
+ /** One card, as the board reads it. */
18
+ export interface Card {
19
+ id: string;
20
+ /** "<key>#<number>" — the board and the number on it, joined. */
21
+ identifier: string;
22
+ projectKey: string;
23
+ number: number;
24
+ kind: string;
25
+ source: string;
26
+ repo?: string;
27
+ title: string;
28
+ description?: string;
29
+ /** The column. One of {@link COLUMNS}. */
30
+ status: string;
31
+ priority: string;
32
+ assignee?: string;
33
+ labels: string[];
34
+ /** Unix seconds; 0/absent means unscheduled. */
35
+ startAt?: number;
36
+ dueAt?: number;
37
+ createdAt: number;
38
+ updatedAt: number;
39
+ }
40
+ /**
41
+ * THE COLUMNS, and they are the platform's own closed set rather than a list a
42
+ * reader types. `newIssue` names them: "backlog, todo, in_progress, done or
43
+ * canceled". A column that is not one of these cannot be moved to, because the
44
+ * move is a relabel and the forge would not know the label.
45
+ */
46
+ export declare const COLUMNS: readonly [{
47
+ readonly id: 'backlog';
48
+ readonly label: 'Backlog';
49
+ }, {
50
+ readonly id: 'todo';
51
+ readonly label: 'To do';
52
+ }, {
53
+ readonly id: 'in_progress';
54
+ readonly label: 'Doing';
55
+ }, {
56
+ readonly id: 'done';
57
+ readonly label: 'Done';
58
+ }, {
59
+ readonly id: 'canceled';
60
+ readonly label: 'Canceled';
61
+ }];
62
+ /** The priorities the store accepts, loudest first. */
63
+ export declare const PRIORITIES: readonly ['urgent', 'high', 'medium', 'low', 'none'];
64
+ export declare const DEFAULT_CARDS: Card[];
65
+ /** Every card on the org's board, or one project's when `key` is given. */
66
+ export declare function board(client?: AiClient | null, params?: {
67
+ key?: string;
68
+ status?: string;
69
+ label?: string;
70
+ scheduled?: boolean;
71
+ }, signal?: AbortSignal): Promise<Card[]>;
72
+ /** The boards themselves — a project is a repository on the forge. */
73
+ export declare function projects(client?: AiClient | null, signal?: AbortSignal): Promise<{
74
+ key: string;
75
+ name?: string;
76
+ }[]>;
77
+ /** Opens a card in a column. */
78
+ export declare function open(client: AiClient | null | undefined, key: string, card: {
79
+ title: string;
80
+ description?: string;
81
+ status?: string;
82
+ priority?: string;
83
+ labels?: string[];
84
+ }, signal?: AbortSignal): Promise<Card>;
85
+ /**
86
+ * Edits a card. Absent fields are left alone.
87
+ */
88
+ export declare function edit(client: AiClient | null | undefined, key: string, num: number, patch: {
89
+ title?: string;
90
+ description?: string;
91
+ status?: string;
92
+ priority?: string;
93
+ assignee?: string;
94
+ labels?: string[];
95
+ startAt?: number;
96
+ dueAt?: number;
97
+ }, signal?: AbortSignal): Promise<Card>;
98
+ /** Takes the card — the store's own verb for who is doing the work. */
99
+ export declare function claim(client: AiClient | null | undefined, key: string, num: number, signal?: AbortSignal): Promise<Card>;
100
+ /** One line of a description that is a task. */
101
+ export interface Step {
102
+ done: boolean;
103
+ text: string;
104
+ /** Which line it is, so ticking one rewrites that line and no other. */
105
+ line: number;
106
+ }
107
+ /**
108
+ * The task items in a description.
109
+ */
110
+ export declare function steps(description?: string): Step[];
111
+ /** The description with one step's box flipped, and every other line untouched. */
112
+ export declare function tick(description: string, line: number, done: boolean): string;
113
+ /** The description with one more step at the end of its list. */
114
+ export declare function addStep(description: string, text: string): string;
115
+ export interface Sift {
116
+ holder?: string;
117
+ by?: 'person' | 'agent';
118
+ state?: 'done' | 'open';
119
+ due?: 'none' | 'late' | 'day' | 'week' | 'month';
120
+ labels?: string[];
121
+ active?: number;
122
+ text?: string;
123
+ }
124
+ export declare const SETTLED: Set<string>;
125
+ export declare function sift(cards: Card[], want: Sift, me?: string, agents?: Set<string>): Card[];