@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,87 @@
1
+ import { bearer as held, scope } from './session.js';
2
+ import { api } from './api.js';
3
+ export class CodingRefusal extends Error {
4
+ status;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.status = status;
8
+ }
9
+ }
10
+ function token() {
11
+ return held();
12
+ }
13
+ async function call(path, init) {
14
+ const tok = token();
15
+ const headers = new Headers(init?.headers);
16
+ if (!headers.has('Content-Type')) {
17
+ headers.set('Content-Type', 'application/json');
18
+ }
19
+ if (tok && !headers.has('Authorization')) {
20
+ for (const [k, v] of Object.entries(scope()))
21
+ if (!headers.has(k))
22
+ headers.set(k, v);
23
+ }
24
+ const res = await fetch(`${api()}${path}`, {
25
+ ...init,
26
+ // A run is a fact about right now, and a cached refusal survives the fix —
27
+ // it reads as the system still being broken long after it is not.
28
+ cache: 'no-store',
29
+ headers,
30
+ });
31
+ if (res.status === 401 && tok)
32
+ throw new CodingRefusal(401, 'Your session expired — sign in again');
33
+ if (res.status === 401 && !tok)
34
+ throw new CodingRefusal(401, 'Sign in or use Free AI to start a run');
35
+ if (res.status === 403)
36
+ throw new CodingRefusal(403, 'Your role cannot start runs in this organization');
37
+ if (!res.ok) {
38
+ let detail = '';
39
+ try {
40
+ const body = (await res.json());
41
+ detail = body?.error?.message || body?.msg || '';
42
+ }
43
+ catch {
44
+ // A refusal with no JSON body is still a refusal; the status carries it.
45
+ }
46
+ throw new CodingRefusal(res.status, detail || `Could not start the run (${res.status})`);
47
+ }
48
+ return res.json();
49
+ }
50
+ /**
51
+ * Starts one autonomous coding run and answers the session it opened.
52
+ *
53
+ * 202, not 200: the run is ACCEPTED here and finishes later, so what comes back
54
+ * is where to watch it — `sessionId` is the row the working view already
55
+ * renders — and never its output.
56
+ */
57
+ export async function startCoding(params) {
58
+ const body = await call('/v1/agents/coding', { method: 'POST', body: JSON.stringify(params) });
59
+ const r = (body ?? {});
60
+ if (!r.sessionId) {
61
+ // Accepted with no session is not a run anybody can watch. Say so rather
62
+ // than hand back a half-value the caller will render as success.
63
+ throw new CodingRefusal(502, 'The run was accepted but named no session');
64
+ }
65
+ return {
66
+ sessionId: r.sessionId,
67
+ repo: r.repo ?? '',
68
+ branch: r.branch ?? '',
69
+ routed: r.routed ?? false,
70
+ targetId: r.targetId ?? '',
71
+ };
72
+ }
73
+ export const startRun = startCoding;
74
+ /**
75
+ * The machines this org has claimed.
76
+ *
77
+ * An empty list is an ANSWER — this org has claimed none, so every run goes to
78
+ * a sandbox — and is not an error. A refusal still throws, because "we could
79
+ * not ask" and "there are none" must not render the same.
80
+ */
81
+ export async function listTargets() {
82
+ const body = await call('/v1/agents/targets', { method: 'GET' });
83
+ const t = body?.targets;
84
+ return Array.isArray(t) ? t : [];
85
+ }
86
+ /** A machine can take a run only while it is online. */
87
+ export const dispatchable = (t) => t.status === 'online';
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Where a sign-in comes back to, decided in ONE place.
3
+ *
4
+ * Signing in is a round trip through another origin: the browser leaves for
5
+ * hanzo.id's /authorize and returns to /auth/callback, and nothing about that
6
+ * return says which page the person was reading when they started. So the page
7
+ * is written down on the way out and read back on the way in. One writer (the
8
+ * header's Sign in), one reader (the callback), read-once.
9
+ *
10
+ * sessionStorage, because the note must NOT outlive the trip it belongs to. The
11
+ * session itself is localStorage — signing in is meant to survive the tab — but
12
+ * a destination is not a session: it answers "which page did this one sign-in
13
+ * start from", and a page remembered from yesterday's tab is not an answer to
14
+ * that. It survives the redirect because a redirect keeps the tab, which is the
15
+ * whole lifetime it needs.
16
+ *
17
+ * Every access is guarded because storage THROWS rather than returning null in
18
+ * a browser that refuses it (Safari private mode, a locked-down profile). A
19
+ * person who cannot be sent back to their page must still be signed in, so a
20
+ * refusal degrades to the default and never to an exception.
21
+ */
22
+ /** The default on the apex: the app, which is `/` for anyone signed in
23
+ * (app/page.tsx). It answers the case where there is no page to go back to (a
24
+ * typed /login, a /signup funnel, a stranger who cleared their storage
25
+ * mid-flight). */
26
+ export declare const DEFAULT_DESTINATION = "/";
27
+ /** Where a sign-in with no page to go back to lands on `hostname`: the app on
28
+ * the apex, and the rooms' Home everywhere else. */
29
+ export declare function landing(hostname: string): string;
30
+ /** Write down where we are, immediately before leaving for the issuer.
31
+ *
32
+ * It reads `window.location` itself rather than taking a path, because a
33
+ * caller that can name the page is a caller that can name the wrong one —
34
+ * there is exactly one moment this is correct to call and it is this one. */
35
+ export declare function rememberDestination(): void;
36
+ /** Where a person signs in: this site's own page. */
37
+ export declare const LOGIN = "/login";
38
+ /** Where a new one signs up: this site's own page, which the host names. */
39
+ export declare const signUp: () => string;
40
+ export declare function recognize(): void;
41
+ /**
42
+ * The page a signed-out reader is sent to when nothing says which: /login for
43
+ * a browser that has signed in here, /signup for a stranger, who has no
44
+ * password to type. Both ask hanzo.id first and link to each other.
45
+ */
46
+ export declare function door(): string;
47
+ /**
48
+ * Send this reader to sign in, and back to this page after.
49
+ *
50
+ * Every signed-out door into the app is this one call: `/` at a place in the
51
+ * app, /dev, a room. The page is written down, and the browser goes to `to`,
52
+ * which asks IAM for an existing session before it draws a form
53
+ * (components/auth/panel.tsx `Gate`). On a `guest` host the same address is asked for on hanzo.ai instead. With no `to`
54
+ * the page is `door()`'s; a control that says "Sign in" names LOGIN.
55
+ */
56
+ export declare function enter(to?: string): void;
57
+ /**
58
+ * The `?next=` a sign-in page was opened with, when it is an address on this
59
+ * site (`safe`): where the sign-in goes on to. Anything else is undefined.
60
+ */
61
+ export declare function onward(search: string): string | undefined;
62
+ /**
63
+ * On a `guest` host, send this sign-in to `address` on hanzo.ai and report that
64
+ * it left; anywhere else, do nothing and report false so the caller signs in
65
+ * here. A guest host cannot finish a sign-in (no client lists its callback), so
66
+ * starting one there only reaches IAM's refusal.
67
+ */
68
+ export declare function handoff(address: string): boolean;
69
+ /**
70
+ * Write down `address` as where this sign-in goes, in place of the page it
71
+ * started from: for a page that knows the reader's next stop rather than being
72
+ * it — /signup with a paid plan chosen goes on to /pay. Held to the same rule
73
+ * as everything read back: a path on this origin, or nothing is written.
74
+ */
75
+ export declare function aim(address: string): void;
76
+ /** The plan picker. */
77
+ export declare const PICKER = "/onboarding";
78
+ /**
79
+ * Where a completed sign-in goes first: the plan picker, which sends the reader
80
+ * on to `to` (components/onboarding/flow.tsx). A shared chat opens straight
81
+ * away. The picker is reached this way only, so it is shown once per sign-in.
82
+ */
83
+ export declare function picker(to: string): string;
84
+ /** Free was chosen: the sign-in this starts goes straight to the app. */
85
+ export declare function chooseFree(): void;
86
+ /** Read-once: whether Free was chosen before this sign-in. */
87
+ export declare function choseFree(): boolean;
88
+ /** Read-once: where this sign-in was headed, or this host's landing. */
89
+ export declare function takeDestination(): string;
90
+ /**
91
+ * A same-origin path, or `fallback`.
92
+ *
93
+ * `startsWith('/')` alone is not that test, and the gap is an open redirect:
94
+ * `//evil.example` and `/\evil.example` both begin with a slash and both
95
+ * navigate OFF this origin — the browser reads them as protocol-relative
96
+ * authorities, not paths. A single leading slash NOT followed by a second
97
+ * slash or a backslash is what makes a path a path.
98
+ *
99
+ * Exported so the rule can be exercised without a DOM: it is the one piece of
100
+ * this module that is a decision rather than a storage call.
101
+ */
102
+ export declare function safe(stored: string | null | undefined, fallback?: string): string;
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Where a sign-in comes back to, decided in ONE place.
3
+ *
4
+ * Signing in is a round trip through another origin: the browser leaves for
5
+ * hanzo.id's /authorize and returns to /auth/callback, and nothing about that
6
+ * return says which page the person was reading when they started. So the page
7
+ * is written down on the way out and read back on the way in. One writer (the
8
+ * header's Sign in), one reader (the callback), read-once.
9
+ *
10
+ * sessionStorage, because the note must NOT outlive the trip it belongs to. The
11
+ * session itself is localStorage — signing in is meant to survive the tab — but
12
+ * a destination is not a session: it answers "which page did this one sign-in
13
+ * start from", and a page remembered from yesterday's tab is not an answer to
14
+ * that. It survives the redirect because a redirect keeps the tab, which is the
15
+ * whole lifetime it needs.
16
+ *
17
+ * Every access is guarded because storage THROWS rather than returning null in
18
+ * a browser that refuses it (Safari private mode, a locked-down profile). A
19
+ * person who cannot be sent back to their page must still be signed in, so a
20
+ * refusal degrades to the default and never to an exception.
21
+ */
22
+ import { apex, guest, SIGNIN } from "./host.js";
23
+ import { SHARED } from "./share.js";
24
+ import { where } from "../where.js";
25
+ /** The default on the apex: the app, which is `/` for anyone signed in
26
+ * (app/page.tsx). It answers the case where there is no page to go back to (a
27
+ * typed /login, a /signup funnel, a stranger who cleared their storage
28
+ * mid-flight). */
29
+ export const DEFAULT_DESTINATION = "/";
30
+ /** Where a sign-in with no page to go back to lands on `hostname`: the app on
31
+ * the apex, and the rooms' Home everywhere else. */
32
+ export function landing(hostname) {
33
+ return apex(hostname) ? DEFAULT_DESTINATION : where().home;
34
+ }
35
+ /** The storage key.
36
+ *
37
+ * No brand in it — a key is a name, and the product it belongs to is not part
38
+ * of what it names. But not the bare word `destination` either: sessionStorage
39
+ * is scoped to the ORIGIN (per tab, but every page in it), and this origin also serves the chat SPA under
40
+ * /chat/, so a one-word key is a name two unrelated programs could both reach
41
+ * for. The qualifier says which flow owns it, which is the thing that makes it
42
+ * unambiguous. */
43
+ const KEY = "signin.destination";
44
+ /**
45
+ * The addresses a sign-in must never return to, because each of them RESTARTS
46
+ * the thing that just finished.
47
+ *
48
+ * /login calls `login()` from an effect on mount, so returning there sends the
49
+ * browser straight back to /authorize — a loop that ends only when the person
50
+ * gives up. /signup does the same with a signup hint. /auth/callback would
51
+ * re-run `handleCallback()` against an authorization code that is single-use
52
+ * and a verifier that was consumed by the exchange it is repeating, so it fails
53
+ * and bounces to /login, which then loops as above.
54
+ *
55
+ * They are excluded here rather than at the point of writing because this is
56
+ * the boundary that has to hold: the value has crossed storage and a foreign
57
+ * origin between the two, and a rule enforced only on the way out is a rule
58
+ * enforced nowhere.
59
+ */
60
+ const RESTARTS_THE_FLOW = new Set(["/login", "/signup", "/auth/callback"]);
61
+ /** Write down where we are, immediately before leaving for the issuer.
62
+ *
63
+ * It reads `window.location` itself rather than taking a path, because a
64
+ * caller that can name the page is a caller that can name the wrong one —
65
+ * there is exactly one moment this is correct to call and it is this one. */
66
+ export function rememberDestination() {
67
+ try {
68
+ const { pathname, search, hash } = window.location;
69
+ window.sessionStorage.setItem(KEY, pathname + search + hash);
70
+ }
71
+ catch {
72
+ /* no store — the callback then uses the default; never throw on the way to
73
+ a login, or the control does nothing at all instead of signing anyone in */
74
+ }
75
+ }
76
+ /** Where a person signs in: this site's own page. */
77
+ export const LOGIN = "/login";
78
+ /** Where a new one signs up: this site's own page, which the host names. */
79
+ export const signUp = () => where().signUp;
80
+ /**
81
+ * Whether this browser has held a session on this site. It is marked where the
82
+ * site resolves a signed-in user (app/providers.tsx `Identity`) and outlives
83
+ * sign-out, which clears the SDK's keys and not this one.
84
+ */
85
+ const KNOWN = "hz_known";
86
+ export function recognize() {
87
+ try {
88
+ window.localStorage.setItem(KNOWN, "1");
89
+ }
90
+ catch {
91
+ /* no store — this browser reads as a stranger's and is offered sign-up */
92
+ }
93
+ }
94
+ /**
95
+ * The page a signed-out reader is sent to when nothing says which: /login for
96
+ * a browser that has signed in here, /signup for a stranger, who has no
97
+ * password to type. Both ask hanzo.id first and link to each other.
98
+ */
99
+ export function door() {
100
+ try {
101
+ return window.localStorage.getItem(KNOWN) === "1" ? LOGIN : signUp();
102
+ }
103
+ catch {
104
+ return signUp();
105
+ }
106
+ }
107
+ /**
108
+ * Send this reader to sign in, and back to this page after.
109
+ *
110
+ * Every signed-out door into the app is this one call: `/` at a place in the
111
+ * app, /dev, a room. The page is written down, and the browser goes to `to`,
112
+ * which asks IAM for an existing session before it draws a form
113
+ * (components/auth/panel.tsx `Gate`). On a `guest` host the same address is asked for on hanzo.ai instead. With no `to`
114
+ * the page is `door()`'s; a control that says "Sign in" names LOGIN.
115
+ */
116
+ export function enter(to = door()) {
117
+ const { pathname, search, hash } = window.location;
118
+ const here = `${pathname}${search}${hash}`;
119
+ if (handoff(here))
120
+ return;
121
+ rememberDestination();
122
+ // Written in the address too, so the place (a room, a prompt in `?q=`)
123
+ // survives what sessionStorage does not: a new tab, a pasted link.
124
+ const back = safe(here, "");
125
+ window.location.assign(back && back !== "/" ? `${to}?next=${encodeURIComponent(back)}` : to);
126
+ }
127
+ /**
128
+ * The `?next=` a sign-in page was opened with, when it is an address on this
129
+ * site (`safe`): where the sign-in goes on to. Anything else is undefined.
130
+ */
131
+ export function onward(search) {
132
+ const next = new URLSearchParams(search).get("next");
133
+ return next && safe(next, "") === next ? next : undefined;
134
+ }
135
+ /**
136
+ * On a `guest` host, send this sign-in to `address` on hanzo.ai and report that
137
+ * it left; anywhere else, do nothing and report false so the caller signs in
138
+ * here. A guest host cannot finish a sign-in (no client lists its callback), so
139
+ * starting one there only reaches IAM's refusal.
140
+ */
141
+ export function handoff(address) {
142
+ if (typeof window === "undefined" || !guest(window.location.hostname))
143
+ return false;
144
+ window.location.assign(SIGNIN + address);
145
+ return true;
146
+ }
147
+ /**
148
+ * Write down `address` as where this sign-in goes, in place of the page it
149
+ * started from: for a page that knows the reader's next stop rather than being
150
+ * it — /signup with a paid plan chosen goes on to /pay. Held to the same rule
151
+ * as everything read back: a path on this origin, or nothing is written.
152
+ */
153
+ export function aim(address) {
154
+ if (safe(address, "") !== address)
155
+ return;
156
+ try {
157
+ window.sessionStorage.setItem(KEY, address);
158
+ }
159
+ catch {
160
+ /* no store: the callback uses the default */
161
+ }
162
+ }
163
+ /** The plan picker. */
164
+ export const PICKER = "/onboarding";
165
+ /**
166
+ * Where a completed sign-in goes first: the plan picker, which sends the reader
167
+ * on to `to` (components/onboarding/flow.tsx). A shared chat opens straight
168
+ * away. The picker is reached this way only, so it is shown once per sign-in.
169
+ */
170
+ export function picker(to) {
171
+ const path = to.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
172
+ return path === SHARED || path === PICKER ? to : `${PICKER}?next=${encodeURIComponent(to)}`;
173
+ }
174
+ /**
175
+ * FREE, CHOSEN BEFORE SIGNING IN, IS AN ANSWER. A paid plan chosen on a pricing
176
+ * card rides the sign-in as its destination (`/pay…?plan=`), and the picker opens
177
+ * its checkout. Free has no page to ride on, so the choice is written down here,
178
+ * in this tab, for the one sign-in it starts, and that sign-in skips the picker.
179
+ */
180
+ const FREE = "signin.free";
181
+ /** Free was chosen: the sign-in this starts goes straight to the app. */
182
+ export function chooseFree() {
183
+ try {
184
+ window.sessionStorage.setItem(FREE, "1");
185
+ }
186
+ catch {
187
+ /* no store: the picker asks, and Free is one click there */
188
+ }
189
+ }
190
+ /** Read-once: whether Free was chosen before this sign-in. */
191
+ export function choseFree() {
192
+ try {
193
+ const chosen = window.sessionStorage.getItem(FREE) === "1";
194
+ window.sessionStorage.removeItem(FREE);
195
+ return chosen;
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
201
+ /** Read-once: where this sign-in was headed, or this host's landing. */
202
+ export function takeDestination() {
203
+ return safe(take(), landing(window.location.hostname));
204
+ }
205
+ /** The storage half, alone, so the guard covers the read and nothing else — a
206
+ * `try` wrapped around the decision too would turn a bug in the rule below
207
+ * into a silent fallback instead of a failure anyone could see. */
208
+ function take() {
209
+ try {
210
+ const stored = window.sessionStorage.getItem(KEY);
211
+ window.sessionStorage.removeItem(KEY);
212
+ return stored;
213
+ }
214
+ catch {
215
+ return null;
216
+ }
217
+ }
218
+ /**
219
+ * A same-origin path, or `fallback`.
220
+ *
221
+ * `startsWith('/')` alone is not that test, and the gap is an open redirect:
222
+ * `//evil.example` and `/\evil.example` both begin with a slash and both
223
+ * navigate OFF this origin — the browser reads them as protocol-relative
224
+ * authorities, not paths. A single leading slash NOT followed by a second
225
+ * slash or a backslash is what makes a path a path.
226
+ *
227
+ * Exported so the rule can be exercised without a DOM: it is the one piece of
228
+ * this module that is a decision rather than a storage call.
229
+ */
230
+ export function safe(stored, fallback = DEFAULT_DESTINATION) {
231
+ if (!stored)
232
+ return fallback;
233
+ if (!stored.startsWith("/") ||
234
+ stored.startsWith("//") ||
235
+ stored.startsWith("/\\"))
236
+ return fallback;
237
+ // The browser's parser drops tabs and newlines and reads a backslash as a
238
+ // slash, so `/\t/evil.example` is `//evil.example` to it. An address is kept
239
+ // only when that same parser resolves it onto this origin.
240
+ let parsed;
241
+ try {
242
+ parsed = new URL(stored, HERE);
243
+ }
244
+ catch {
245
+ return fallback;
246
+ }
247
+ if (parsed.origin !== HERE)
248
+ return fallback;
249
+ // Compared without a trailing slash, because `/login` and `/login/` are the
250
+ // same page here and only one of them would be caught by a literal set. That
251
+ // is not hypothetical: this export answers both, and /auth/callback is itself
252
+ // inside the marketing layout, so it wears the header and its Sign in can be
253
+ // clicked — from the one address whose second visit is guaranteed to fail.
254
+ const path = parsed.pathname.replace(/\/+$/, "") || "/";
255
+ if (RESTARTS_THE_FLOW.has(path))
256
+ return fallback;
257
+ return stored;
258
+ }
259
+ /** An origin no address can name, to resolve a path against. */
260
+ const HERE = "https://hanzo.invalid";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Hanzo Tasks — the durable execution engine — as the board reaches it.
3
+ *
4
+ * TWO SUBSYSTEMS, ONE SEAM, and the seam is the point. `/v1/todo` holds what
5
+ * should be done and who holds it; `/v1/tasks` runs work that survives a crash,
6
+ * event-sourced and replayable. They are not two spellings of one thing: a card
7
+ * is dragged between columns and a run is replayed, and neither verb makes sense
8
+ * on the other. So nothing here merges them — a card HANDS ITS WORK to the
9
+ * engine, and the engine answers about the run.
10
+ *
11
+ * THE JOIN IS THE NAME, and it is derived rather than stored. A card started
12
+ * from this board runs under `card-<board>-<number>`, so the workflow says which
13
+ * card it came from and the card can find its run — with no new column on either
14
+ * side, and nothing to fall out of step. The forge issue and the workflow are
15
+ * still two objects in two stores; the identifier is what makes them one fact.
16
+ */
17
+ import type { AiClient } from '@hanzo/ai';
18
+ /** The engine's default namespace. A deployment with more picks per org later. */
19
+ export declare const NAMESPACE = "default";
20
+ /** One durable run, as the engine reports it. */
21
+ export interface Run {
22
+ workflowId: string;
23
+ runId?: string;
24
+ type?: string;
25
+ status?: string;
26
+ startTime?: string;
27
+ closeTime?: string;
28
+ historyLength?: number;
29
+ taskQueue?: string;
30
+ }
31
+ /** THE STATUSES the engine publishes, said the way a person would. */
32
+ export declare function reads(status?: string): string;
33
+ /** The workflow id a card's work runs under. Derived, never stored. */
34
+ export declare const runName: (board: string, number: number) => string;
35
+ /**
36
+ * THE QUEUE AND THE TYPE a card's work is handed to.
37
+ *
38
+ * `agents` because the worker that would execute it is an agent, and the engine
39
+ * matches work to workers by queue name — one queue per kind of worker is the
40
+ * whole of its routing. The engine ACCEPTS a start with nothing subscribed and
41
+ * leaves the run pending, which is the honest state: the work is recorded, and
42
+ * it moves when something is listening.
43
+ */
44
+ export declare const AGENT_QUEUE = "agents";
45
+ export declare const AGENT_WORKFLOW = "AgentWorkflow";
46
+ /** Every run the engine holds, newest first as it answers. */
47
+ export declare function runs(client: AiClient, signal?: AbortSignal): Promise<Run[]>;
48
+ /** The run a card's work is doing, or null when it has never been started. */
49
+ export declare function runOf(client: AiClient, board: string, number: number, signal?: AbortSignal): Promise<Run | null>;
50
+ /**
51
+ * Hands a card's work to the engine.
52
+ *
53
+ * IT STARTS EVEN WITH NOTHING LISTENING — measured, not assumed: the engine
54
+ * accepts the start and leaves the run RUNNING with one history event until a
55
+ * worker polls the queue. That is the engine's own semantics and it is the
56
+ * useful one: the work is durably recorded the moment it is handed over, and it
57
+ * moves when something subscribes. A card therefore shows "running" for work
58
+ * nobody is doing yet, which is true and is exactly the state worth seeing.
59
+ *
60
+ * `requestId` is the card's own name, so pressing twice is one run.
61
+ */
62
+ export declare function start(client: AiClient, board: string, number: number, work: {
63
+ title: string;
64
+ queue: string;
65
+ type: string;
66
+ }, signal?: AbortSignal): Promise<Run>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Hanzo Tasks — the durable execution engine — as the board reaches it.
3
+ *
4
+ * TWO SUBSYSTEMS, ONE SEAM, and the seam is the point. `/v1/todo` holds what
5
+ * should be done and who holds it; `/v1/tasks` runs work that survives a crash,
6
+ * event-sourced and replayable. They are not two spellings of one thing: a card
7
+ * is dragged between columns and a run is replayed, and neither verb makes sense
8
+ * on the other. So nothing here merges them — a card HANDS ITS WORK to the
9
+ * engine, and the engine answers about the run.
10
+ *
11
+ * THE JOIN IS THE NAME, and it is derived rather than stored. A card started
12
+ * from this board runs under `card-<board>-<number>`, so the workflow says which
13
+ * card it came from and the card can find its run — with no new column on either
14
+ * side, and nothing to fall out of step. The forge issue and the workflow are
15
+ * still two objects in two stores; the identifier is what makes them one fact.
16
+ */
17
+ /** The engine's default namespace. A deployment with more picks per org later. */
18
+ export const NAMESPACE = 'default';
19
+ /** THE STATUSES the engine publishes, said the way a person would. */
20
+ export function reads(status) {
21
+ if (!status)
22
+ return 'unknown';
23
+ return ({
24
+ WORKFLOW_EXECUTION_STATUS_RUNNING: 'running',
25
+ WORKFLOW_EXECUTION_STATUS_COMPLETED: 'done',
26
+ WORKFLOW_EXECUTION_STATUS_FAILED: 'failed',
27
+ WORKFLOW_EXECUTION_STATUS_CANCELED: 'canceled',
28
+ WORKFLOW_EXECUTION_STATUS_TERMINATED: 'stopped',
29
+ WORKFLOW_EXECUTION_STATUS_TIMED_OUT: 'timed out',
30
+ }[status] ?? status.replace(/^WORKFLOW_EXECUTION_STATUS_/, '').toLowerCase());
31
+ }
32
+ /** The workflow id a card's work runs under. Derived, never stored. */
33
+ export const runName = (board, number) => `card-${board}-${number}`;
34
+ /**
35
+ * THE QUEUE AND THE TYPE a card's work is handed to.
36
+ *
37
+ * `agents` because the worker that would execute it is an agent, and the engine
38
+ * matches work to workers by queue name — one queue per kind of worker is the
39
+ * whole of its routing. The engine ACCEPTS a start with nothing subscribed and
40
+ * leaves the run pending, which is the honest state: the work is recorded, and
41
+ * it moves when something is listening.
42
+ */
43
+ export const AGENT_QUEUE = 'agents';
44
+ export const AGENT_WORKFLOW = 'AgentWorkflow';
45
+ /** Every run the engine holds, newest first as it answers. */
46
+ export async function runs(client, signal) {
47
+ const out = await client.http.json({
48
+ path: `/v1/tasks/namespaces/${NAMESPACE}/workflows`,
49
+ signal,
50
+ });
51
+ const rows = Array.isArray(out?.executions) ? out.executions : [];
52
+ return rows.map(shape);
53
+ }
54
+ /** The run a card's work is doing, or null when it has never been started. */
55
+ export async function runOf(client, board, number, signal) {
56
+ const name = runName(board, number);
57
+ const out = await client.http.json({
58
+ path: `/v1/tasks/namespaces/${NAMESPACE}/workflows/${encodeURIComponent(name)}/executions`,
59
+ signal,
60
+ });
61
+ const rows = Array.isArray(out?.executions) ? out.executions : [];
62
+ // The chain is every attempt under that name; the newest is the one running.
63
+ return rows.length ? shape(rows[rows.length - 1]) : null;
64
+ }
65
+ /**
66
+ * Hands a card's work to the engine.
67
+ *
68
+ * IT STARTS EVEN WITH NOTHING LISTENING — measured, not assumed: the engine
69
+ * accepts the start and leaves the run RUNNING with one history event until a
70
+ * worker polls the queue. That is the engine's own semantics and it is the
71
+ * useful one: the work is durably recorded the moment it is handed over, and it
72
+ * moves when something subscribes. A card therefore shows "running" for work
73
+ * nobody is doing yet, which is true and is exactly the state worth seeing.
74
+ *
75
+ * `requestId` is the card's own name, so pressing twice is one run.
76
+ */
77
+ export async function start(client, board, number, work, signal) {
78
+ const out = await client.http.json({
79
+ method: 'POST',
80
+ path: `/v1/tasks/namespaces/${NAMESPACE}/workflows`,
81
+ body: {
82
+ workflowId: runName(board, number),
83
+ workflowType: { name: work.type },
84
+ taskQueue: { name: work.queue },
85
+ input: [{ card: `${board}#${number}`, title: work.title }],
86
+ // The card is the request, so a double press is one run rather than two.
87
+ requestId: runName(board, number),
88
+ },
89
+ signal,
90
+ });
91
+ return shape(out);
92
+ }
93
+ /** The engine's row, flattened to what a card needs to draw. */
94
+ function shape(row) {
95
+ const r = (row ?? {});
96
+ const ex = (r.execution ?? {});
97
+ return {
98
+ workflowId: String(ex.workflowId ?? r.workflowId ?? ''),
99
+ runId: ex.runId ?? r.runId,
100
+ type: r.type?.name ?? r.workflowType?.name,
101
+ status: r.status,
102
+ startTime: r.startTime,
103
+ closeTime: r.closeTime,
104
+ historyLength: r.historyLength,
105
+ taskQueue: typeof r.taskQueue === 'string' ? r.taskQueue : r.taskQueue?.name,
106
+ };
107
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Activation: the first time this browser does what the product is for,
3
+ * counted once as `first_action` with that thing as its `action` — the one
4
+ * activation name every product shares (@hanzo/events), which GA4 hears as
5
+ * `first_message` for the only action this site counts, `chat_message`.
6
+ *
7
+ * The mark lives in localStorage, so it is once per browser: a person on a
8
+ * second device is counted there too, and a browser that stores nothing is
9
+ * never counted, which undercounts rather than inflating.
10
+ */
11
+ export declare function first(action: string): void;
@@ -0,0 +1,23 @@
1
+ import { track } from './tags.js';
2
+ /**
3
+ * Activation: the first time this browser does what the product is for,
4
+ * counted once as `first_action` with that thing as its `action` — the one
5
+ * activation name every product shares (@hanzo/events), which GA4 hears as
6
+ * `first_message` for the only action this site counts, `chat_message`.
7
+ *
8
+ * The mark lives in localStorage, so it is once per browser: a person on a
9
+ * second device is counted there too, and a browser that stores nothing is
10
+ * never counted, which undercounts rather than inflating.
11
+ */
12
+ export function first(action) {
13
+ const key = `hz_first.${action}`;
14
+ try {
15
+ if (window.localStorage.getItem(key))
16
+ return;
17
+ window.localStorage.setItem(key, '1');
18
+ }
19
+ catch {
20
+ return;
21
+ }
22
+ track('first_action', { action });
23
+ }