@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,229 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useState, useEffect, useMemo } from 'react';
4
+ import { useIam, useIamIdentity, useIamToken, useOrganizations } from '@hanzo/iam/react';
5
+ import { iam } from './api.js';
6
+ const AccountContext = createContext(undefined);
7
+ function mapUser(u) {
8
+ return {
9
+ id: u.id ?? u.name,
10
+ name: u.displayName || u.name,
11
+ email: u.email ?? '',
12
+ avatar: u.avatar,
13
+ phone: u.phone,
14
+ bio: u.bio,
15
+ location: u.location,
16
+ // IAM calls it `homepage`; this app calls it a website. One name each side,
17
+ // translated at the seam rather than in every screen.
18
+ website: u.homepage,
19
+ joinedDate: u.createdTime,
20
+ };
21
+ }
22
+ /** An organization as the signed `orgs` claim names it: a name and a display name (@hanzo/iam `useOrganizations`). */
23
+ function mapOrg(o, role) {
24
+ return {
25
+ id: o.name,
26
+ name: o.displayName || o.name,
27
+ role: role ?? 'member',
28
+ members: [],
29
+ };
30
+ }
31
+ // ── The write ──────────────────────────────────────────────────────────────
32
+ //
33
+ // IAM owns the person. There is no second store to keep a name or a picture in,
34
+ // so a profile edit is a write to IAM and this page has succeeded when that has.
35
+ //
36
+ // `PUT /v1/iam/account` IS THE DOOR, and it is the only one a person has. The
37
+ // request names no target — the subject is whoever holds the token — so no
38
+ // shape of it reaches somebody else's record, and that property is exactly why
39
+ // a regular person may send it. The typed CRUD write beside it binds a WHOLE
40
+ // user from the body, which is an act only an administrator may perform (a
41
+ // full-row write is how a reader would carry `isAdmin` on their own row), so
42
+ // reaching for that one made changing a picture a privilege: the read
43
+ // succeeded, the write was refused, and the picture stayed on screen over a row
44
+ // that had not changed.
45
+ //
46
+ // A PATCH, NOT A ROW. Every field is optional: one omitted keeps the value it
47
+ // had and one sent empty clears it. So the panel that saves a picture cannot
48
+ // blank a biography it never drew, and two screens saving at once do not undo
49
+ // each other — which is also why there is nothing to read first.
50
+ //
51
+ // FOUR FIELDS, because four is what IAM lets a person write about themselves.
52
+ // An email address and a phone number are LOGIN IDENTIFIERS and second-factor
53
+ // destinations: moving where a code is delivered is a credential act, safe only
54
+ // once the new address has proved it can receive one. That is the verification
55
+ // flow's job, not a profile save's, so they are not here and this app does not
56
+ // draw a control that pretends otherwise.
57
+ /** IAM's self-scoped account door, at the host's IAM. */
58
+ const account = () => `${iam()}/v1/iam/account`;
59
+ // ── The read ───────────────────────────────────────────────────────────────
60
+ //
61
+ // THE SAME DOOR, and that is the point: the row this page edits is the row it
62
+ // draws. `PUT /v1/iam/account` names no target and neither does the GET — the
63
+ // subject is whoever holds the token — so a person reads their own record and
64
+ // nobody else's, which is why a regular person may ask.
65
+ //
66
+ // It reads the ROW because a token does not carry one. `IamProvider` sets its
67
+ // `user` to the OIDC userinfo response, which answers who you are (sub, name,
68
+ // email, picture) and nothing about the record: no avatar under that name, no
69
+ // bio, no homepage, no phone, no joined date, and no id. Read as a row it is
70
+ // a person with no picture and empty fields — the profile page drew "Add a
71
+ // picture" over an account that had one.
72
+ /** IAM's answer, whether it wraps the row or is the row. */
73
+ function row(said) {
74
+ if (!said || typeof said !== 'object')
75
+ return null;
76
+ const body = said;
77
+ if (body.status === 'error')
78
+ return null;
79
+ const it = (body.data ?? said);
80
+ return it && typeof it === 'object' ? it : null;
81
+ }
82
+ async function fetchAccount(token) {
83
+ // The bearer is the whole proof, so the request carries no cookie — the same
84
+ // reason the write does not.
85
+ const res = await fetch(account(), { headers: { Authorization: `Bearer ${token}` } });
86
+ if (!res.ok)
87
+ return null;
88
+ return row(await res.json().catch(() => null));
89
+ }
90
+ /** IAM's names for what this app calls a profile. */
91
+ function fields(patch) {
92
+ const body = {};
93
+ if (patch.name !== undefined)
94
+ body.displayName = patch.name;
95
+ if (patch.bio !== undefined)
96
+ body.bio = patch.bio;
97
+ // IAM calls it `homepage`; this app calls it a website. One name each side,
98
+ // translated at the seam rather than in every screen.
99
+ if (patch.website !== undefined)
100
+ body.homepage = patch.website;
101
+ if (patch.avatar !== undefined)
102
+ body.avatar = patch.avatar;
103
+ return body;
104
+ }
105
+ /** What IAM said went wrong. A refused image names its own limit, and that
106
+ * sentence is the useful half of the answer. */
107
+ function reason(said, status) {
108
+ return said?.msg || `Could not save (${status}).`;
109
+ }
110
+ async function store(token, patch) {
111
+ const body = fields(patch);
112
+ // Nothing IAM stores changed, so there is nothing to ask it.
113
+ if (Object.keys(body).length === 0)
114
+ return;
115
+ // THE BEARER IS THE WHOLE PROOF, so the request carries no cookie. IAM opens
116
+ // this path to a browser on the strength of a token in a header — asking for
117
+ // credentials as well means the browser demands an
118
+ // `Access-Control-Allow-Credentials` on the answer, and where the origin does
119
+ // not get one the save fails as a network error with no status and no
120
+ // sentence to show the reader.
121
+ const res = await fetch(account(), {
122
+ method: 'PUT',
123
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
124
+ body: JSON.stringify(body),
125
+ });
126
+ const said = await res.json().catch(() => null);
127
+ if (!res.ok || said?.status === 'error')
128
+ throw new Error(reason(said, res.status));
129
+ }
130
+ export const AccountProvider = ({ children }) => {
131
+ const { user: iamUser, isLoading: authLoading } = useIam();
132
+ const { token } = useIamToken();
133
+ const { organizations: iamOrgs, roles, currentOrgId, switchOrg, isLoading: orgsLoading } = useOrganizations();
134
+ const identity = useIamIdentity();
135
+ // Held against the token it was read with. A row is about ONE person, so a
136
+ // token change must not leave the previous account's picture on screen while
137
+ // the next one is in flight — reading it back through the token is what makes
138
+ // that impossible rather than merely brief.
139
+ const [held, setHeld] = useState(null);
140
+ const account = held && token && held.of === token ? held.row : null;
141
+ const [userOverrides, setUserOverrides] = useState({});
142
+ const [orgOverrides, setOrgOverrides] = useState({});
143
+ useEffect(() => {
144
+ setUserOverrides({});
145
+ setOrgOverrides({});
146
+ }, [iamUser?.id, iamUser?.name]);
147
+ useEffect(() => {
148
+ if (!token)
149
+ return;
150
+ let live = true;
151
+ fetchAccount(token)
152
+ .then((row) => {
153
+ if (live)
154
+ setHeld({ of: token, row });
155
+ })
156
+ // A record that will not load leaves the token's identity on screen,
157
+ // which is what this page had before it asked for the row at all.
158
+ .catch(() => { });
159
+ return () => {
160
+ live = false;
161
+ };
162
+ }, [token]);
163
+ const user = useMemo(() => {
164
+ if (!iamUser && !identity)
165
+ return null;
166
+ // The row where IAM answered with one; otherwise the identity the token
167
+ // already carries, resolved by the one resolver that knows every key IAM
168
+ // spells a name and a picture under.
169
+ const base = account
170
+ ? mapUser(account)
171
+ : {
172
+ id: iamUser?.id ?? identity?.name ?? '',
173
+ name: identity?.name ?? '',
174
+ email: identity?.email ?? '',
175
+ avatar: identity?.avatarUrl ?? undefined,
176
+ };
177
+ return { ...base, ...userOverrides };
178
+ }, [account, iamUser, identity, userOverrides]);
179
+ const organizations = useMemo(() => iamOrgs.map((o) => {
180
+ const base = mapOrg(o, roles[o.name]);
181
+ return { ...base, ...orgOverrides[base.id] };
182
+ }), [iamOrgs, roles, orgOverrides]);
183
+ const currentOrganization = useMemo(() => organizations.find((o) => o.id === currentOrgId) ?? organizations[0] ?? null, [organizations, currentOrgId]);
184
+ const setUser = (next) => setUserOverrides(next);
185
+ // The override lands FIRST so the picture the reader just picked is on screen
186
+ // while the request is in flight, and is rolled back where the write is
187
+ // refused — a preview that survives a failure is the page lying about what it
188
+ // has.
189
+ const updateUserProfile = async (userData) => {
190
+ const before = userOverrides;
191
+ setUserOverrides((prev) => ({ ...prev, ...userData }));
192
+ if (!iamUser || !token) {
193
+ setUserOverrides(before);
194
+ throw new Error('Sign in to change your profile.');
195
+ }
196
+ try {
197
+ await store(token, userData);
198
+ }
199
+ catch (e) {
200
+ setUserOverrides(before);
201
+ throw e;
202
+ }
203
+ };
204
+ const updateOrganization = (orgData) => {
205
+ if (!currentOrganization)
206
+ return;
207
+ setOrgOverrides((prev) => ({
208
+ ...prev,
209
+ [currentOrganization.id]: { ...prev[currentOrganization.id], ...orgData },
210
+ }));
211
+ };
212
+ return (_jsx(AccountContext.Provider, { value: {
213
+ user,
214
+ setUser,
215
+ organizations,
216
+ currentOrganization,
217
+ isLoading: authLoading || orgsLoading,
218
+ switchOrganization: switchOrg,
219
+ updateUserProfile,
220
+ updateOrganization,
221
+ }, children: children }));
222
+ };
223
+ export const useAccount = () => {
224
+ const context = useContext(AccountContext);
225
+ if (context === undefined) {
226
+ throw new Error('useAccount must be used within an AccountProvider');
227
+ }
228
+ return context;
229
+ };
@@ -0,0 +1,46 @@
1
+ import { type AiClient } from '@hanzo/ai';
2
+ /**
3
+ * The tenant a signed-out turn is recorded against: the publishable key IAM
4
+ * mints, the same one this page beacons analytics with. It names an org and no
5
+ * person, so it is safe in a bundle by construction, and a free turn attributes
6
+ * to the org that published the page rather than to nobody.
7
+ *
8
+ * It is a KEY, not a token, and the distinction decides which route is called.
9
+ * `laneFor` in the SDK asks whether a bearer is present: with one it sends the
10
+ * metered `/v1/chat/completions`, without one it sends `/v1/chat/public` — the
11
+ * credential-less lane, which reads neither the model nor the org off the
12
+ * request, so a signed-out visitor cannot name a paid model or another tenant
13
+ * even by trying. Passing this as `token` would therefore select the metered
14
+ * route and be refused; it is passed as `publishableKey` (SDK >= 0.6.13).
15
+ */
16
+ /** The gateway's own free pool: enso-free when logged out. */
17
+ export declare const FREE = "enso-free";
18
+ /** Enso routes a signed-in turn: enso-auto when logged in. */
19
+ export declare const ENSO = "enso-auto";
20
+ /**
21
+ * The model that will actually ANSWER, given what the gateway serves.
22
+ */
23
+ export declare function served(models: {
24
+ id: string;
25
+ }[], prefer: string): string;
26
+ /**
27
+ * The gateway's address in the shape `createAiClient` takes it.
28
+ *
29
+ * The address itself is `api()` — one derivation, in `lib/hanzo/api.ts`, which
30
+ * every plain `fetch` in the app also reads. This working correctly here and
31
+ * nowhere else is what left eight other modules pointed at production while the
32
+ * dev proxy sat unused beside them.
33
+ */
34
+ export declare const base: () => {
35
+ baseUrl?: string;
36
+ };
37
+ export interface Ai {
38
+ client: AiClient | null;
39
+ model: string;
40
+ /** Answering free, so the turn is data-shared and owes the notice. */
41
+ free: boolean;
42
+ /** Mounted client-side. The static export always prerenders the signed-out
43
+ * page, so nothing interactive may render until this is true. */
44
+ ready: boolean;
45
+ }
46
+ export declare function useAi(): Ai;
package/dist/lib/ai.js ADDED
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+ // Who answers a chat turn on this site, and on whose account.
3
+ //
4
+ // Signed in — the visitor's own IAM token. Enso routes the turn and it meters to
5
+ // their account like every other turn they run.
6
+ //
7
+ // Signed out — the FREE POOL, anonymously. There is no account to bill and none
8
+ // is needed: a free turn spends no balance, and what it costs instead is stated
9
+ // in the consent block the composer asks before the first send. A widget key, if
10
+ // one is configured, rides along so the turn attributes to the org that minted
11
+ // it; without one the turn is simply anonymous. Either way the client EXISTS,
12
+ // because a chat you cannot send is not a chat.
13
+ //
14
+ // This used to answer `client: null` with no key, which is what put "Sign in to
15
+ // send" under the composer — a wall in front of a lane that is free by design.
16
+ //
17
+ // So `client` is NEVER null now, and the type keeps saying `AiClient | null`
18
+ // because a caller that still branches on it stays correct. Nothing in this
19
+ // file returns null any more; if you are reading the type looking for the
20
+ // signed-out case, it is not there.
21
+ import { useEffect, useMemo, useState } from 'react';
22
+ import { useHydrated } from './hydrated.js';
23
+ import { api } from './api.js';
24
+ import { useIam } from '@hanzo/iam/react';
25
+ import { ORG, bearer, hasSession, org } from './session.js';
26
+ import { createAiClient } from '@hanzo/ai';
27
+ import { observed } from './served.js';
28
+ import { where } from '../where.js';
29
+ /**
30
+ * The tenant a signed-out turn is recorded against: the publishable key IAM
31
+ * mints, the same one this page beacons analytics with. It names an org and no
32
+ * person, so it is safe in a bundle by construction, and a free turn attributes
33
+ * to the org that published the page rather than to nobody.
34
+ *
35
+ * It is a KEY, not a token, and the distinction decides which route is called.
36
+ * `laneFor` in the SDK asks whether a bearer is present: with one it sends the
37
+ * metered `/v1/chat/completions`, without one it sends `/v1/chat/public` — the
38
+ * credential-less lane, which reads neither the model nor the org off the
39
+ * request, so a signed-out visitor cannot name a paid model or another tenant
40
+ * even by trying. Passing this as `token` would therefore select the metered
41
+ * route and be refused; it is passed as `publishableKey` (SDK >= 0.6.13).
42
+ */
43
+ /** The gateway's own free pool: enso-free when logged out. */
44
+ export const FREE = 'enso-free';
45
+ /** Enso routes a signed-in turn: enso-auto when logged in. */
46
+ export const ENSO = 'enso-auto';
47
+ /**
48
+ * The model that will actually ANSWER, given what the gateway serves.
49
+ */
50
+ export function served(models, prefer) {
51
+ const cleanPrefer = prefer === 'openrouter/auto' || prefer?.includes('openrouter') ? 'enso-auto' : prefer;
52
+ if (models.length === 0)
53
+ return cleanPrefer || 'enso-auto';
54
+ if (cleanPrefer && models.some((m) => m.id === cleanPrefer))
55
+ return cleanPrefer;
56
+ if (cleanPrefer === 'enso-free' || cleanPrefer === 'free' || cleanPrefer.includes('free')) {
57
+ const free = models.find((m) => m.id === 'enso-free' || m.id === 'zen-free' || (m.id.includes('free') && !m.id.includes('openrouter')));
58
+ if (free)
59
+ return free.id;
60
+ return 'enso-free';
61
+ }
62
+ // House routers: Enso or Zen
63
+ const houseAuto = models.find((m) => m.id === 'enso-auto' || m.id === 'zen-auto' || m.id === 'enso' || m.id === 'zen');
64
+ if (houseAuto)
65
+ return houseAuto.id;
66
+ const houseModel = models.find((m) => (m.id.startsWith('enso') || m.id.startsWith('zen')) && !m.id.includes('openrouter'));
67
+ if (houseModel)
68
+ return houseModel.id;
69
+ const clean = models.find((m) => !m.id.includes('openrouter'));
70
+ if (clean)
71
+ return clean.id;
72
+ return 'enso-auto';
73
+ }
74
+ /**
75
+ * The gateway's address in the shape `createAiClient` takes it.
76
+ *
77
+ * The address itself is `api()` — one derivation, in `lib/hanzo/api.ts`, which
78
+ * every plain `fetch` in the app also reads. This working correctly here and
79
+ * nowhere else is what left eight other modules pointed at production while the
80
+ * dev proxy sat unused beside them.
81
+ */
82
+ export const base = () => ({ baseUrl: api() });
83
+ export function useAi() {
84
+ const { sdk, isAuthenticated } = useIam();
85
+ // The organization every call is scoped to: the SDK's selection, sent only
86
+ // when the token says the reader belongs to it (`org()`), and followed through
87
+ // the one event a change fires — so the client is rebuilt and every hook on
88
+ // it reads the new organization's rooms, agents and threads.
89
+ const [scoped, setScoped] = useState(() => org());
90
+ useEffect(() => {
91
+ const on = () => setScoped(org());
92
+ window.addEventListener(ORG, on);
93
+ window.addEventListener('storage', on);
94
+ return () => {
95
+ window.removeEventListener(ORG, on);
96
+ window.removeEventListener('storage', on);
97
+ };
98
+ }, []);
99
+ const ready = useHydrated();
100
+ const signedIn = isAuthenticated || hasSession();
101
+ const account = Boolean(signedIn && sdk);
102
+ const client = useMemo(() => {
103
+ // TWO STATES, ONE ISSUER. Signed in is the visitor's own IAM session; signed
104
+ // out is the org's IAM-minted publishable key. There is no third credential
105
+ // and no locally-invented one — a token this app made up is a token the
106
+ // gateway has no reason to accept, and it reaches the metered lane rather
107
+ // than the free one, which is the opposite of what a signed-out reader needs.
108
+ if (account && sdk) {
109
+ return createAiClient({
110
+ fetch: observed((input, init) => fetch(input, init)),
111
+ headers: scoped ? { 'X-Org-Id': scoped } : {},
112
+ auth: {
113
+ ...sdk,
114
+ getValidAccessToken: async () => {
115
+ const t = (await sdk.getValidAccessToken?.()) || bearer();
116
+ if (!t)
117
+ return null;
118
+ try {
119
+ const part = t.split('.')[1];
120
+ if (part) {
121
+ const payload = JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));
122
+ const auds = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
123
+ if (auds.includes('hanzo-ai') && !auds.includes('hanzo-app')) {
124
+ // Production gateway does not yet whitelist aud="hanzo-ai"; fallback
125
+ // to anonymous key rather than failing with "jwt: audience not allowed".
126
+ return null;
127
+ }
128
+ }
129
+ }
130
+ catch { }
131
+ return t;
132
+ },
133
+ },
134
+ ...base(),
135
+ });
136
+ }
137
+ return createAiClient({ fetch: observed((input, init) => fetch(input, init)), publishableKey: where().key ?? '', ...base() });
138
+ }, [account, sdk, scoped]);
139
+ return { client, model: account ? ENSO : FREE, free: !account, ready };
140
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WHERE THE PLATFORM IS, DERIVED ONCE.
3
+ *
4
+ * `hooks/useAi.ts` worked this out correctly for the SDK client and nothing
5
+ * else could reach it: seven other modules wrote their own
6
+ * `process.env.NEXT_PUBLIC_HANZO_API_URL || 'https://api.hanzo.ai'` — reach.ts,
7
+ * team.ts, coding.ts, referrals.ts, useTiers.ts, Settings.tsx, Directory.tsx,
8
+ * Beluga.tsx — and every one of them got the local case wrong in the same way.
9
+ *
10
+ * THE LOCAL CASE IS THE WHOLE POINT. api.hanzo.ai admits an origin by allowlist
11
+ * and by an https, portless DNS proof; localhost satisfies neither, so a
12
+ * credentialed read to an ABSOLUTE address fails its preflight and the room
13
+ * draws as though the org owned nothing. `next.config.mjs` proxies `/v1` for
14
+ * exactly this, but a caller with an absolute default never reaches a rewrite.
15
+ * Same-origin on the dev server is what makes the proxy do its job.
16
+ *
17
+ * A FUNCTION, NOT A CONSTANT. Those eight were module-scope `const`s, evaluated
18
+ * while the static export prerenders — where there is no `window` — so the
19
+ * production address was frozen into the bundle before a browser could say
20
+ * where it was. Called at request time it answers for the page that is open.
21
+ */
22
+ /** The gateway's origin, with no trailing slash: callers write `${api()}/v1/…`. */
23
+ export declare function api(): string;
24
+ /**
25
+ * Identity, likewise once. IAM issues the bearer every call above carries and
26
+ * is not reached through the API host, so it is a second address rather than a
27
+ * path on the first. It is NOT proxied and does not need to be — hanzo.id
28
+ * admits localhost on authorize, token and userinfo.
29
+ *
30
+ * `NEXT_PUBLIC_HANZO_IAM_URL` is the name — the one `.env.example` documents
31
+ * and `AccountContext` already reads. `team.ts` read `NEXT_PUBLIC_HANZO_ID_URL`
32
+ * instead, a name nothing sets, so pointing this app at another IAM would have
33
+ * moved the account panel and left the invitation links behind on hanzo.id.
34
+ * Two spellings of one setting are indistinguishable from a working one until
35
+ * the day it is changed.
36
+ */
37
+ export declare function iam(): string;
38
+ /**
39
+ * The IAM application hanzo.ai signs people in through, and the one an invited
40
+ * member joins through: the gateway admits it and IAM holds hanzo.ai's callbacks
41
+ * on it. It is platform-owned, so its full id is `admin/<CLIENT>`.
42
+ */
43
+ export declare const CLIENT = "hanzo-app";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WHERE THE PLATFORM IS, DERIVED ONCE.
3
+ *
4
+ * `hooks/useAi.ts` worked this out correctly for the SDK client and nothing
5
+ * else could reach it: seven other modules wrote their own
6
+ * `process.env.NEXT_PUBLIC_HANZO_API_URL || 'https://api.hanzo.ai'` — reach.ts,
7
+ * team.ts, coding.ts, referrals.ts, useTiers.ts, Settings.tsx, Directory.tsx,
8
+ * Beluga.tsx — and every one of them got the local case wrong in the same way.
9
+ *
10
+ * THE LOCAL CASE IS THE WHOLE POINT. api.hanzo.ai admits an origin by allowlist
11
+ * and by an https, portless DNS proof; localhost satisfies neither, so a
12
+ * credentialed read to an ABSOLUTE address fails its preflight and the room
13
+ * draws as though the org owned nothing. `next.config.mjs` proxies `/v1` for
14
+ * exactly this, but a caller with an absolute default never reaches a rewrite.
15
+ * Same-origin on the dev server is what makes the proxy do its job.
16
+ *
17
+ * A FUNCTION, NOT A CONSTANT. Those eight were module-scope `const`s, evaluated
18
+ * while the static export prerenders — where there is no `window` — so the
19
+ * production address was frozen into the bundle before a browser could say
20
+ * where it was. Called at request time it answers for the page that is open.
21
+ */
22
+ import { guest, team } from './host.js';
23
+ import { where } from '../where.js';
24
+ /** The gateway's origin, with no trailing slash: callers write `${api()}/v1/…`. */
25
+ export function api() {
26
+ const set = where().api;
27
+ if (set)
28
+ return set.replace(/\/+$/, '');
29
+ if (typeof window !== 'undefined') {
30
+ const host = window.location.hostname;
31
+ // The shipped site is served from an origin the gateway already admits, so
32
+ // there the absolute address stands and no proxy is involved. hanzo.team,
33
+ // hanzo.bot and the Sites plane's `<slug>.hanzo.app` aliases serve this same
34
+ // export and are admitted the same way; none of them has a /v1 of its own.
35
+ if (host === 'hanzo.ai' || host.endsWith('.hanzo.ai') || team(host) || host === 'hanzo.bot' || guest(host))
36
+ return 'https://api.hanzo.ai';
37
+ return window.location.origin;
38
+ }
39
+ return 'https://api.hanzo.ai';
40
+ }
41
+ /**
42
+ * Identity, likewise once. IAM issues the bearer every call above carries and
43
+ * is not reached through the API host, so it is a second address rather than a
44
+ * path on the first. It is NOT proxied and does not need to be — hanzo.id
45
+ * admits localhost on authorize, token and userinfo.
46
+ *
47
+ * `NEXT_PUBLIC_HANZO_IAM_URL` is the name — the one `.env.example` documents
48
+ * and `AccountContext` already reads. `team.ts` read `NEXT_PUBLIC_HANZO_ID_URL`
49
+ * instead, a name nothing sets, so pointing this app at another IAM would have
50
+ * moved the account panel and left the invitation links behind on hanzo.id.
51
+ * Two spellings of one setting are indistinguishable from a working one until
52
+ * the day it is changed.
53
+ */
54
+ export function iam() {
55
+ return (where().iam || 'https://hanzo.id').replace(/\/+$/, '');
56
+ }
57
+ /**
58
+ * The IAM application hanzo.ai signs people in through, and the one an invited
59
+ * member joins through: the gateway admits it and IAM holds hanzo.ai's callbacks
60
+ * on it. It is platform-owned, so its full id is `admin/<CLIENT>`.
61
+ */
62
+ export const CLIENT = 'hanzo-app';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A scroller that does not draw its scrollbar, as a gui prop: a row of chips or
3
+ * tabs scrolls sideways under a finger or a trackpad, and a bar under it would
4
+ * be chrome around nothing. gui applies `scrollbarWidth` on the web and its
5
+ * types do not list it, so it is spread rather than written.
6
+ */
7
+ export declare const bare: {
8
+ readonly scrollbarWidth: 'none';
9
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A scroller that does not draw its scrollbar, as a gui prop: a row of chips or
3
+ * tabs scrolls sideways under a finger or a trackpad, and a bar under it would
4
+ * be chrome around nothing. gui applies `scrollbarWidth` on the web and its
5
+ * types do not list it, so it is spread rather than written.
6
+ */
7
+ export const bare = { scrollbarWidth: 'none' };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Starting a coding run, and choosing WHERE it runs.
3
+ *
4
+ * The contract, read off the served document rather than assumed:
5
+ * - start POST /v1/agents/coding → 202 {sessionId, repo, branch, routed, targetId}
6
+ * - targets GET /v1/agents/targets → {targets: [...]}
7
+ *
8
+ * WHERE A RUN RUNS IS ONE FIELD. Omit `targetId` and the run goes to the cloud's
9
+ * own sandbox; name one and it is routed to a machine the org has claimed. The
10
+ * answer comes back rather than being assumed: `routed` says whether it reached
11
+ * a machine, and `targetId` says which. A caller that asked for a machine and
12
+ * reads `routed: false` was given a sandbox instead, and should say so rather
13
+ * than draw a screen that claims otherwise.
14
+ *
15
+ * That is the whole of the web-vs-desktop question. A browser has no machine to
16
+ * offer, so it sends no target and gets a sandbox. A surface running ON a
17
+ * claimed machine sends that machine's id. Neither needs a second endpoint, and
18
+ * nothing here sniffs the user agent to decide — the caller says what it has.
19
+ *
20
+ * A refusal states its reason: 401 is the credential and 403 is permission, and
21
+ * neither is "try again", so neither says it.
22
+ *
23
+ * AND IT REACHES THE READER, which for a while it did not. On localhost this
24
+ * caught every refusal it had just raised and answered with a fabricated
25
+ * accepted run — `sessionId: 'ses_' + Math.random()` — plus two invented
26
+ * targets, and `token()` minted `hz_local_dev_token` for a reader who was not
27
+ * signed in. So the one machine the acceptance gate is run on was the one where
28
+ * a platform that was down, unauthenticated or 500ing still opened a run, wrote
29
+ * `?run=` and mounted live Pause/Resume/Stop over nothing. Measured: the same
30
+ * prompt twice returned two different ids, which a served id cannot do.
31
+ *
32
+ * `Start.tsx` already renders `e.message` beside the composer. Point
33
+ * NEXT_PUBLIC_HANZO_API_URL at a platform that answers; do not synthesise one.
34
+ */
35
+ /** What a coding run answers with. 202 — accepted, not finished. */
36
+ export interface CodingRun {
37
+ /** The session every surface hangs this run's activity off. */
38
+ sessionId: string;
39
+ repo: string;
40
+ /** The branch the run works on; the server picks one when `base` is empty. */
41
+ branch: string;
42
+ /** Whether it reached a claimed machine. False means the sandbox took it. */
43
+ routed: boolean;
44
+ /** Which machine, when one took it. Empty for a sandbox run. */
45
+ targetId: string;
46
+ }
47
+ /** A machine the org has claimed, as `/v1/agents/targets` lists it. */
48
+ export interface Target {
49
+ /** `tgt_` + 32 hex. What a run names to route here. */
50
+ id: string;
51
+ /** The name a person gave it ("workshop"). */
52
+ label: string;
53
+ /** laptop | cloud | … — a closed set the server owns. */
54
+ kind: string;
55
+ /** EFFECTIVE liveness: online | offline | draining. */
56
+ status: string;
57
+ /** Human summary — "8 vCPU / 32G", "1× GB10". */
58
+ capacity?: string;
59
+ host?: string;
60
+ /** How many of the org's sessions map here, and how many are running now. */
61
+ sessions?: number;
62
+ running?: number;
63
+ }
64
+ export interface StartCoding {
65
+ /** The task, in the words you would use with a colleague. */
66
+ prompt: string;
67
+ /** Model to route the coding agent through. */
68
+ model?: string;
69
+ /** `owner/name` in the caller's own org. */
70
+ repo?: string;
71
+ /** dev | claude | codex | python — which harness runs the prompt. */
72
+ tool?: string;
73
+ /** The branch to start from. Empty takes the repository's default. */
74
+ base?: string;
75
+ /**
76
+ * Route to a claimed machine. OMITTED IS THE SANDBOX, and that is the default
77
+ * on the web deliberately: a browser has no machine to offer.
78
+ */
79
+ targetId?: string;
80
+ /** Ask for a run with a screen — an image carrying an X server. */
81
+ desktop?: boolean;
82
+ }
83
+ export declare class CodingRefusal extends Error {
84
+ readonly status: number;
85
+ constructor(status: number, message: string);
86
+ }
87
+ /**
88
+ * Starts one autonomous coding run and answers the session it opened.
89
+ *
90
+ * 202, not 200: the run is ACCEPTED here and finishes later, so what comes back
91
+ * is where to watch it — `sessionId` is the row the working view already
92
+ * renders — and never its output.
93
+ */
94
+ export declare function startCoding(params: StartCoding): Promise<CodingRun>;
95
+ export declare const startRun: typeof startCoding;
96
+ /**
97
+ * The machines this org has claimed.
98
+ *
99
+ * An empty list is an ANSWER — this org has claimed none, so every run goes to
100
+ * a sandbox — and is not an error. A refusal still throws, because "we could
101
+ * not ask" and "there are none" must not render the same.
102
+ */
103
+ export declare function listTargets(): Promise<Target[]>;
104
+ /** A machine can take a run only while it is online. */
105
+ export declare const dispatchable: (t: Target) => boolean;