@lumerahq/cli 0.30.13 → 0.30.14

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": "@lumerahq/cli",
3
- "version": "0.30.13",
3
+ "version": "0.30.14",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,9 +1,47 @@
1
- import { createContext } from 'react';
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export type ViewerMemberTag = {
4
+ id: string;
5
+ key: string;
6
+ label: string;
7
+ color: string;
8
+ };
9
+
10
+ export type ViewerContext = {
11
+ version: 1;
12
+ isSignedIn: boolean;
13
+ subjectType: 'anonymous' | 'org_member' | 'public_app_user';
14
+ belongsToOrganization: boolean;
15
+ identity: { id: string; name: string; email: string } | null;
16
+ organizationMembership: {
17
+ id: string;
18
+ role: string;
19
+ isCompanyAdmin: boolean;
20
+ department: ViewerMemberTag | null;
21
+ tags: ViewerMemberTag[];
22
+ } | null;
23
+ appUser: { id: string; role: string } | null;
24
+ };
2
25
 
3
26
  export type AuthContextValue = {
4
27
  company?: { id: string; name?: string; apiName?: string };
5
28
  user?: { id: string; name: string; email: string; role?: string };
29
+ viewer: ViewerContext;
6
30
  sessionToken?: string;
7
31
  };
8
32
 
9
33
  export const AuthContext = createContext<AuthContextValue | null>(null);
34
+
35
+ export const useAuth = (): AuthContextValue => {
36
+ const value = useContext(AuthContext);
37
+ if (!value) throw new Error('useAuth must be used inside AuthContext.Provider');
38
+ return value;
39
+ };
40
+
41
+ export const hasMemberTag = (viewer: ViewerContext, key: string): boolean =>
42
+ viewer.organizationMembership?.tags.some(
43
+ (tag) => tag.key.trim().toLowerCase() === key.trim().toLowerCase()
44
+ ) === true;
45
+
46
+ export const isDepartment = (viewer: ViewerContext, key: string): boolean =>
47
+ viewer.organizationMembership?.department?.key.trim().toLowerCase() === key.trim().toLowerCase();
@@ -11,7 +11,12 @@ import { StrictMode, useEffect, useRef, useState } from 'react';
11
11
  import ReactDOM from 'react-dom/client';
12
12
  import { Toaster } from 'sonner';
13
13
 
14
- import { AuthContext, type AuthContextValue } from './lib/auth';
14
+ import {
15
+ AuthContext,
16
+ type AuthContextValue,
17
+ type ViewerContext,
18
+ type ViewerMemberTag,
19
+ } from './lib/auth';
15
20
  import { routeTree } from './routeTree.gen';
16
21
  import './styles.css';
17
22
 
