openxiangda-cli 2.0.0-alpha.84 → 2.0.0-alpha.85

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": "openxiangda-cli",
3
- "version": "2.0.0-alpha.84",
3
+ "version": "2.0.0-alpha.85",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@oclif/core": "4.13.3",
26
- "openxiangda-contracts": "2.0.0-alpha.38",
27
- "openxiangda-devkit-core": "2.0.0-alpha.51",
28
- "openxiangda-mcp": "2.0.0-alpha.51"
26
+ "openxiangda-contracts": "2.0.0-alpha.39",
27
+ "openxiangda-devkit-core": "2.0.0-alpha.52",
28
+ "openxiangda-mcp": "2.0.0-alpha.52"
29
29
  },
30
30
  "devDependencies": {
31
31
  "tsx": "4.23.12",
@@ -15,7 +15,7 @@
15
15
  "@nestjs/common": "11.1.29",
16
16
  "@nestjs/core": "11.1.29",
17
17
  "@nestjs/platform-fastify": "11.1.29",
18
- "openxiangda-nest": "2.0.0-alpha.47",
18
+ "openxiangda-nest": "2.0.0-alpha.48",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -16,7 +16,7 @@
16
16
  "antd": "6.4.3",
17
17
  "dayjs": "1.11.18",
18
18
  "docx-preview": "0.3.7",
19
- "openxiangda-contracts": "2.0.0-alpha.38",
19
+ "openxiangda-contracts": "2.0.0-alpha.39",
20
20
  "react": "19.2.8",
21
21
  "react-dom": "19.2.8",
22
22
  "react-router-dom": "7.8.2",
@@ -13,6 +13,7 @@ import {
13
13
  type DataTransactionResult,
14
14
  type DirectoryEntryPage,
15
15
  type DirectoryResolveRequest,
16
+ type CurrentUser,
16
17
  } from 'openxiangda-contracts/browser';
17
18
  import { applicationCode, runtimeMount } from './runtime-meta';
18
19
  import { useSyncExternalStore } from 'react';
@@ -85,6 +86,13 @@ export interface RuntimeIdentity {
85
86
  roleCodes: string[];
86
87
  capabilityCodes: string[];
87
88
  isAppSuperAdmin: boolean;
89
+ authorization?: {
90
+ mode: 'union' | 'focused_role';
91
+ focusedRoleCode: string | null;
92
+ availableRoles: Array<{ code: string; name: string }>;
93
+ hasAppSuperAdminGrant: boolean;
94
+ contextRevision: number;
95
+ };
88
96
  environment: {
89
97
  id: string;
90
98
  key: 'preproduction' | 'production';
@@ -101,12 +109,7 @@ interface ConnectedCurrent {
101
109
  principal: Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
102
110
  }
103
111
 
104
- interface DeployedCurrent {
105
- schemaVersion: 'openxiangda.current-user/v2';
106
- appCode: string;
107
- environment: RuntimeIdentity['environment'];
108
- principal: { type: 'user' } & Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
109
- }
112
+ type DeployedCurrent = CurrentUser;
110
113
 
111
114
  let activeIdentity: RuntimeIdentity | undefined;
112
115
 
@@ -117,14 +120,18 @@ export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
117
120
  `${nativeBase()}/current-user?environmentKey=${encodeURIComponent(mount.environmentKey)}`
118
121
  );
119
122
  if (
120
- current.schemaVersion !== 'openxiangda.current-user/v2' ||
123
+ current.schemaVersion !== SCHEMA_VERSIONS.currentUser ||
121
124
  current.appCode !== mount.appCode ||
122
125
  current.environment.key !== mount.environmentKey ||
123
126
  current.principal.type !== 'user'
124
127
  ) {
125
128
  throw new Error('OPENXIANGDA_DEPLOYED_CURRENT_USER_INVALID');
126
129
  }
127
- activeIdentity = { ...current.principal, environment: current.environment };
130
+ activeIdentity = {
131
+ ...current.principal,
132
+ environment: current.environment,
133
+ authorization: current.authorization,
134
+ };
128
135
  return activeIdentity;
129
136
  }
130
137
  const current = await request<ConnectedCurrent>(`/service/openxiangda-api/v2/applications/${applicationCode()}/dev-sessions/current`);
@@ -132,6 +139,68 @@ export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
132
139
  return activeIdentity;
133
140
  }
134
141
 
