@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.
- package/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE THE SESSION IS KEPT, named once.
|
|
3
|
+
*
|
|
4
|
+
* The @hanzo/iam SDK owns these keys and publishes no reader a plain module can
|
|
5
|
+
* call — `getSession()` needs `configureIam()`, and this app configures through
|
|
6
|
+
* `IamProvider` instead. So the names are restated here, in ONE file, and
|
|
7
|
+
* everything that needs the bearer asks this.
|
|
8
|
+
*
|
|
9
|
+
* That is not a style preference. Four modules restated the name themselves and
|
|
10
|
+
* every one of them got it wrong — they read `hanzo_access_token` while the SDK
|
|
11
|
+
* writes `hanzo_iam_access_token`, so invitations, referrals, coding runs and
|
|
12
|
+
* signed-in telemetry all read null and behaved as if nobody was signed in.
|
|
13
|
+
* Nothing errored: `bearer()` threw "Sign in to manage invitations" at somebody
|
|
14
|
+
* who was signed in, which reads as a permissions problem rather than a typo.
|
|
15
|
+
* Two derivations of one name never agree; this is the one derivation.
|
|
16
|
+
*/
|
|
17
|
+
import { AT, TALK } from './host.js';
|
|
18
|
+
import { api } from './api.js';
|
|
19
|
+
/** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
|
|
20
|
+
const PREFIX = 'hanzo_iam_';
|
|
21
|
+
const ACCESS = `${PREFIX}access_token`;
|
|
22
|
+
const REFRESH = `${PREFIX}refresh_token`;
|
|
23
|
+
const EXPIRES = `${PREFIX}expires_at`;
|
|
24
|
+
const KEYS = [ACCESS, REFRESH, `${PREFIX}id_token`, EXPIRES];
|
|
25
|
+
/**
|
|
26
|
+
* The access token, or null.
|
|
27
|
+
*
|
|
28
|
+
* Guarded because storage THROWS rather than answering null in a browser that
|
|
29
|
+
* refuses it, and a caller reaching for the bearer is usually about to make a
|
|
30
|
+
* request — one that should fail as unauthenticated, not as an exception.
|
|
31
|
+
*/
|
|
32
|
+
export function bearer() {
|
|
33
|
+
if (typeof window === 'undefined')
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return window.localStorage.getItem(ACCESS);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether the stored access token is live: present, and not past the expiry the
|
|
44
|
+
* SDK wrote beside it — the SDK's own fast path (`IAM#getValidAccessToken`)
|
|
45
|
+
* without the refresh, which is a request and cannot be waited for here. No
|
|
46
|
+
* expiry reads as live, as it does in the SDK.
|
|
47
|
+
*/
|
|
48
|
+
export function live() {
|
|
49
|
+
if (typeof window === 'undefined')
|
|
50
|
+
return false;
|
|
51
|
+
try {
|
|
52
|
+
const expires = window.localStorage.getItem(EXPIRES);
|
|
53
|
+
return Boolean(bearer()) && (!expires || Date.now() < Number(expires));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The attribute on `<html>` that says `/` is showing the app, not the landing. */
|
|
60
|
+
export const APP = 'data-app';
|
|
61
|
+
/**
|
|
62
|
+
* An address on `/` that names a place in the app — `?at=` in Dev, `?chat` in
|
|
63
|
+
* Chat (lib/host.ts `AT`, `TALK`). The app is signed-in only, so a reader
|
|
64
|
+
* nobody has signed in is sent to hanzo.id with it rather than shown the
|
|
65
|
+
* landing, and it comes back with them.
|
|
66
|
+
*/
|
|
67
|
+
const PLACE = new RegExp(`[?&](?:${AT}|${TALK})(?:[=&]|$)`);
|
|
68
|
+
/** Whether `search` names a place in the app. */
|
|
69
|
+
export const place = (search) => PLACE.test(search);
|
|
70
|
+
/**
|
|
71
|
+
* `live()`, and a place in the app, as the script `/` runs as the first child
|
|
72
|
+
* of its landing (app/_apex.tsx `[data-landing]`), before the rest of the
|
|
73
|
+
* landing is parsed (app/page.tsx). Either marks the document with `APP` and
|
|
74
|
+
* takes the landing off the page at once: a signed-in reader is never shown the
|
|
75
|
+
* pitch while the app loads, and a reader on their way to sign in to a place is
|
|
76
|
+
* not shown it while they leave. Same keys, same test. An inline style, because
|
|
77
|
+
* this is decided before any stylesheet or component has run.
|
|
78
|
+
*/
|
|
79
|
+
export const BOOT = `try{var t=localStorage.getItem('${ACCESS}'),e=localStorage.getItem('${EXPIRES}');if((t&&(!e||Date.now()<Number(e)))||${PLACE}.test(location.search)){document.documentElement.setAttribute('${APP}','');var l=document.currentScript&&document.currentScript.parentElement;if(l&&l.hasAttribute('data-landing'))l.style.display='none'}}catch(_){}`;
|
|
80
|
+
/**
|
|
81
|
+
* A RETURNING PERSON IS RECOGNISED WITHOUT GOING ANYWHERE.
|
|
82
|
+
*
|
|
83
|
+
* IAM keeps its session cookie on this origin (universe static-sites
|
|
84
|
+
* `iam-on-hanzo-ai`), set when a person signs in on /login or /signup. So those
|
|
85
|
+
* two pages ask IAM first, before drawing a form, with a POST that carries no
|
|
86
|
+
* credential: @hanzo/iam `resume`, which IAM's single sign-on branch answers
|
|
87
|
+
* with a code from the session, or refuses. A code signs the reader in with
|
|
88
|
+
* nothing typed; a refusal draws the form. Nobody is sent to hanzo.id, and no
|
|
89
|
+
* frame is opened.
|
|
90
|
+
*
|
|
91
|
+
* ONCE PER TAB, then never again in it. Only a person's browser asks: a crawler
|
|
92
|
+
* or an automated browser is drawn the form.
|
|
93
|
+
*/
|
|
94
|
+
const PROBED = 'signin.probed';
|
|
95
|
+
/** What names itself a crawler, a preview fetcher or a headless browser. */
|
|
96
|
+
const AUTOMATED = /bot|crawl|spider|slurp|preview|lighthouse|headless/i;
|
|
97
|
+
/**
|
|
98
|
+
* Whether this tab may still ask: a person's browser, and not asked in this tab
|
|
99
|
+
* yet. A browser that refuses storage cannot remember having asked, so it never
|
|
100
|
+
* asks.
|
|
101
|
+
*/
|
|
102
|
+
export function unasked() {
|
|
103
|
+
if (typeof window === 'undefined')
|
|
104
|
+
return false;
|
|
105
|
+
if (navigator.webdriver || AUTOMATED.test(navigator.userAgent))
|
|
106
|
+
return false;
|
|
107
|
+
try {
|
|
108
|
+
return !window.sessionStorage.getItem(PROBED);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Spend this tab's ask: it is written down before it is made, so it is made once. */
|
|
115
|
+
export function asked() {
|
|
116
|
+
try {
|
|
117
|
+
window.sessionStorage.setItem(PROBED, '1');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* unasked() already answered no for a browser with no store */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether the browser holds an existing session (stored token, JWT subject,
|
|
125
|
+
* or migration candidate).
|
|
126
|
+
*
|
|
127
|
+
* Guarded against storage exceptions. Used across workspace and marketing
|
|
128
|
+
* headers so a signed-in person never sees "Sign in".
|
|
129
|
+
*/
|
|
130
|
+
export function hasSession() {
|
|
131
|
+
if (typeof window === 'undefined')
|
|
132
|
+
return false;
|
|
133
|
+
try {
|
|
134
|
+
return Boolean(bearer() ||
|
|
135
|
+
subject() ||
|
|
136
|
+
window.localStorage.getItem('hanzo_access_token') ||
|
|
137
|
+
window.localStorage.getItem(WHO) ||
|
|
138
|
+
window.sessionStorage?.getItem(ACCESS));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Carry a session across the move from per-tab storage to per-origin.
|
|
146
|
+
*
|
|
147
|
+
* The session used to be held in `sessionStorage`, which is why it did not
|
|
148
|
+
* survive closing a tab. It is `localStorage` now — but everyone already signed
|
|
149
|
+
* in was signed in THERE, and the new build looks HERE, so the fix for "I have
|
|
150
|
+
* to sign in again" would itself have signed everybody out one last time.
|
|
151
|
+
*
|
|
152
|
+
* The old store is read once and its keys moved over. After that this does
|
|
153
|
+
* nothing, because there is nothing left to find: a migration, not a fallback,
|
|
154
|
+
* and a fallback is what would keep two answers about who is signed in alive.
|
|
155
|
+
*
|
|
156
|
+
* ONLY WHEN THE NEW STORE IS EMPTY. A session established since the move is the
|
|
157
|
+
* current one, and a stale per-tab copy must never overwrite it.
|
|
158
|
+
*/
|
|
159
|
+
export function carry() {
|
|
160
|
+
if (typeof window === 'undefined')
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
const from = window.sessionStorage;
|
|
164
|
+
const to = window.localStorage;
|
|
165
|
+
// The SDK's own write test first. A store that refuses a write is one the
|
|
166
|
+
// SDK does not keep the session in — it keeps it per tab — and the throw
|
|
167
|
+
// leaves before anything is touched.
|
|
168
|
+
const probe = `${PREFIX}probe`;
|
|
169
|
+
to.setItem(probe, '1');
|
|
170
|
+
to.removeItem(probe);
|
|
171
|
+
// The access token is the session. It is carried only into a store that
|
|
172
|
+
// holds none, and the per-tab copy is removed either way: left behind, it
|
|
173
|
+
// would be carried back the next time a sign-out empties the shared store.
|
|
174
|
+
const into = !to.getItem(ACCESS) && from.getItem(ACCESS) !== null;
|
|
175
|
+
for (const key of KEYS) {
|
|
176
|
+
const held = from.getItem(key);
|
|
177
|
+
if (into && held !== null)
|
|
178
|
+
to.setItem(key, held);
|
|
179
|
+
from.removeItem(key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* no store, or one that refuses — the reader signs in again, which is what
|
|
184
|
+
happened before this file existed */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Who this browser's local state belongs to. */
|
|
188
|
+
const WHO = 'hanzo:who';
|
|
189
|
+
/** The SDK's two per-person selections. Everything else under its prefix is
|
|
190
|
+
* the session's own machinery — tokens, the login in flight — and stays. */
|
|
191
|
+
const CHOSEN = [`${PREFIX}current_org`, `${PREFIX}current_project`];
|
|
192
|
+
/** What a change of person leaves in place. */
|
|
193
|
+
const kept = (key) => key === WHO || key === 'hanzo:sidebar:open' || (key.startsWith(PREFIX) && !CHOSEN.includes(key));
|
|
194
|
+
/**
|
|
195
|
+
* The subject the stored access token names, or undefined.
|
|
196
|
+
*
|
|
197
|
+
* Read off the claim rather than waited for from userinfo, because that is
|
|
198
|
+
* what makes `own` runnable BEFORE anything else reads storage: the token is
|
|
199
|
+
* in place from the first line of the first script, and the SDK's own fast
|
|
200
|
+
* path judges a session the same way. Expiry is not consulted — a stale
|
|
201
|
+
* token still says whose browser this is, and the SDK either refreshes it or
|
|
202
|
+
* clears it, at which point the answer changes here too.
|
|
203
|
+
*/
|
|
204
|
+
export function subject() {
|
|
205
|
+
const token = bearer();
|
|
206
|
+
if (!token)
|
|
207
|
+
return undefined;
|
|
208
|
+
try {
|
|
209
|
+
const part = token.split('.')[1];
|
|
210
|
+
const claims = JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));
|
|
211
|
+
return typeof claims.sub === 'string' && claims.sub ? claims.sub : undefined;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Bind everything this site keeps in the browser to ONE person.
|
|
219
|
+
*
|
|
220
|
+
* Every `hanzo*` key — conversations, contacts, the calendar, the board, the
|
|
221
|
+
* agent's persona, the chat's history, the SDK's current org — is written per
|
|
222
|
+
* browser and would be read by whoever is signed in next. So the subject is
|
|
223
|
+
* recorded beside them, and when a different subject arrives (or nobody: a
|
|
224
|
+
* sign-out) the last person's keys are removed before anything reads them.
|
|
225
|
+
* The SDK's `hanzo_iam_*` keys are the arriving session itself and stay —
|
|
226
|
+
* except the org and project it last selected, which are the last person's.
|
|
227
|
+
*
|
|
228
|
+
* Same subject, no work. Called synchronously where the providers mount, so
|
|
229
|
+
* a page's first read of storage already sees a browser that is the reader's.
|
|
230
|
+
*/
|
|
231
|
+
export function own(sub) {
|
|
232
|
+
if (typeof window === 'undefined')
|
|
233
|
+
return;
|
|
234
|
+
try {
|
|
235
|
+
const store = window.localStorage;
|
|
236
|
+
if (store.getItem(WHO) === (sub ?? null))
|
|
237
|
+
return;
|
|
238
|
+
for (const key of Object.keys(store))
|
|
239
|
+
if (key.startsWith('hanzo') && !kept(key))
|
|
240
|
+
store.removeItem(key);
|
|
241
|
+
if (sub)
|
|
242
|
+
store.setItem(WHO, sub);
|
|
243
|
+
else
|
|
244
|
+
store.removeItem(WHO);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* a browser that refuses storage kept nothing to remove */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Follow the session every tab of this site shares.
|
|
252
|
+
*
|
|
253
|
+
* The SDK in each tab keeps its own copy of who is signed in and is told
|
|
254
|
+
* nothing when another tab changes the store. A sign-out there empties the
|
|
255
|
+
* tokens, and a tab that did not hear it goes on drawing the person it was
|
|
256
|
+
* opened for while every call it makes carries `X-Org-Id` and no bearer, which
|
|
257
|
+
* the platform answers as nobody (`a validated principal is required`).
|
|
258
|
+
*
|
|
259
|
+
* `moved` runs when the stored access token stops naming `was` — removed, or
|
|
260
|
+
* naming someone else; a cleared store (`key` null) is read the same way — and
|
|
261
|
+
* when the organization a SuperAdmin stepped into (`assumed`) changes, because
|
|
262
|
+
* the person is the same and every request's scope is not. Any other token for
|
|
263
|
+
* the same subject is not a move: that is a refresh in another tab, and
|
|
264
|
+
* `bearer()` reads it at call time. A tab on the sign-in callback is finishing
|
|
265
|
+
* its own sign-in and is left to it.
|
|
266
|
+
*
|
|
267
|
+
* When the store is left with nobody, the silent ask is marked spent in this
|
|
268
|
+
* tab before `moved` runs: the sign-out that emptied it may still be on its way
|
|
269
|
+
* to IAM, and an ask arriving first would be answered from the session being
|
|
270
|
+
* ended and sign this tab straight back in. The reader signs in again from the
|
|
271
|
+
* form. Returns the unsubscribe.
|
|
272
|
+
*/
|
|
273
|
+
export function follow(was, moved) {
|
|
274
|
+
const inside = assumed();
|
|
275
|
+
const on = (e) => {
|
|
276
|
+
if (e.storageArea !== window.localStorage)
|
|
277
|
+
return;
|
|
278
|
+
if (e.key !== null && e.key !== ACCESS)
|
|
279
|
+
return;
|
|
280
|
+
if (window.location.pathname.startsWith('/auth/callback'))
|
|
281
|
+
return;
|
|
282
|
+
const now = subject();
|
|
283
|
+
if (now === was && assumed() === inside)
|
|
284
|
+
return;
|
|
285
|
+
if (!now)
|
|
286
|
+
asked();
|
|
287
|
+
moved();
|
|
288
|
+
};
|
|
289
|
+
window.addEventListener('storage', on);
|
|
290
|
+
return () => window.removeEventListener('storage', on);
|
|
291
|
+
}
|
|
292
|
+
/** A token's claims, or null. */
|
|
293
|
+
function claimsOf(token) {
|
|
294
|
+
if (!token)
|
|
295
|
+
return null;
|
|
296
|
+
try {
|
|
297
|
+
const part = token.split('.')[1];
|
|
298
|
+
return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** The stored token's claims, or null. */
|
|
305
|
+
function claims() {
|
|
306
|
+
return claimsOf(bearer());
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The organizations the token says this person belongs to, home first.
|
|
310
|
+
* Empty for an account that has not made one: the SDK's own list falls back to
|
|
311
|
+
* the `owner` claim, which is the application's org and nobody's to work in.
|
|
312
|
+
*/
|
|
313
|
+
export function orgs() {
|
|
314
|
+
const set = claims()?.orgs;
|
|
315
|
+
const out = [];
|
|
316
|
+
for (const ref of Array.isArray(set) ? set : []) {
|
|
317
|
+
const o = ref?.org;
|
|
318
|
+
if (typeof o === 'string' && o && !out.includes(o))
|
|
319
|
+
out.push(o);
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The reader's role in one organization as IAM signed it into the token's `orgs`
|
|
325
|
+
* claim — owner, admin or member — or null where the token names no such org.
|
|
326
|
+
* It is the role in THAT org only (HIP-0527: org admin is scoped to its org), so
|
|
327
|
+
* it says nothing about any other org and nothing about SuperAdmin.
|
|
328
|
+
*/
|
|
329
|
+
export function role(o) {
|
|
330
|
+
if (!o)
|
|
331
|
+
return null;
|
|
332
|
+
const set = claims()?.orgs;
|
|
333
|
+
for (const ref of Array.isArray(set) ? set : []) {
|
|
334
|
+
const r = ref;
|
|
335
|
+
if (r?.org === o)
|
|
336
|
+
return typeof r.role === 'string' && r.role ? r.role : null;
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Who the token names — its display name and email — or null. The IAM user says
|
|
342
|
+
* the same once userinfo answers; this is the answer before it, so a signed-in
|
|
343
|
+
* reader is never drawn as a stranger while it is asked.
|
|
344
|
+
*/
|
|
345
|
+
export function named() {
|
|
346
|
+
const c = claims();
|
|
347
|
+
if (!c)
|
|
348
|
+
return null;
|
|
349
|
+
const text = (k) => (typeof c[k] === 'string' ? c[k] : '');
|
|
350
|
+
return { name: text('displayName') || text('name'), email: text('email') };
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The reader's account as IAM keys a membership: `<home org>/<username>`, or null.
|
|
354
|
+
* The token's `name` is the username and its `orgs` claim lists the home org first.
|
|
355
|
+
*/
|
|
356
|
+
export function me() {
|
|
357
|
+
const name = claims()?.name;
|
|
358
|
+
const home = orgs()[0];
|
|
359
|
+
return typeof name === 'string' && name && home ? `${home}/${name}` : null;
|
|
360
|
+
}
|
|
361
|
+
/** The organization this browser works in: the SDK's selection, when it is one of the token's. */
|
|
362
|
+
export function org() {
|
|
363
|
+
try {
|
|
364
|
+
const mine = orgs();
|
|
365
|
+
const chosen = window.localStorage.getItem(`${PREFIX}current_org`);
|
|
366
|
+
if (chosen && mine.includes(chosen))
|
|
367
|
+
return chosen;
|
|
368
|
+
// ONE ORGANIZATION IS NOT A CHOICE. Answering null here until `Orgs` had
|
|
369
|
+
// recorded a pick meant every single-organization reader began UNSCOPED:
|
|
370
|
+
// the client was built without X-Org-Id, the pick fired the event a moment
|
|
371
|
+
// later, and useAi rebuilt the client — so every hook restarted after first
|
|
372
|
+
// paint and a thread or a palette read in that window came back empty. The
|
|
373
|
+
// sole organization is the answer from the first render.
|
|
374
|
+
return mine.length === 1 ? mine[0] : null;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** The headers that scope a request to the signed-in person and their organization. */
|
|
381
|
+
export function scope() {
|
|
382
|
+
const h = {};
|
|
383
|
+
const t = bearer();
|
|
384
|
+
if (t)
|
|
385
|
+
h.Authorization = `Bearer ${t}`;
|
|
386
|
+
const o = org();
|
|
387
|
+
if (o)
|
|
388
|
+
h['X-Org-Id'] = o;
|
|
389
|
+
return h;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Expire the access token so the next load mints one carrying the account's
|
|
393
|
+
* current claims — after it joins an organization. The token itself stays,
|
|
394
|
+
* because it is what names the person: removing it would read as a sign-out
|
|
395
|
+
* and `own()` would empty everything the person just chose. The SDK sees an
|
|
396
|
+
* expired token and refreshes on the refresh token.
|
|
397
|
+
*/
|
|
398
|
+
export function renew() {
|
|
399
|
+
try {
|
|
400
|
+
window.localStorage.setItem(EXPIRES, '0');
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
/* no store to renew */
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** Fired on `window` when the organization this browser works in changes. */
|
|
407
|
+
export const ORG = 'hanzo:org';
|
|
408
|
+
/** This person picked an organization in this browser. `own()` clears it with the person. */
|
|
409
|
+
export const PICKED = 'hanzo:org:picked';
|
|
410
|
+
/**
|
|
411
|
+
* Work in this organization from now on: the SDK's selection, noted as picked
|
|
412
|
+
* and announced. Noted here, so a choice made anywhere — the rooms' gate, the
|
|
413
|
+
* app's account menu — is one the gate does not ask for again.
|
|
414
|
+
*/
|
|
415
|
+
export function pick(o) {
|
|
416
|
+
try {
|
|
417
|
+
window.localStorage.setItem(`${PREFIX}current_org`, o);
|
|
418
|
+
window.localStorage.setItem(PICKED, '1');
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* the choice lives only in this page */
|
|
422
|
+
}
|
|
423
|
+
window.dispatchEvent(new Event(ORG));
|
|
424
|
+
}
|
|
425
|
+
/** The reserved org whose own people are the platform's operators. */
|
|
426
|
+
export const ADMIN_ORG = 'admin';
|
|
427
|
+
/**
|
|
428
|
+
* Whether the signed-in person is a SUPERADMIN, as IAM signs it: the org of the
|
|
429
|
+
* person's own row — the first entry of the token's `orgs`, which IAM always puts
|
|
430
|
+
* first (hanzoai/iam internal/store/membership.go `MemberOrgRefs`) — is the
|
|
431
|
+
* reserved `admin` org. It is the equality IAM decides `assume` and the
|
|
432
|
+
* organization list on (pkg/schema/user.go `SuperAdmin`) and cloud's `Super`.
|
|
433
|
+
* Not the `owner` claim: a sign-in token carries the APPLICATION's org there. A
|
|
434
|
+
* membership of `admin` is not it either, and neither is being an org's admin.
|
|
435
|
+
* It decides only what is drawn: IAM refuses everyone else the list of every
|
|
436
|
+
* organization and the step into one.
|
|
437
|
+
*/
|
|
438
|
+
export function superAdmin() {
|
|
439
|
+
return orgs()[0] === ADMIN_ORG;
|
|
440
|
+
}
|
|
441
|
+
/** The organization a SuperAdmin stepped into through IAM, from the token's `assumed` claim, or null. */
|
|
442
|
+
export function assumed() {
|
|
443
|
+
const org = claims()?.assumed;
|
|
444
|
+
return typeof org === 'string' && org ? org : null;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Step into an organization as its support, or step back out (`org` null).
|
|
448
|
+
*
|
|
449
|
+
* IAM does the whole act: `POST /v1/iam/assume {org}` answers the same person's
|
|
450
|
+
* token re-scoped to that organization — `assumed` names it and `orgs` gains it —
|
|
451
|
+
* and records the step in that organization's audit log; `POST /v1/iam/release`
|
|
452
|
+
* answers the token with nothing assumed and records that too
|
|
453
|
+
* (hanzoai/iam internal/oidc/masquerade.go). The answer is kept where the SDK
|
|
454
|
+
* keeps the session, with its own expiry, so every request made from here on
|
|
455
|
+
* carries it and the SDK's next refresh ends support mode by itself. The page
|
|
456
|
+
* reloads so every room reads under it.
|
|
457
|
+
*/
|
|
458
|
+
export async function support(org) {
|
|
459
|
+
const token = bearer();
|
|
460
|
+
if (!token)
|
|
461
|
+
throw new Error('Sign in first.');
|
|
462
|
+
const res = await fetch(`${api()}/v1/iam/${org ? 'assume' : 'release'}`, {
|
|
463
|
+
method: 'POST',
|
|
464
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
465
|
+
body: JSON.stringify(org ? { org } : {}),
|
|
466
|
+
signal: AbortSignal.timeout(20_000),
|
|
467
|
+
});
|
|
468
|
+
const body = (await res.json().catch(() => null));
|
|
469
|
+
const next = body?.data?.accessToken;
|
|
470
|
+
const exp = claimsOf(next ?? null)?.exp;
|
|
471
|
+
if (!res.ok || !next || typeof exp !== 'number')
|
|
472
|
+
throw new Error(body?.msg || `IAM answered ${res.status}.`);
|
|
473
|
+
window.localStorage.setItem(ACCESS, next);
|
|
474
|
+
window.localStorage.setItem(EXPIRES, String(exp * 1000));
|
|
475
|
+
pick(org ?? homeOf(next));
|
|
476
|
+
window.location.reload();
|
|
477
|
+
}
|
|
478
|
+
/** The org of the person's own row: the first entry of a token's `orgs`. */
|
|
479
|
+
function homeOf(token) {
|
|
480
|
+
const set = claimsOf(token)?.orgs;
|
|
481
|
+
const first = Array.isArray(set) ? set[0]?.org : undefined;
|
|
482
|
+
return typeof first === 'string' ? first : '';
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Leave support mode when IAM cannot answer the step out: the stored token is
|
|
486
|
+
* marked spent, so the SDK's refresh — which mints from the person's own grant
|
|
487
|
+
* and assumes nothing — replaces it on the reload.
|
|
488
|
+
*/
|
|
489
|
+
export function abandon() {
|
|
490
|
+
renew();
|
|
491
|
+
pick('');
|
|
492
|
+
window.location.reload();
|
|
493
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sharing a conversation by link.
|
|
3
|
+
*
|
|
4
|
+
* The platform makes a read-only link to one conversation and answers its token
|
|
5
|
+
* once (github.com/hanzoai/agent share.go, under /v1/agent/chat). It keeps only
|
|
6
|
+
* the token's hash, so the token is held here, in this person's browser, for
|
|
7
|
+
* the conversation it opens — `own()` in lib/auth/session.ts removes it with
|
|
8
|
+
* every other `hanzo*` key when a different person signs in.
|
|
9
|
+
*
|
|
10
|
+
* A shared chat opens for its owner and for the people who open the link signed
|
|
11
|
+
* in, and for nobody else. A reader who is not signed in is told the title and
|
|
12
|
+
* asked to sign in; a signed-in reader is recorded as a viewer the owner can
|
|
13
|
+
* see, reads the chat, and finds it afterwards under Shared with you, read by
|
|
14
|
+
* its share id (`/chat/shared?s=<id>`) with no token at all. The token names a
|
|
15
|
+
* chat and authenticates nobody: every read carries the reader's own bearer.
|
|
16
|
+
*
|
|
17
|
+
* The token rides the address as the FRAGMENT (`/chat/shared#<token>`), which a
|
|
18
|
+
* browser never sends to any server. It leaves the address before anything on
|
|
19
|
+
* the page can read it: `SEAL`, the first script in the document head, moves it
|
|
20
|
+
* into this tab's sessionStorage and replaces the address with one that has no
|
|
21
|
+
* fragment, before the analytics and ad tags configure themselves. It reaches
|
|
22
|
+
* the platform only in a request body, and survives the sign-in round trip in
|
|
23
|
+
* sessionStorage — never back in an address.
|
|
24
|
+
*/
|
|
25
|
+
/** The page a shared conversation is read on. */
|
|
26
|
+
export declare const SHARED = "/chat/shared";
|
|
27
|
+
/** Where this tab holds a link's token between opening it and reading it. */
|
|
28
|
+
export declare const OPENING = "hanzo:share:open";
|
|
29
|
+
/**
|
|
30
|
+
* The document-head script that takes a share token out of the address. On any
|
|
31
|
+
* spelling of /chat/shared with a fragment — and on any page whose fragment has
|
|
32
|
+
* a share token's shape, the not-found page included — the fragment goes to
|
|
33
|
+
* sessionStorage (and to `window.hzShare` for a browser that refuses storage)
|
|
34
|
+
* and the address is replaced by the same path and query with no fragment. It
|
|
35
|
+
* runs as the document is parsed and again on every `hashchange` and
|
|
36
|
+
* `popstate`, with its listeners registered before any tag's, so a link pasted
|
|
37
|
+
* into an open tab is taken out before a tag's own history listener reads the
|
|
38
|
+
* address. First in the head, so no tag and no module ever sees an address
|
|
39
|
+
* carrying a token.
|
|
40
|
+
*/
|
|
41
|
+
export declare const SEAL = "(function(){function s(){try{var l=location,h=l.hash;if(h.length<2)return;var t=h.slice(1);try{t=decodeURIComponent(t)}catch(_){}var p=l.pathname.toLowerCase().replace(/\\/+/g,'/').replace(/\\/index\\.html$/,'').replace(/\\.html$/,'').replace(/\\/$/,'');if(p!=='/chat/shared'&&!/^[A-Za-z0-9_-]{43}$/.test(t))return;window.hzShare=t;try{sessionStorage.setItem('hanzo:share:open',t)}catch(_){}history.replaceState(null,'',l.pathname+l.search)}catch(_){}}s();addEventListener('hashchange',s);addEventListener('popstate',s)})()";
|
|
42
|
+
/** `SEAL` for a fragment the head's listeners did not take: the token goes to
|
|
43
|
+
* this tab and the address loses the fragment. */
|
|
44
|
+
export declare function take(): void;
|
|
45
|
+
/** The address that opens a share, on this origin unless told otherwise. */
|
|
46
|
+
export declare function link(token: string, origin?: string): string;
|
|
47
|
+
/** The address a viewer reads a chat shared with them at, without the token. */
|
|
48
|
+
export declare function address(share: string): string;
|
|
49
|
+
/** A token this tab is holding for a link it opened and has not read yet, or ''. */
|
|
50
|
+
export declare function opening(): string;
|
|
51
|
+
/** Forget the token this tab was holding, once the link has been read. */
|
|
52
|
+
export declare function opened(): void;
|
|
53
|
+
/** One signed-in person who opened a link, as its owner sees them. */
|
|
54
|
+
export interface Viewer {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
openedAt: string;
|
|
58
|
+
}
|
|
59
|
+
/** One live link to a conversation, as its owner sees it. */
|
|
60
|
+
export interface ShareRow {
|
|
61
|
+
id: string;
|
|
62
|
+
access: 'read';
|
|
63
|
+
createdAt: string;
|
|
64
|
+
viewers: Viewer[];
|
|
65
|
+
}
|
|
66
|
+
/** One turn of a shared conversation. `model` is set on an assistant turn the
|
|
67
|
+
* platform stored from that model's completion, and absent on a turn the
|
|
68
|
+
* person who shared the chat recorded. */
|
|
69
|
+
export interface SharedTurn {
|
|
70
|
+
role: 'user' | 'assistant';
|
|
71
|
+
content: string;
|
|
72
|
+
model?: string;
|
|
73
|
+
createdAt?: string;
|
|
74
|
+
}
|
|
75
|
+
/** A shared conversation as the platform answers it: the whole snapshot for a
|
|
76
|
+
* signed-in reader who opened it, the title alone for anyone else. */
|
|
77
|
+
export interface Shared {
|
|
78
|
+
/** The share's id, for a signed-in reader: where they read it again. */
|
|
79
|
+
share?: string;
|
|
80
|
+
title: string;
|
|
81
|
+
/** Who shared it, as they signed in: for a signed-in reader. */
|
|
82
|
+
by?: string;
|
|
83
|
+
/** Set for a signed-in reader who has not opened this link yet: opening it
|
|
84
|
+
* records them as a viewer, and the person who shared it sees their name. */
|
|
85
|
+
confirm?: boolean;
|
|
86
|
+
access: 'read';
|
|
87
|
+
full: boolean;
|
|
88
|
+
messages: SharedTurn[];
|
|
89
|
+
}
|
|
90
|
+
/** One chat shared with the reader, and who shared it. */
|
|
91
|
+
export interface SharedItem {
|
|
92
|
+
share: string;
|
|
93
|
+
title: string;
|
|
94
|
+
by: string;
|
|
95
|
+
openedAt: string;
|
|
96
|
+
}
|
|
97
|
+
/** A refusal, carrying the platform's own sentence and status. */
|
|
98
|
+
export declare class Refused extends Error {
|
|
99
|
+
readonly status: number;
|
|
100
|
+
constructor(message: string, status: number);
|
|
101
|
+
}
|
|
102
|
+
/** The owner's side: make, list and revoke the links to one conversation, and
|
|
103
|
+
* remove one viewer from a link. */
|
|
104
|
+
export declare const shares: {
|
|
105
|
+
list: (thread: string) => Promise<ShareRow[]>;
|
|
106
|
+
make: (thread: string) => Promise<{
|
|
107
|
+
share: ShareRow;
|
|
108
|
+
token: string;
|
|
109
|
+
}>;
|
|
110
|
+
revoke: (thread: string, id: string) => Promise<unknown>;
|
|
111
|
+
unview: (thread: string, id: string, viewer: string) => Promise<unknown>;
|
|
112
|
+
};
|
|
113
|
+
/** Open a link, or null when it opens nothing (never made, revoked, or closed
|
|
114
|
+
* to this reader). `open` is the reader's go-ahead to be recorded as a viewer,
|
|
115
|
+
* given after they were told who shared it and that their name is shown. */
|
|
116
|
+
export declare function read(token: string, open?: boolean): Promise<Shared | null>;
|
|
117
|
+
/** Read a chat shared with the reader by its share id, or null when it is no
|
|
118
|
+
* longer open to them. */
|
|
119
|
+
export declare function readShared(share: string): Promise<Shared | null>;
|
|
120
|
+
/** The chats shared with the reader, most recently opened first. */
|
|
121
|
+
export declare function sharedWithMe(): Promise<SharedItem[]>;
|
|
122
|
+
/** The link this browser holds for a conversation, if its share is still live. */
|
|
123
|
+
export declare function heldToken(thread: string, live: ShareRow[]): {
|
|
124
|
+
id: string;
|
|
125
|
+
token: string;
|
|
126
|
+
} | null;
|
|
127
|
+
/** Hold a new link's token for its conversation; null forgets it. */
|
|
128
|
+
export declare function hold(thread: string, value: {
|
|
129
|
+
id: string;
|
|
130
|
+
token: string;
|
|
131
|
+
} | null): void;
|