@astralbeam/sdk 0.2.0 → 0.3.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/README.md CHANGED
@@ -40,16 +40,28 @@ import { createAstralBeamTokenRoute } from "@astralbeam/sdk/server"
40
40
 
41
41
  export const POST = createAstralBeamTokenRoute({
42
42
  apiKey: () => process.env.ASTRALBEAM_API_KEY, // key_<organization>_<key>_abo_<secret>
43
- tenantUser: async (request) => {
44
- const session = await getApplicationSession(request)
45
- return session && { id: session.user.id, name: session.user.name }
46
- },
43
+ authenticate: (request) => getApplicationSession(request),
44
+ user: (session) => ({
45
+ id: session.user.id,
46
+ name: session.user.name,
47
+ metadata: { email: session.user.email },
48
+ }),
49
+ tenant: (session) => ({
50
+ id: session.tenant.id,
51
+ name: session.tenant.name,
52
+ metadata: { plan: session.tenant.plan },
53
+ }),
47
54
  })
48
55
  ```
49
56
 
50
57
  - Add one endpoint, `/api/astralbeam/token` by default, that authenticates your own session first.
51
58
  - The factory owns the method check, the unconfigured 503, the unauthenticated 401, and `no-store`.
52
- - Derive `tenantUser` from trusted server-side state, never from anything the browser sent.
59
+ - Authenticate once, then derive `user` and `tenant` separately from that same application session.
60
+ - Derive `user` and `tenant` from trusted server-side state, never from anything the browser sent.
61
+ - Provide stable tenant-local `user.id` and stable `tenant.id` values; names are optional, and set `user.admin` only from trusted state.
62
+ - Put custom tenant and tenant-user fields in their respective `metadata` JSON objects; never include secrets.
63
+ - SDK fields use camelCase; AstralBeam-owned JWT claims use snake_case, while `metadata` keys are preserved verbatim.
64
+ - Tokens use the API key's organization slug as issuer and the platform audience `astralbeam`; AstralBeam does not require or interpret `sub`.
53
65
  - Tokens are signed, not encrypted: never put a secret in them.
54
66
  - Lifetimes are 60–600 seconds; the SDK renews in memory before expiry.
55
67
 
package/dist/server.d.ts CHANGED
@@ -1,39 +1,57 @@
1
+ import * as Schema from "effect/Schema";
2
+
1
3
  //#region src/server/index.d.ts
2
- declare const ASTRALBEAM_CHAT_TOKEN_AUDIENCE = "astralbeam-chat";
3
- declare const ASTRALBEAM_CHAT_TOKEN_ISSUER = "astralbeam-api-key";
4
- declare const ASTRALBEAM_CHAT_TOKEN_TYPE = "astralbeam-chat+jwt";
5
- declare const ASTRALBEAM_CHAT_TOKEN_VERSION = 2;
4
+ declare const ASTRALBEAM_TOKEN_AUDIENCE = "astralbeam";
5
+ declare const ASTRALBEAM_CHAT_TOKEN_TYPE = "astralbeam+jwt";
6
+ declare const ASTRALBEAM_CHAT_TOKEN_VERSION = 4;
6
7
  declare const ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS = 300;
7
8
  declare const ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS = 600;
8
- interface TenantUser {
9
- /** User of an Organization's Tenant who interacts with the embedded agent sidebar. */
10
- readonly id: string;
11
- }
12
- interface CreateAstralBeamChatTokenOptions<TTenantUser extends TenantUser = TenantUser> {
9
+ declare const TenantSchema: Schema.Struct<{
10
+ readonly id: Schema.String;
11
+ readonly name: Schema.optional<Schema.String>;
12
+ readonly metadata: Schema.optional<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
13
+ }>;
14
+ declare const TenantUserSchema: Schema.Struct<{
15
+ readonly id: Schema.String;
16
+ readonly name: Schema.optional<Schema.String>;
17
+ readonly admin: Schema.optional<Schema.Boolean>;
18
+ readonly metadata: Schema.optional<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
19
+ }>;
20
+ /** Tenant identity from the Organization's application, including JSON metadata. */
21
+ type Tenant = typeof TenantSchema.Type;
22
+ /** User of an Organization's Tenant who interacts with AstralBeam. */
23
+ type TenantUser = typeof TenantUserSchema.Type;
24
+ interface CreateAstralBeamChatTokenOptions<TTenantUser extends TenantUser = TenantUser, TTenant extends Tenant = Tenant> {
13
25
  readonly apiKey: string;
14
- readonly tenantUser: TTenantUser;
26
+ readonly user: TTenantUser;
27
+ readonly tenant: TTenant;
15
28
  readonly expiresInSeconds?: number | undefined;
16
29
  }
17
- interface CreateAstralBeamTokenRouteOptions<TTenantUser extends TenantUser = TenantUser> {
30
+ interface CreateAstralBeamTokenRouteOptions<TSession extends object = object, TTenantUser extends TenantUser = TenantUser, TTenant extends Tenant = Tenant> {
18
31
  /** The full API key, or a thunk read per request; missing or empty answers 503. */
19
32
  readonly apiKey: string | undefined | (() => string | undefined);
20
33
  /**
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.
34
+ * Authenticates the request against the application's own session. Returning nothing, or
35
+ * throwing, answers 401.
23
36
  */
24
- readonly tenantUser: (request: Request) => TTenantUser | null | undefined | Promise<TTenantUser | null | undefined>;
37
+ readonly authenticate: (request: Request) => TSession | null | undefined | Promise<TSession | null | undefined>;
38
+ /** Maps the authenticated session to the tenant user minted into the token. */
39
+ readonly user: (session: TSession) => TTenantUser;
40
+ /** Maps the same authenticated session to the tenant minted into the token. */
41
+ readonly tenant: (session: TSession) => TTenant;
25
42
  readonly expiresInSeconds?: number | undefined;
26
43
  }
27
44
  /**
28
45
  * Builds the fetch-standard `POST` handler for an application's token endpoint, owning the
29
46
  * method check, the unconfigured-key 503, the unauthenticated 401, and the `no-store` header.
30
47
  */
31
- declare function createAstralBeamTokenRoute<TTenantUser extends TenantUser = TenantUser>(options: CreateAstralBeamTokenRouteOptions<TTenantUser>): (request: Request) => Promise<Response>;
48
+ declare function createAstralBeamTokenRoute<TSession extends object, TTenantUser extends TenantUser = TenantUser, TTenant extends Tenant = Tenant>(options: CreateAstralBeamTokenRouteOptions<TSession, TTenantUser, TTenant>): (request: Request) => Promise<Response>;
32
49
  /** Creates the short-lived bearer token returned by an application's server auth endpoint. */
33
- declare function createAstralBeamChatToken<TTenantUser extends TenantUser = TenantUser>({
50
+ declare function createAstralBeamChatToken<TTenantUser extends TenantUser = TenantUser, TTenant extends Tenant = Tenant>({
34
51
  apiKey,
35
- tenantUser,
52
+ user,
53
+ tenant,
36
54
  expiresInSeconds
37
- }: CreateAstralBeamChatTokenOptions<TTenantUser>): Promise<string>;
55
+ }: CreateAstralBeamChatTokenOptions<TTenantUser, TTenant>): Promise<string>;
38
56
  //#endregion
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 };
57
+ export { ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_TYPE, ASTRALBEAM_CHAT_TOKEN_VERSION, ASTRALBEAM_TOKEN_AUDIENCE, CreateAstralBeamChatTokenOptions, CreateAstralBeamTokenRouteOptions, Tenant, TenantSchema, TenantUser, TenantUserSchema, createAstralBeamChatToken, createAstralBeamTokenRoute };
package/dist/server.js CHANGED
@@ -638,54 +638,64 @@ var SignJWT = class {
638
638
  };
639
639
  //#endregion
640
640
  //#region src/server/index.ts
641
- const ASTRALBEAM_CHAT_TOKEN_AUDIENCE = "astralbeam-chat";
642
- const ASTRALBEAM_CHAT_TOKEN_ISSUER = "astralbeam-api-key";
643
- const ASTRALBEAM_CHAT_TOKEN_TYPE = "astralbeam-chat+jwt";
644
- const ASTRALBEAM_CHAT_TOKEN_VERSION = 2;
641
+ const ASTRALBEAM_TOKEN_AUDIENCE = "astralbeam";
642
+ const ASTRALBEAM_CHAT_TOKEN_TYPE = "astralbeam+jwt";
643
+ const ASTRALBEAM_CHAT_TOKEN_VERSION = 4;
645
644
  const ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS = 300;
646
645
  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
646
  const CHAT_TOKEN_MAX_BYTES = 16384;
650
- const TENANT_USER_MAX_BYTES = 8192;
651
- const TENANT_USER_MAX_DEPTH = 10;
647
+ const IDENTITY_MAX_BYTES = 8192;
652
648
  const textEncoder = new TextEncoder();
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, {
649
+ const SlugSchema = Schema.String.pipe(Schema.check(Schema.isPattern(/^[0-9a-z-]{1,63}$/)));
650
+ const ApiKeySecretSchema = Schema.String.pipe(Schema.check(Schema.isPattern(/^abo_[A-Za-z]{64}$/)));
651
+ const ApiKeySchema = Schema.TemplateLiteral([
652
+ "key_",
653
+ SlugSchema,
654
+ "_",
655
+ SlugSchema,
656
+ "_",
657
+ ApiKeySecretSchema
658
+ ]);
659
+ const isApiKey = Schema.is(ApiKeySchema);
660
+ const MetadataSchema = Schema.JsonObject.annotate({ message: "metadata must be a JSON object" });
661
+ const TenantExternalIdSchema = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => value.length >= 1 && value.length <= 255, { message: "tenant.id must be a 1-255 character string" })));
662
+ const TenantUserExternalIdSchema = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => value.length >= 1 && value.length <= 255, { message: "user.id must be a 1-255 character string" })));
663
+ const TenantSchema = Schema.Struct({
664
+ id: TenantExternalIdSchema,
665
+ name: Schema.optional(Schema.String),
666
+ metadata: Schema.optional(MetadataSchema)
667
+ });
668
+ const TenantUserSchema = Schema.Struct({
669
+ id: TenantUserExternalIdSchema,
670
+ name: Schema.optional(Schema.String),
671
+ admin: Schema.optional(Schema.Boolean),
672
+ metadata: Schema.optional(MetadataSchema)
673
+ });
674
+ const IdentitySchema = Schema.Struct({
675
+ user: TenantUserSchema,
676
+ tenant: TenantSchema
677
+ }).pipe(Schema.check(Schema.makeFilter((value) => textEncoder.encode(JSON.stringify(value)).byteLength <= IDENTITY_MAX_BYTES, { message: `user and tenant must not exceed ${IDENTITY_MAX_BYTES} bytes` })));
678
+ const decodeIdentity = Schema.decodeUnknownSync(IdentitySchema, {
656
679
  errors: "all",
657
680
  onExcessProperty: "error",
658
681
  reportInput: false
659
682
  });
660
683
  function parseApiKey(apiKey) {
684
+ if (!isApiKey(apiKey)) throw new Error("apiKey must match key_<organization>_<key>_abo_<secret>");
661
685
  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>");
686
+ const keyId = apiKey.slice(0, separator);
687
+ const keySecret = apiKey.slice(separator + 1);
665
688
  return {
666
- id,
667
- secret
689
+ keyId,
690
+ organizationSlug: keyId.slice(4, keyId.indexOf("_", 4)),
691
+ keySecret
668
692
  };
669
693
  }
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)));
694
+ function validatedIdentity(user, tenant) {
695
+ return JSON.parse(JSON.stringify(decodeIdentity({
696
+ user,
697
+ tenant
698
+ })));
689
699
  }
690
700
  async function signingKey(secret) {
691
701
  const digest = await crypto.subtle.digest("SHA-256", textEncoder.encode(secret));
@@ -706,17 +716,18 @@ function createAstralBeamTokenRoute(options) {
706
716
  if (request.method !== "POST") return tokenRouteResponse({ error: "Use POST" }, 405);
707
717
  const apiKey = typeof options.apiKey === "function" ? options.apiKey() : options.apiKey;
708
718
  if (!apiKey) return tokenRouteResponse({ error: "The AstralBeam API key is not configured" }, 503);
709
- let tenantUser;
719
+ let session;
710
720
  try {
711
- tenantUser = await options.tenantUser(request);
721
+ session = await options.authenticate(request);
712
722
  } catch {
713
- tenantUser = void 0;
723
+ session = void 0;
714
724
  }
715
- if (!tenantUser) return tokenRouteResponse({ error: "The session could not be verified" }, 401);
725
+ if (!session) return tokenRouteResponse({ error: "The session could not be verified" }, 401);
716
726
  try {
717
727
  return tokenRouteResponse({ token: await createAstralBeamChatToken({
718
728
  apiKey,
719
- tenantUser,
729
+ user: options.user(session),
730
+ tenant: options.tenant(session),
720
731
  ...options.expiresInSeconds === void 0 ? {} : { expiresInSeconds: options.expiresInSeconds }
721
732
  }) }, 200);
722
733
  } catch {
@@ -725,21 +736,22 @@ function createAstralBeamTokenRoute(options) {
725
736
  };
726
737
  }
727
738
  /** Creates the short-lived bearer token returned by an application's server auth endpoint. */
728
- async function createAstralBeamChatToken({ apiKey, tenantUser, expiresInSeconds = 300 }) {
739
+ async function createAstralBeamChatToken({ apiKey, user, tenant, expiresInSeconds = 300 }) {
729
740
  if (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 60 || expiresInSeconds > 600) throw new Error("AstralBeam chat tokens must live for 60-600 seconds");
730
- const { id: apiKeyId, secret } = parseApiKey(apiKey);
731
- const identity = validatedTenantUser(tenantUser);
741
+ const { keyId, organizationSlug, keySecret } = parseApiKey(apiKey);
742
+ const identity = validatedIdentity(user, tenant);
732
743
  const now = Math.floor(Date.now() / 1e3);
733
744
  const token = await new SignJWT({
734
- ver: 2,
735
- tenantUser: identity
745
+ ver: 4,
746
+ user: identity.user,
747
+ tenant: identity.tenant
736
748
  }).setProtectedHeader({
737
749
  alg: "HS256",
738
750
  typ: ASTRALBEAM_CHAT_TOKEN_TYPE,
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));
751
+ kid: keyId
752
+ }).setIssuer(organizationSlug).setAudience(ASTRALBEAM_TOKEN_AUDIENCE).setIssuedAt(now).setExpirationTime(now + expiresInSeconds).sign(await signingKey(keySecret));
741
753
  if (textEncoder.encode(token).byteLength > CHAT_TOKEN_MAX_BYTES) throw new Error(`AstralBeam chat tokens must not exceed ${CHAT_TOKEN_MAX_BYTES} bytes`);
742
754
  return token;
743
755
  }
744
756
  //#endregion
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 };
757
+ export { ASTRALBEAM_CHAT_TOKEN_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_MAX_LIFETIME_SECONDS, ASTRALBEAM_CHAT_TOKEN_TYPE, ASTRALBEAM_CHAT_TOKEN_VERSION, ASTRALBEAM_TOKEN_AUDIENCE, TenantSchema, TenantUserSchema, createAstralBeamChatToken, createAstralBeamTokenRoute };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astralbeam/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Frontend SDK for AstralBeam: drop-in agent UI, chat streaming, and server helpers",
5
5
  "license": "MIT",
6
6
  "type": "module",