@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,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
|
+
}
|