@@ -39,6 +44,117 @@ declare module '@tanstack/react-router' {
39
44
 
40
45
  const ROUTE_STORAGE_KEY = '{{projectName}}-route';
41
46
 
47
+ type ViewerHostPayload = HostPayload & { viewer?: ViewerContext };
48
+
49
+ type ViewerResponse = {
50
+ version?: number;
51
+ is_signed_in?: boolean;
52
+ subject_type?: 'anonymous' | 'org_member' | 'public_app_user';
53
+ belongs_to_organization?: boolean;
54
+ identity?: { id?: string; name?: string; email?: string } | null;
55
+ organization_membership?: {
56
+ id?: string;
57
+ role?: string;
58
+ is_company_admin?: boolean;
59
+ department?: ViewerMemberTag | null;
60
+ tags?: ViewerMemberTag[];
61
+ } | null;
62
+ app_user?: { id?: string; role?: string } | null;
63
+ };
64
+
65
+ const viewerFromPayload = (payload: ViewerHostPayload): ViewerContext => {
66
+ if (payload.viewer) return payload.viewer;
67
+ const user = payload.user;
68
+ if (user && (payload.subjectType === 'app_user' || payload.subjectType === 'public_app_user')) {
69
+ return {
70
+ version: 1,
71
+ isSignedIn: true,
72
+ subjectType: 'public_app_user',
73
+ belongsToOrganization: false,
74
+ identity: { id: user.id, name: user.name, email: user.email },
75
+ organizationMembership: null,
76
+ appUser: { id: user.id, role: user.role ?? '' },
77
+ };
78
+ }
79
+ if (user) {
80
+ return {
81
+ version: 1,
82
+ isSignedIn: true,
83
+ subjectType: 'org_member',
84
+ belongsToOrganization: true,
85
+ identity: { id: user.id, name: user.name, email: user.email },
86
+ organizationMembership: {
87
+ id: user.id,
88
+ role: user.role ?? '',
89
+ isCompanyAdmin: user.role?.toLowerCase() === 'admin',
90
+ department: null,
91
+ tags: [],
92
+ },
93
+ appUser: null,
94
+ };
95
+ }
96
+ return {
97
+ version: 1,
98
+ isSignedIn: false,
99
+ subjectType: 'anonymous',
100
+ belongsToOrganization: false,
101
+ identity: null,
102
+ organizationMembership: null,
103
+ appUser: null,
104
+ };
105
+ };
106
+
107
+ const viewerFromResponse = (response: ViewerResponse): ViewerContext => ({
108
+ version: 1,
109
+ isSignedIn: response.is_signed_in === true,
110
+ subjectType: response.subject_type ?? 'anonymous',
111
+ belongsToOrganization: response.belongs_to_organization === true,
112
+ identity: response.identity
113
+ ? {
114
+ id: response.identity.id ?? '',
115
+ name: response.identity.name ?? '',
116
+ email: response.identity.email ?? '',
117
+ }
118
+ : null,
119
+ organizationMembership: response.organization_membership
120
+ ? {
121
+ id: response.organization_membership.id ?? '',
122
+ role: response.organization_membership.role ?? '',
123
+ isCompanyAdmin: response.organization_membership.is_company_admin === true,
124
+ department: response.organization_membership.department ?? null,
125
+ tags: response.organization_membership.tags ?? [],
126
+ }
127
+ : null,
128
+ appUser: response.app_user
129
+ ? {
130
+ id: response.app_user.id ?? '',
131
+ role: response.app_user.role ?? '',
132
+ }
133
+ : null,
134
+ });
135
+
136
+ const resolveViewer = async (payload: ViewerHostPayload): Promise<ViewerContext> => {
137
+ if (payload.viewer) return payload.viewer;
138
+
139
+ // The checked-in lockfile can briefly trail a newly published UI bridge.
140
+ // Keep standalone apps complete during that release window by reading the
141
+ // canonical block from the same authenticated /api/me endpoint.
142
+ if (!isEmbedded()) {
143
+ try {
144
+ const response = await fetch('/api/me', { credentials: 'same-origin' });
145
+ if (response.ok) {
146
+ const body = (await response.json()) as { viewer?: ViewerResponse };
147
+ if (body.viewer) return viewerFromResponse(body.viewer);
148
+ }
149
+ } catch {
150
+ // Compatibility fallback below preserves the pre-viewer behavior when
151
+ // an older app host does not yet expose the canonical response.
152
+ }
153
+ }
154
+
155
+ return viewerFromPayload(payload);
156
+ };
157
+
42
158
  function RouteRestorer() {
43
159
  const isFirstLoad = useRef(true);
44
160
 
@@ -75,7 +191,8 @@ const App = () => {
75
191
  // in the Lumera shell, same-origin /api with the app-session cookie when
76
192
  // served standalone on the app's own hostname, and direct dev fetch when
77
193
  // VITE_DEV_API_BASE_URL is set.
78
- const cleanup = onInitMessage((payload?: HostPayload) => {
194
+ const cleanup = onInitMessage((rawPayload?: HostPayload) => {
195
+ const payload = rawPayload as ViewerHostPayload | undefined;
79
196
  if (!payload) {
80
197
  // No identity and no app context: authentication is required
81
198
  // (payload.user alone being undefined means an anonymous visitor on
@@ -83,12 +200,15 @@ const App = () => {
83
200
  setStatus('unauthenticated');
84
201
  return;
85
202
  }
86
- setHostContext({
87
- company: payload.company,
88
- user: payload.user,
89
- sessionToken: payload.session?.token,
203
+ void resolveViewer(payload).then((viewer) => {
204
+ setHostContext({
205
+ company: payload.company,
206
+ user: payload.user,
207
+ viewer,
208
+ sessionToken: payload.session?.token,
209
+ });
210
+ setStatus('ready');
90
211
  });
91
- setStatus('ready');
92
212
  });
93
213
 
94
214
  postReadyMessage();