@startsimpli/auth 0.4.27 → 0.4.28

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": "@startsimpli/auth",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
4
4
  "description": "Shared authentication package for StartSimpli Next.js apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,35 @@
1
+ /**
2
+ * loginAppHint() reads the signin target (?app=<slug>) from the URL so the
3
+ * login token POST can carry it — central auth scopes the minted token's
4
+ * audience by it (startsim-qxv2.5).
5
+ */
6
+ import { describe, it, expect, afterEach } from 'vitest';
7
+ import { loginAppHint } from '../client/functions';
8
+
9
+ function setUrl(search: string) {
10
+ window.history.pushState({}, '', `/signin${search}`);
11
+ }
12
+
13
+ describe('loginAppHint', () => {
14
+ afterEach(() => setUrl(''));
15
+
16
+ it('returns the app slug from ?app=', () => {
17
+ setUrl('?app=foundry&return_to=https%3A%2F%2Ffoundry.startsimpli.com%2F');
18
+ expect(loginAppHint()).toBe('foundry');
19
+ });
20
+
21
+ it('returns the tenant slug for a tenant-app signin', () => {
22
+ setUrl('?app=mcr');
23
+ expect(loginAppHint()).toBe('mcr');
24
+ });
25
+
26
+ it('returns undefined when no app param is present', () => {
27
+ setUrl('?return_to=https%3A%2F%2Fexample.com');
28
+ expect(loginAppHint()).toBeUndefined();
29
+ });
30
+
31
+ it('treats a blank app as absent', () => {
32
+ setUrl('?app=');
33
+ expect(loginAppHint()).toBeUndefined();
34
+ });
35
+ });
@@ -11,7 +11,7 @@ import type {
11
11
  AuthUser,
12
12
  } from '../types';
13
13
  import { isTokenExpired, getTokenExpiresAt, shouldRefreshToken } from '../utils';
14
- import { extractApiError, setAccessToken as setModuleAccessToken } from './functions';
14
+ import { extractApiError, loginAppHint, setAccessToken as setModuleAccessToken } from './functions';
15
15
  import { deleteCookie } from '../utils/cookies';
16
16
  import type { AuthBackend, RegisterPayload } from './backend';
17
17
 
@@ -68,11 +68,12 @@ export class AuthClient implements AuthBackend {
68
68
  * Login with email and password
69
69
  */
70
70
  async login(email: string, password: string): Promise<Session> {
71
+ const app = loginAppHint(); // scope the token audience to the login target (qxv2.5)
71
72
  const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/`, {
72
73
  method: 'POST',
73
74
  headers: { 'Content-Type': 'application/json' },
74
75
  credentials: 'include', // Include cookies for refresh token
75
- body: JSON.stringify({ email, password }),
76
+ body: JSON.stringify({ email, password, ...(app ? { app } : {}) }),
76
77
  });
77
78
 
78
79
  if (!response.ok) {
@@ -59,6 +59,19 @@ export function notifySessionExpired(): void {
59
59
  _onSessionExpired();
60
60
  }
61
61
 
62
+ /**
63
+ * The app/target the user is signing in FOR, read from the signin URL
64
+ * (?app=<slug>). Central auth uses it to scope the minted token's audience:
65
+ * a central/platform login (app=foundry) yields a first-party token; a tenant
66
+ * login (app=<tenant>) yields the tenant audience. Browser-only (undefined
67
+ * off-browser, e.g. SSR). (startsim-qxv2.5)
68
+ */
69
+ export function loginAppHint(): string | undefined {
70
+ if (typeof window === 'undefined') return undefined;
71
+ const app = new URLSearchParams(window.location.search).get('app');
72
+ return app?.trim() || undefined;
73
+ }
74
+
62
75
  // --- Endpoint paths (Django backend defaults) ---
63
76
 
64
77
  const API_BASE = '/api/v1';
@@ -20,6 +20,7 @@
20
20
  import type { AuthUser, Session } from '../types';
21
21
  import { getTokenExpiresAt } from '../utils/token';
22
22
  import { extractApiError } from '../utils/api-error';
23
+ import { loginAppHint } from './functions';
23
24
 
24
25
  /** Persistence for the refresh token. Web uses cookies (and never needs this);
25
26
  * native provides a SecureStore-backed implementation. */
@@ -89,10 +90,11 @@ export class TokenAuthClient {
89
90
  }
90
91
 
91
92
  async login(email: string, password: string): Promise<Session> {
93
+ const app = loginAppHint(); // scope the token audience to the login target (qxv2.5)
92
94
  const response = await this.fetchImpl(`${this.apiBaseUrl}/api/v1/auth/token/`, {
93
95
  method: 'POST',
94
96
  headers: { 'Content-Type': 'application/json', 'X-Auth-Mode': 'token' },
95
- body: JSON.stringify({ email, password }),
97
+ body: JSON.stringify({ email, password, ...(app ? { app } : {}) }),
96
98
  });
97
99
 
98
100
  if (!response.ok) {