@ateam-ai-mobile/core 0.1.9 → 0.1.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai-mobile/core",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Headless ADAS SDK core — auth bridge, API client, chat/job streaming, MCP calls, theme tokens. No UI, no Expo dependencies.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,6 +22,7 @@
22
22
  * so behaviour is identical (the R0.5 authRefresh baseline is the oracle).
23
23
  */
24
24
  import { getCoreApiUrl, type RefreshOutcome } from './config';
25
+ import { decodeJwtPayload } from './jwt';
25
26
 
26
27
  export interface SessionUser {
27
28
  actorId: string;
@@ -71,26 +72,6 @@ export interface AuthSessionDeps {
71
72
  onToken?: (token: string, user: SessionUser | null) => void | Promise<void>;
72
73
  }
73
74
 
74
- // ── JWT projection (verbatim from the app) ──────────────────────────────────
75
-
76
- function decodeJwtPayload(token: string): Record<string, any> | null {
77
- try {
78
- const segs = token.split('.');
79
- if (segs.length !== 3) return null;
80
- // base64url → base64
81
- const b64 = segs[1].replace(/-/g, '+').replace(/_/g, '/');
82
- // Pad to a multiple of 4
83
- const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
84
- const json = typeof atob === 'function'
85
- ? atob(padded)
86
- // Hermes always has atob; fallback for older runtimes
87
- : Buffer.from(padded, 'base64').toString('utf8');
88
- return JSON.parse(json);
89
- } catch {
90
- return null;
91
- }
92
- }
93
-
94
75
  export function userFromJwt(token: string): SessionUser | null {
95
76
  const claims = decodeJwtPayload(token);
96
77
  if (!claims?.sub) return null;
package/src/config.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { AttachmentCache } from './storage/types';
12
+ import { tenantFromToken } from './jwt';
12
13
 
13
14
  /** Result of a host-driven token refresh. Mirrors the semantics the API
14
15
  * client depends on: only 'auth_failed' logs the user out; 'transient'
@@ -62,10 +63,9 @@ export interface AdasConfig {
62
63
  * the user belongs to). */
63
64
  consumerAuthUrl?: UrlSource;
64
65
  tokenProvider: TokenProvider;
65
- /** Tenant slug — used for plugin URL resolution and voice, never sent as a
66
- * header (the backend derives the tenant from the JWT). Pass a getter when
67
- * the host resolves the tenant after startup (storage hydration, runtime
68
- * tenant switch). */
66
+ /** The host's tenant PICK — used only while signed out (e.g. the login
67
+ * screen's chosen workspace). A signed-in session's tenant is always the
68
+ * token's `tenant` claim, the one the backend acts on (getTenant()). */
69
69
  tenant?: string | (() => string);
70
70
  /** Current end user as known to the host. actorId is used as the chat
71
71
  * reply context; without it, replies route to 'unknown'. */
@@ -132,7 +132,17 @@ export function getConsumerAuthApiUrl(): string {
132
132
  return resolveUrl(url);
133
133
  }
134
134
 
135
+ /** The tenant the app is scoped to. Signed in: the live token's `tenant` claim,
136
+ * the same tenant every API call acts on, so a plugin URL, the voice session
137
+ * and the header can never name a different workspace than the backend does
138
+ * (2026-09-29: a stored pick still said 'ada' while the token said
139
+ * ateam-mcp-test, and a plugin page 401'd). Signed out: the host's pick, or ''
140
+ * when there is none — never a default tenant. */
135
141
  export function getTenant(): string {
142
+ const token = getConfig().tokenProvider.getToken();
143
+ // Signed in, the token is the ONLY answer: a token without a claim names no
144
+ // tenant (''), never the host's stored pick and never a default.
145
+ if (token) return tenantFromToken(token) || '';
136
146
  const t = getConfig().tenant;
137
147
  return (typeof t === 'function' ? t() : t) || '';
138
148
  }
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ export type { Session, SessionEnv } from './session';
50
50
  // SDK-owned session state machine (R6) — JWT lifecycle, refresh table, restore,
51
51
  // logout orchestration. The host provides secure storage + a logout teardown hook.
52
52
  export { createAuthSession, userFromJwt } from './authSession';
53
+ export { tenantFromToken } from './jwt';
53
54
  export type {
54
55
  AuthSession,
55
56
  AuthSessionState,
package/src/jwt.ts ADDED
@@ -0,0 +1,29 @@
1
+ // JWT projection shared by the session state machine and the tenant getter.
2
+ // Decode only: the token was issued and is verified by the platform; the client
3
+ // reads its claims to know WHO and WHERE, never to trust them for access.
4
+
5
+ export function decodeJwtPayload(token: string): Record<string, any> | null {
6
+ try {
7
+ const segs = token.split('.');
8
+ if (segs.length !== 3) return null;
9
+ // base64url → base64
10
+ const b64 = segs[1].replace(/-/g, '+').replace(/_/g, '/');
11
+ // Pad to a multiple of 4
12
+ const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
13
+ const json = typeof atob === 'function'
14
+ ? atob(padded)
15
+ // Hermes always has atob; fallback for older runtimes
16
+ : Buffer.from(padded, 'base64').toString('utf8');
17
+ return JSON.parse(json);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /** The tenant a signed-in session is scoped to: the token's `tenant` claim, the
24
+ * same value the backend reads. Null for no token or a token without one. */
25
+ export function tenantFromToken(token: string | null | undefined): string | null {
26
+ if (!token) return null;
27
+ const t = decodeJwtPayload(token)?.tenant;
28
+ return typeof t === 'string' && t ? t : null;
29
+ }