@astralbeam/sdk 0.0.6 → 0.2.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/dist/react.d.ts CHANGED
@@ -1,12 +1,51 @@
1
+ import { C as AstralBeamChatState, S as AstralBeamChatCoreOptions, x as AstralBeamChatCore } from "./index-DtIS-V14.js";
1
2
  import { ReactNode } from "react";
2
- import { AstralBeamChatAttachmentOptions, AstralBeamChatColorScheme, AstralBeamChatTheme, ToolDefinition, WidgetDefinition as WidgetDefinition$1 } from "@astralbeam/sdk/client";
3
+ import { AstralBeamChatAttachmentOptions, AstralBeamChatColorScheme, AstralBeamChatTheme, InferParameters, JsonSchemaObject, ParametersSchema, ToolDefinition, WidgetDefinition as WidgetDefinition$1, defineTool } from "@astralbeam/sdk/client";
3
4
 
4
- //#region src/react.d.ts
5
+ //#region src/react/index.d.ts
5
6
  interface WidgetDefinition extends Omit<WidgetDefinition$1, "render"> {
6
7
  /** Draws the widget with the agent-chosen props, in the host's own React tree. */
7
8
  render: (props: Record<string, unknown>) => ReactNode;
8
9
  }
10
+ interface TypedReactWidgetDefinition<S extends ParametersSchema = JsonSchemaObject> {
11
+ description: string;
12
+ parameters?: S;
13
+ render: (props: InferParameters<S>) => ReactNode;
14
+ }
15
+ /** Declares a host widget; a Standard Schema `parameters` types (and validates) `render`'s props. */
16
+ declare function defineWidget<const S extends ParametersSchema = JsonSchemaObject>(widget: TypedReactWidgetDefinition<S>): WidgetDefinition;
17
+ /** Everything `useAstralBeamChat` returns: the live state plus the session's actions. */
18
+ interface UseAstralBeamChatResult extends AstralBeamChatState {
19
+ sendMessage: AstralBeamChatCore["sendMessage"];
20
+ addToolResult: AstralBeamChatCore["addToolResult"];
21
+ stop: () => void;
22
+ reload: () => Promise<void>;
23
+ reset: () => void;
24
+ /** The underlying headless session, for anything the flattened surface does not carry. */
25
+ core: AstralBeamChatCore;
26
+ }
27
+ /**
28
+ * The headless chat session as a React hook: authentication, transport, tools, and transcript
29
+ * state with no markup, for hosts that own their whole chat UI. Transport identity (endpoints,
30
+ * agent) and the declared tool/widget set are fixed for the component's lifetime — remount with
31
+ * a React `key` to change them — but `execute` and `onRenderWidget` read the latest render, so
32
+ * ordinary closures over props and state stay live. Whether a widget renderer exists at all is
33
+ * part of the declared surface and is read at mount.
34
+ */
35
+ declare function useAstralBeamChat(options: AstralBeamChatCoreOptions): UseAstralBeamChatResult;
36
+ /** Imperative surface of a mounted `<AstralBeamChat>`, for hosts that draw their own controls. */
37
+ interface AstralBeamChatRef {
38
+ /** Clears the conversation: transcript, drafts, attachments, and live widget renders. */
39
+ reset: () => void;
40
+ /** Stops the in-flight generation, if any; the transcript keeps what already streamed. */
41
+ stop: () => void;
42
+ }
9
43
  interface AstralBeamChatProps {
44
+ /**
45
+ * Public ID of the organization-owned agent, fixed for this mounted chat. Omit it to use the
46
+ * organization's default agent, which the dashboard's agents page selects.
47
+ */
48
+ agentId?: string;
10
49
  /** Name shown in the widget's header; prop changes apply immediately. Default `"AstralBeam"`. */
11
50
  title?: string;
12
51
  /**
@@ -14,16 +53,20 @@ interface AstralBeamChatProps {
14
53
  * the transcript the full height. Prop changes apply immediately. Default `true`.
15
54
  */
16
55
  showHeader?: boolean;
56
+ /** Replaces the header's content with the host's own React content; `showHeader` still applies. */
57
+ header?: ReactNode;
58
+ /** Replaces the empty-transcript state with the host's own React content. */
59
+ empty?: ReactNode;
60
+ /** Extra host controls at the end of the composer's button row, next to send. */
61
+ composerActions?: ReactNode;
17
62
  /** Headline shown on the empty transcript; prop changes apply immediately. Default `"Ask the assistant"`. */
18
63
  emptyTitle?: string;
19
64
  /** Subtitle under the empty transcript's headline; prop changes apply immediately. */
20
65
  emptyDescription?: string;
21
- /** URL of the AstralBeam chat endpoint the widget streams from. Default `"/api/chat"`. */
22
- chatEndpoint?: string;
23
- /** Application endpoint that mints a short-lived chat JWT; omit for guest chat. */
24
- authEndpoint?: string;
25
- /** Host-specific instructions the endpoint appends to the agent's system prompt. */
26
- systemPrompt?: string;
66
+ /** Base URL of the AstralBeam API; the widget calls `/chat` under it. Default the hosted cloud. */
67
+ apiUrl?: string;
68
+ /** Application endpoint that mints a short-lived chat JWT. Default `"/api/astralbeam/token"`. */
69
+ authTokenUrl?: string;
27
70
  /** Host-defined tools the agent can call, executed in the host's React app, keyed by name. */
28
71
  tools?: Record<string, ToolDefinition>;
29
72
  /** Host-defined widgets the agent can render inline in the conversation, keyed by identifier. */
@@ -34,26 +77,14 @@ interface AstralBeamChatProps {
34
77
  theme?: AstralBeamChatTheme | undefined;
35
78
  /** File attachments in the composer, on by default; `false` turns them off. */
36
79
  attachments?: boolean | AstralBeamChatAttachmentOptions;
80
+ /** Shows the collected sandbox panel (files with downloads, command log) above the composer. Default `false`. */
81
+ sandboxPanel?: boolean;
37
82
  /**
38
83
  * Logs every SDK action to the browser console with UTC timestamps and full payloads,
39
84
  * and asks the endpoint to log its side of the run too; prop changes apply immediately.
40
85
  */
41
86
  debug?: boolean;
42
87
  }
43
- declare function AstralBeamChat({
44
- title,
45
- showHeader,
46
- emptyTitle,
47
- emptyDescription,
48
- chatEndpoint,
49
- authEndpoint,
50
- systemPrompt,
51
- tools,
52
- widgets,
53
- colorScheme,
54
- theme,
55
- attachments,
56
- debug
57
- }: AstralBeamChatProps): import("react").JSX.Element;
88
+ declare const AstralBeamChat: import("react").ForwardRefExoticComponent<AstralBeamChatProps & import("react").RefAttributes<AstralBeamChatRef>>;
58
89
  //#endregion
59
- export { AstralBeamChat, type AstralBeamChatAttachmentOptions, type AstralBeamChatColorScheme, AstralBeamChatProps, type AstralBeamChatTheme, type ToolDefinition, WidgetDefinition };
90
+ export { AstralBeamChat, type AstralBeamChatAttachmentOptions, type AstralBeamChatColorScheme, type AstralBeamChatCore, type AstralBeamChatCoreOptions, AstralBeamChatProps, AstralBeamChatRef, type AstralBeamChatState, type AstralBeamChatTheme, type InferParameters, type ParametersSchema, type ToolDefinition, TypedReactWidgetDefinition, UseAstralBeamChatResult, WidgetDefinition, defineTool, defineWidget, useAstralBeamChat };
package/dist/react.js CHANGED
@@ -1,16 +1,55 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
1
+ import { C as DEFAULT_COLOR_SCHEME, r as createAstralBeamChat } from "./core-BVlttAaO.js";
2
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
3
  import { createPortal } from "react-dom";
3
- import { mountAstralBeamChat } from "@astralbeam/sdk/client";
4
- import { jsx } from "react/jsx-runtime";
5
- //#region src/lib/client-constants.ts
6
- /** Color scheme used when the mount options and the React prop give none. */
7
- const DEFAULT_COLOR_SCHEME = "system";
8
- //#endregion
9
- //#region src/react.tsx
10
- function AstralBeamChat({ title, showHeader, emptyTitle, emptyDescription, chatEndpoint, authEndpoint, systemPrompt, tools, widgets = {}, colorScheme = DEFAULT_COLOR_SCHEME, theme, attachments, debug }) {
4
+ import { defineTool, mountAstralBeamChat } from "@astralbeam/sdk/client";
5
+ import { jsxs } from "react/jsx-runtime";
6
+ //#region src/react/index.tsx
7
+ /** Declares a host widget; a Standard Schema `parameters` types (and validates) `render`'s props. */
8
+ function defineWidget(widget) {
9
+ return widget;
10
+ }
11
+ /**
12
+ * The headless chat session as a React hook: authentication, transport, tools, and transcript
13
+ * state with no markup, for hosts that own their whole chat UI. Transport identity (endpoints,
14
+ * agent) and the declared tool/widget set are fixed for the component's lifetime — remount with
15
+ * a React `key` to change them — but `execute` and `onRenderWidget` read the latest render, so
16
+ * ordinary closures over props and state stay live. Whether a widget renderer exists at all is
17
+ * part of the declared surface and is read at mount.
18
+ */
19
+ function useAstralBeamChat(options) {
20
+ const optionsRef = useRef(options);
21
+ optionsRef.current = options;
22
+ const [core] = useState(() => createAstralBeamChat({
23
+ ...options,
24
+ tools: Object.fromEntries(Object.entries(options.tools ?? {}).map(([name, definition]) => [name, {
25
+ ...definition,
26
+ execute: (input) => {
27
+ const current = optionsRef.current.tools?.[name];
28
+ if (!current) throw new Error(`Tool "${name}" is no longer registered`);
29
+ return current.execute(input);
30
+ }
31
+ }])),
32
+ onRenderWidget: options.onRenderWidget === void 0 ? void 0 : (request) => optionsRef.current.onRenderWidget?.(request)
33
+ }));
34
+ useEffect(() => () => core.dispose(), [core]);
35
+ return {
36
+ ...useSyncExternalStore(core.subscribe, core.getState, core.getState),
37
+ sendMessage: core.sendMessage,
38
+ addToolResult: core.addToolResult,
39
+ stop: core.stop,
40
+ reload: core.reload,
41
+ reset: core.reset,
42
+ core
43
+ };
44
+ }
45
+ const AstralBeamChat = forwardRef(function AstralBeamChat({ agentId, title, showHeader, header, empty, composerActions, emptyTitle, emptyDescription, apiUrl, authTokenUrl, tools, widgets = {}, colorScheme = DEFAULT_COLOR_SCHEME, theme, attachments, sandboxPanel, debug }, ref) {
11
46
  const targetRef = useRef(null);
12
47
  const handleRef = useRef(null);
13
48
  const [activeRenders, setActiveRenders] = useState(/* @__PURE__ */ new Map());
49
+ useImperativeHandle(ref, () => ({
50
+ reset: () => handleRef.current?.reset(),
51
+ stop: () => handleRef.current?.stop()
52
+ }), []);
14
53
  const toolsRef = useRef(tools);
15
54
  useEffect(() => {
16
55
  toolsRef.current = tools;
@@ -42,30 +81,57 @@ function AstralBeamChat({ title, showHeader, emptyTitle, emptyDescription, chatE
42
81
  };
43
82
  }
44
83
  }])), [widgets]);
84
+ const [chromeContainers, setChromeContainers] = useState(/* @__PURE__ */ new Map());
85
+ const hasHeader = header !== void 0;
86
+ const hasEmpty = empty !== void 0;
87
+ const hasComposerActions = composerActions !== void 0;
88
+ const chromeSlots = useMemo(() => {
89
+ const build = (name) => (container) => {
90
+ setChromeContainers((previous) => new Map(previous).set(name, container));
91
+ return () => {
92
+ setChromeContainers((previous) => {
93
+ const next = new Map(previous);
94
+ next.delete(name);
95
+ return next;
96
+ });
97
+ };
98
+ };
99
+ return {
100
+ ...hasHeader ? { header: build("header") } : {},
101
+ ...hasEmpty ? { empty: build("empty") } : {},
102
+ ...hasComposerActions ? { composerActions: build("composerActions") } : {}
103
+ };
104
+ }, [
105
+ hasHeader,
106
+ hasEmpty,
107
+ hasComposerActions
108
+ ]);
45
109
  const live = useMemo(() => ({
46
110
  title,
47
111
  showHeader,
48
112
  emptyTitle,
49
113
  emptyDescription,
50
- systemPrompt,
51
114
  colorScheme,
52
115
  theme,
53
116
  attachments,
117
+ sandboxPanel,
54
118
  debug,
55
119
  tools: hostTools,
56
- widgets: hostWidgets
120
+ widgets: hostWidgets,
121
+ slots: chromeSlots
57
122
  }), [
58
123
  title,
59
124
  showHeader,
60
125
  emptyTitle,
61
126
  emptyDescription,
62
- systemPrompt,
63
127
  colorScheme,
64
128
  theme,
65
129
  attachments,
130
+ sandboxPanel,
66
131
  debug,
67
132
  hostTools,
68
- hostWidgets
133
+ hostWidgets,
134
+ chromeSlots
69
135
  ]);
70
136
  const liveRef = useRef(live);
71
137
  liveRef.current = live;
@@ -73,8 +139,9 @@ function AstralBeamChat({ title, showHeader, emptyTitle, emptyDescription, chatE
73
139
  if (!targetRef.current) return;
74
140
  const handle = mountAstralBeamChat(targetRef.current, {
75
141
  ...liveRef.current,
76
- chatEndpoint,
77
- authEndpoint
142
+ agentId,
143
+ apiUrl,
144
+ authTokenUrl
78
145
  });
79
146
  handleRef.current = handle;
80
147
  return () => {
@@ -85,14 +152,19 @@ function AstralBeamChat({ title, showHeader, emptyTitle, emptyDescription, chatE
85
152
  useEffect(() => {
86
153
  handleRef.current?.update(live);
87
154
  }, [live]);
88
- return /* @__PURE__ */ jsx("div", {
155
+ const chromeContent = {
156
+ header,
157
+ empty,
158
+ composerActions
159
+ };
160
+ return /* @__PURE__ */ jsxs("div", {
89
161
  style: { height: "100%" },
90
162
  ref: targetRef,
91
- children: [...activeRenders].map(([key, { widget, container, props }]) => {
163
+ children: [[...activeRenders].map(([key, { widget, container, props }]) => {
92
164
  const definition = widgets[widget];
93
165
  return definition ? createPortal(definition.render(props), container, key) : null;
94
- })
166
+ }), [...chromeContainers].map(([name, container]) => createPortal(chromeContent[name], container, `astralbeam-chrome-${name}`))]
95
167
  });
96
- }
168
+ });
97
169
  //#endregion
98
- export { AstralBeamChat };
170
+ export { AstralBeamChat, defineTool, defineWidget, useAstralBeamChat };
package/dist/server.d.ts CHANGED
@@ -1,33 +1,39 @@
1
- //#region src/server.d.ts
1
+ //#region src/server/index.d.ts
2
2
  declare const ASTRALBEAM_CHAT_TOKEN_AUDIENCE = "astralbeam-chat";
3
- declare const ASTRALBEAM_CHAT_TOKEN_ISSUER = "astralbeam-global";
3
+ declare const ASTRALBEAM_CHAT_TOKEN_ISSUER = "astralbeam-api-key";
4
4
  declare const ASTRALBEAM_CHAT_TOKEN_TYPE = "astralbeam-chat+jwt";
5
- declare const ASTRALBEAM_CHAT_TOKEN_KEY_ID = "global-v1";
5
+ declare const ASTRALBEAM_CHAT_TOKEN_VERSION = 2;
6
6
  declare const ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS = 300;
7
7
  declare const ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS = 600;
8
- interface AstralBeamChatUser {
9
- id: string;
10
- name?: string | undefined;
11
- email?: string | undefined;
12
- avatarUrl?: string | undefined;
8
+ interface TenantUser {
9
+ /** User of an Organization's Tenant who interacts with the embedded agent sidebar. */
10
+ readonly id: string;
13
11
  }
14
- interface AstralBeamChatTenant {
15
- id: string;
16
- name?: string | undefined;
17
- logoUrl?: string | undefined;
12
+ interface CreateAstralBeamChatTokenOptions<TTenantUser extends TenantUser = TenantUser> {
13
+ readonly apiKey: string;
14
+ readonly tenantUser: TTenantUser;
15
+ readonly expiresInSeconds?: number | undefined;
18
16
  }
19
- interface CreateAstralBeamChatTokenOptions {
20
- secret: string | Uint8Array;
21
- user: AstralBeamChatUser;
22
- tenant: AstralBeamChatTenant;
23
- expiresInSeconds?: number | undefined;
17
+ interface CreateAstralBeamTokenRouteOptions<TTenantUser extends TenantUser = TenantUser> {
18
+ /** The full API key, or a thunk read per request; missing or empty answers 503. */
19
+ readonly apiKey: string | undefined | (() => string | undefined);
20
+ /**
21
+ * Authenticates the request against the application's own session and returns the tenant
22
+ * user minted into the token. Returning nothing, or throwing, answers 401.
23
+ */
24
+ readonly tenantUser: (request: Request) => TTenantUser | null | undefined | Promise<TTenantUser | null | undefined>;
25
+ readonly expiresInSeconds?: number | undefined;
24
26
  }
25
- /** Creates the short-lived bearer token returned by an application's auth endpoint. */
26
- declare function createAstralBeamChatToken({
27
- secret,
28
- user,
29
- tenant,
27
+ /**
28
+ * Builds the fetch-standard `POST` handler for an application's token endpoint, owning the
29
+ * method check, the unconfigured-key 503, the unauthenticated 401, and the `no-store` header.
30
+ */
31
+ declare function createAstralBeamTokenRoute<TTenantUser extends TenantUser = TenantUser>(options: CreateAstralBeamTokenRouteOptions<TTenantUser>): (request: Request) => Promise<Response>;
32
+ /** Creates the short-lived bearer token returned by an application's server auth endpoint. */
33
+ declare function createAstralBeamChatToken<TTenantUser extends TenantUser = TenantUser>({
34
+ apiKey,
35
+ tenantUser,
30
36
  expiresInSeconds
31
- }: CreateAstralBeamChatTokenOptions): Promise<string>;
37
+ }: CreateAstralBeamChatTokenOptions<TTenantUser>): Promise<string>;
32
38
  //#endregion
33
- export { ASTRALBEAM_CHAT_TOKEN_AUDIENCE, ASTRALBEAM_CHAT_TOKEN_ISSUER, ASTRALBEAM_CHAT_TOKEN_KEY_ID, ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_TYPE, AstralBeamChatTenant, AstralBeamChatUser, CreateAstralBeamChatTokenOptions, createAstralBeamChatToken };
39
+ export { ASTRALBEAM_CHAT_TOKEN_AUDIENCE, ASTRALBEAM_CHAT_TOKEN_ISSUER, ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_TYPE, ASTRALBEAM_CHAT_TOKEN_VERSION, CreateAstralBeamChatTokenOptions, CreateAstralBeamTokenRouteOptions, TenantUser, createAstralBeamChatToken, createAstralBeamTokenRoute };
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import * as Schema from "effect/Schema";
1
2
  //#region node_modules/.deno/jose@6.2.9/node_modules/jose/dist/webapi/lib/buffer_utils.js
2
3
  const encoder = new TextEncoder();
3
4
  const decoder = new TextDecoder();
@@ -636,59 +637,109 @@ var SignJWT = class {
636
637
  }
637
638
  };
638
639
  //#endregion
639
- //#region src/server.ts
640
+ //#region src/server/index.ts
640
641
  const ASTRALBEAM_CHAT_TOKEN_AUDIENCE = "astralbeam-chat";
641
- const ASTRALBEAM_CHAT_TOKEN_ISSUER = "astralbeam-global";
642
+ const ASTRALBEAM_CHAT_TOKEN_ISSUER = "astralbeam-api-key";
642
643
  const ASTRALBEAM_CHAT_TOKEN_TYPE = "astralbeam-chat+jwt";
643
- const ASTRALBEAM_CHAT_TOKEN_KEY_ID = "global-v1";
644
+ const ASTRALBEAM_CHAT_TOKEN_VERSION = 2;
644
645
  const ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS = 300;
645
646
  const ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS = 600;
647
+ const API_KEY_ID_PATTERN = /^key_[0-9a-z]{1,63}_([0-9a-z]{1,63})$/;
648
+ const API_KEY_SECRET_PATTERN = /^abo_[A-Za-z]{64}$/;
649
+ const CHAT_TOKEN_MAX_BYTES = 16384;
650
+ const TENANT_USER_MAX_BYTES = 8192;
651
+ const TENANT_USER_MAX_DEPTH = 10;
646
652
  const textEncoder = new TextEncoder();
647
- function signingKey(secret) {
648
- const key = typeof secret === "string" ? textEncoder.encode(secret) : secret;
649
- if (key.byteLength < 32) throw new Error("AstralBeam chat signing secrets need at least 32 bytes");
650
- return key;
651
- }
652
- function requiredText(value, label, maxLength) {
653
- const text = value.trim();
654
- if (!text || text.length > maxLength) throw new Error(`${label} must be 1-${maxLength} characters`);
655
- return text;
653
+ const TenantUserJsonSchema = Schema.Json.annotate({ message: "tenantUser must contain only JSON values" });
654
+ const TenantUserSchema = Schema.StructWithRest(Schema.Struct({ id: Schema.String.pipe(Schema.check(Schema.makeFilter((value) => value.length >= 1 && value.length <= 255, { message: "tenantUser.id must be a 1-255 character string" }))) }), [Schema.Record(Schema.String, TenantUserJsonSchema)]).pipe(Schema.check(Schema.makeFilter((value) => !exceedsJsonDepth(value, TENANT_USER_MAX_DEPTH), { message: `tenantUser must not exceed ${TENANT_USER_MAX_DEPTH} levels` })), Schema.check(Schema.makeFilter((value) => textEncoder.encode(JSON.stringify(value)).byteLength <= TENANT_USER_MAX_BYTES, { message: `tenantUser must not exceed ${TENANT_USER_MAX_BYTES} bytes` })));
655
+ const decodeTenantUser = Schema.decodeUnknownSync(TenantUserSchema, {
656
+ errors: "all",
657
+ onExcessProperty: "error",
658
+ reportInput: false
659
+ });
660
+ function parseApiKey(apiKey) {
661
+ const separator = apiKey.lastIndexOf("_abo_");
662
+ const id = apiKey.slice(0, separator);
663
+ const secret = apiKey.slice(separator + 1);
664
+ if (!API_KEY_ID_PATTERN.test(id) || !API_KEY_SECRET_PATTERN.test(secret)) throw new Error("apiKey must match key_<organization>_<key>_abo_<secret>");
665
+ return {
666
+ id,
667
+ secret
668
+ };
656
669
  }
657
- function optionalText(value, label, maxLength) {
658
- if (value === void 0) return void 0;
659
- return requiredText(value, label, maxLength);
670
+ function exceedsJsonDepth(value, maximumDepth) {
671
+ const stack = [{
672
+ value,
673
+ depth: 1
674
+ }];
675
+ while (stack.length > 0) {
676
+ const current = stack.pop();
677
+ if (current.depth > maximumDepth) return true;
678
+ if (typeof current.value !== "object" || current.value === null) continue;
679
+ const children = Array.isArray(current.value) ? current.value : Object.values(current.value);
680
+ for (const child of children) stack.push({
681
+ value: child,
682
+ depth: current.depth + 1
683
+ });
684
+ }
685
+ return false;
686
+ }
687
+ function validatedTenantUser(value) {
688
+ return JSON.parse(JSON.stringify(decodeTenantUser(value)));
689
+ }
690
+ async function signingKey(secret) {
691
+ const digest = await crypto.subtle.digest("SHA-256", textEncoder.encode(secret));
692
+ return textEncoder.encode(encode(new Uint8Array(digest)));
693
+ }
694
+ function tokenRouteResponse(body, status) {
695
+ return Response.json(body, {
696
+ status,
697
+ headers: { "cache-control": "no-store" }
698
+ });
660
699
  }
661
- function optionalUrl(value, label) {
662
- const text = optionalText(value, label, 2048);
663
- if (text === void 0) return void 0;
664
- const url = new URL(text);
665
- if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`${label} must use http or https`);
666
- return url.href;
700
+ /**
701
+ * Builds the fetch-standard `POST` handler for an application's token endpoint, owning the
702
+ * method check, the unconfigured-key 503, the unauthenticated 401, and the `no-store` header.
703
+ */
704
+ function createAstralBeamTokenRoute(options) {
705
+ return async (request) => {
706
+ if (request.method !== "POST") return tokenRouteResponse({ error: "Use POST" }, 405);
707
+ const apiKey = typeof options.apiKey === "function" ? options.apiKey() : options.apiKey;
708
+ if (!apiKey) return tokenRouteResponse({ error: "The AstralBeam API key is not configured" }, 503);
709
+ let tenantUser;
710
+ try {
711
+ tenantUser = await options.tenantUser(request);
712
+ } catch {
713
+ tenantUser = void 0;
714
+ }
715
+ if (!tenantUser) return tokenRouteResponse({ error: "The session could not be verified" }, 401);
716
+ try {
717
+ return tokenRouteResponse({ token: await createAstralBeamChatToken({
718
+ apiKey,
719
+ tenantUser,
720
+ ...options.expiresInSeconds === void 0 ? {} : { expiresInSeconds: options.expiresInSeconds }
721
+ }) }, 200);
722
+ } catch {
723
+ return tokenRouteResponse({ error: "The chat token could not be created" }, 500);
724
+ }
725
+ };
667
726
  }
668
- /** Creates the short-lived bearer token returned by an application's auth endpoint. */
669
- async function createAstralBeamChatToken({ secret, user, tenant, expiresInSeconds = 300 }) {
727
+ /** Creates the short-lived bearer token returned by an application's server auth endpoint. */
728
+ async function createAstralBeamChatToken({ apiKey, tenantUser, expiresInSeconds = 300 }) {
670
729
  if (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 60 || expiresInSeconds > 600) throw new Error("AstralBeam chat tokens must live for 60-600 seconds");
671
- const userId = requiredText(user.id, "user.id", 255);
672
- const tenantId = requiredText(tenant.id, "tenant.id", 255);
730
+ const { id: apiKeyId, secret } = parseApiKey(apiKey);
731
+ const identity = validatedTenantUser(tenantUser);
673
732
  const now = Math.floor(Date.now() / 1e3);
674
- return await new SignJWT({
675
- ver: 1,
676
- user: {
677
- id: userId,
678
- name: optionalText(user.name, "user.name", 200),
679
- email: optionalText(user.email, "user.email", 320),
680
- avatarUrl: optionalUrl(user.avatarUrl, "user.avatarUrl")
681
- },
682
- tenant: {
683
- id: tenantId,
684
- name: optionalText(tenant.name, "tenant.name", 200),
685
- logoUrl: optionalUrl(tenant.logoUrl, "tenant.logoUrl")
686
- }
733
+ const token = await new SignJWT({
734
+ ver: 2,
735
+ tenantUser: identity
687
736
  }).setProtectedHeader({
688
737
  alg: "HS256",
689
738
  typ: ASTRALBEAM_CHAT_TOKEN_TYPE,
690
- kid: ASTRALBEAM_CHAT_TOKEN_KEY_ID
691
- }).setIssuer(ASTRALBEAM_CHAT_TOKEN_ISSUER).setAudience(ASTRALBEAM_CHAT_TOKEN_AUDIENCE).setSubject(userId).setIssuedAt(now).setExpirationTime(now + expiresInSeconds).sign(signingKey(secret));
739
+ kid: apiKeyId
740
+ }).setIssuer(ASTRALBEAM_CHAT_TOKEN_ISSUER).setAudience(ASTRALBEAM_CHAT_TOKEN_AUDIENCE).setSubject(tenantUser.id).setIssuedAt(now).setExpirationTime(now + expiresInSeconds).sign(await signingKey(secret));
741
+ if (textEncoder.encode(token).byteLength > CHAT_TOKEN_MAX_BYTES) throw new Error(`AstralBeam chat tokens must not exceed ${CHAT_TOKEN_MAX_BYTES} bytes`);
742
+ return token;
692
743
  }
693
744
  //#endregion
694
- export { ASTRALBEAM_CHAT_TOKEN_AUDIENCE, ASTRALBEAM_CHAT_TOKEN_ISSUER, ASTRALBEAM_CHAT_TOKEN_KEY_ID, ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_TYPE, createAstralBeamChatToken };
745
+ export { ASTRALBEAM_CHAT_TOKEN_AUDIENCE, ASTRALBEAM_CHAT_TOKEN_ISSUER, ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_TYPE, ASTRALBEAM_CHAT_TOKEN_VERSION, createAstralBeamChatToken, createAstralBeamTokenRoute };
package/dist/vue.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/vue.d.ts
1
+ //#region src/vue/index.d.ts
2
2
  declare const entrypoint = "vue";
3
3
  //#endregion
4
4
  export { entrypoint };
package/dist/vue.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/vue.ts
1
+ //#region src/vue/index.ts
2
2
  const entrypoint = "vue";
3
3
  //#endregion
4
4
  export { entrypoint };