142
+ const AUTHORIZATION_CHANNEL = 'openxiangda.authorization-context/v2';
143
+ const AUTHORIZATION_STORAGE_KEY =
144
+ 'openxiangda.authorization-context-invalidated/v2';
145
+
146
+ type AuthorizationInvalidation = {
147
+ type: 'authorization-context-invalidated';
148
+ appCode: string;
149
+ environmentKey: 'preproduction' | 'production';
150
+ contextRevision: number;
151
+ recordedAt: number;
152
+ };
153
+
154
+ function validAuthorizationInvalidation(
155
+ value: unknown
156
+ ): value is AuthorizationInvalidation {
157
+ if (!value || typeof value !== 'object') return false;
158
+ const candidate = value as Partial<AuthorizationInvalidation>;
159
+ return (
160
+ candidate.type === 'authorization-context-invalidated' &&
161
+ typeof candidate.appCode === 'string' &&
162
+ (candidate.environmentKey === 'preproduction' ||
163
+ candidate.environmentKey === 'production') &&
164
+ Number.isSafeInteger(candidate.contextRevision) &&
165
+ typeof candidate.recordedAt === 'number'
166
+ );
167
+ }
168
+
169
+ export function subscribeAuthorizationInvalidation(listener: () => void) {
170
+ const mount = runtimeMount();
171
+ if (!mount || typeof window === 'undefined') return () => undefined;
172
+ const receive = (value: unknown) => {
173
+ if (
174
+ validAuthorizationInvalidation(value) &&
175
+ value.appCode === mount.appCode &&
176
+ value.environmentKey === mount.environmentKey
177
+ ) {
178
+ activeIdentity = undefined;
179
+ listener();
180
+ }
181
+ };
182
+ let channel: BroadcastChannel | undefined;
183
+ try {
184
+ channel = new BroadcastChannel(AUTHORIZATION_CHANNEL);
185
+ channel.onmessage = event => receive(event.data);
186
+ } catch {
187
+ channel = undefined;
188
+ }
189
+ const onStorage = (event: StorageEvent) => {
190
+ if (event.key !== AUTHORIZATION_STORAGE_KEY || !event.newValue) return;
191
+ try {
192
+ receive(JSON.parse(event.newValue));
193
+ } catch {
194
+ // Invalid browser signals never alter the server-owned identity.
195
+ }
196
+ };
197
+ window.addEventListener('storage', onStorage);
198
+ return () => {
199
+ channel?.close();
200
+ window.removeEventListener('storage', onStorage);
201
+ };
202
+ }
203
+
135
204
  export async function logoutCurrentUser() {
136
205
  await request<null>('/api/auth/logout', {
137
206
  method: 'POST',
@@ -1,6 +1,10 @@
1
1
  import { Alert, Button, Result, Spin } from 'antd';
2
2
  import { createContext, useContext, useEffect, useMemo, useState } from 'react';
3
- import { loadCurrentIdentity, type RuntimeIdentity } from './platform-client';
3
+ import {
4
+ loadCurrentIdentity,
5
+ subscribeAuthorizationInvalidation,
6
+ type RuntimeIdentity,
7
+ } from './platform-client';
4
8
  import { runtimeMount } from './runtime-meta';
5
9
 
6
10
  interface RuntimeValue {
@@ -14,6 +18,15 @@ export function RuntimeBoundary({ children }: { children: React.ReactNode }) {
14
18
  const [identity, setIdentity] = useState<RuntimeIdentity>();
15
19
  const [error, setError] = useState<Error>();
16
20
  const [attempt, setAttempt] = useState(0);
21
+ useEffect(
22
+ () =>
23
+ subscribeAuthorizationInvalidation(() => {
24
+ setIdentity(undefined);
25
+ setError(undefined);
26
+ setAttempt(value => value + 1);
27
+ }),
28
+ []
29
+ );
17
30
  useEffect(() => {
18
31
  let active = true;
19
32
  setError(undefined);
@@ -22,9 +22,9 @@
22
22
  "build": "pnpm --recursive build"
23
23
  },
24
24
  "devDependencies": {
25
- "openxiangda-cli": "2.0.0-alpha.84",
26
- "openxiangda-contracts": "2.0.0-alpha.38",
27
- "openxiangda-devkit-core": "2.0.0-alpha.51",
25
+ "openxiangda-cli": "2.0.0-alpha.85",
26
+ "openxiangda-contracts": "2.0.0-alpha.39",
27
+ "openxiangda-devkit-core": "2.0.0-alpha.52",
28
28
  "typescript": "5.9.3"
29
29
  }
30
30
  }