@i4e/invest4edu-access-core 0.35.0 → 0.37.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 +3 -0
- package/package.json +3 -2
- package/src/index.js +7 -0
- package/src/mobile-app-identity.js +52 -0
- package/src/report-audience.js +38 -7
package/README.md
CHANGED
|
@@ -112,6 +112,9 @@ Model.find(q).setOptions({ skipTenant: true }); // one query
|
|
|
112
112
|
(`/user-entity-link`) — which hats may coexist on one person
|
|
113
113
|
- `ROLE_TYPE_PRIORITY`, `CLIENT_APP_ROLE_NAMES`, `pickPortalRoleName` (`/portal-roles`) —
|
|
114
114
|
which role names a portal session
|
|
115
|
+
- `MOBILE_LOGIN_SOURCES`, `CLIENT_APP_LOGIN_SOURCES`, `PARTNER_APP_HATS`,
|
|
116
|
+
`APP_BUNDLE_ID_PATTERN`, `appBundleIdOf` (`/mobile-app-identity`) — which mobile app a login
|
|
117
|
+
came from, which hats it flags, and the bundle id its session keeps
|
|
115
118
|
|
|
116
119
|
## Not covered
|
|
117
120
|
**Aggregation pipelines** — add an explicit `{ $match: { account_id } }` stage.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i4e/invest4edu-access-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
|
|
3
|
+
"version": "0.37.0",
|
|
4
|
+
"description": "Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, mobile app identity, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./route-features": "./src/route-features.js",
|
|
23
23
|
"./route-screen": "./src/route-screen.js",
|
|
24
24
|
"./report-audience": "./src/report-audience.js",
|
|
25
|
+
"./mobile-app-identity": "./src/mobile-app-identity.js",
|
|
25
26
|
"./subscription-lifecycle": "./src/subscription-lifecycle.js",
|
|
26
27
|
"./entitlement-store": "./src/entitlement-store.js",
|
|
27
28
|
"./proration": "./src/proration.js",
|
package/src/index.js
CHANGED
|
@@ -35,6 +35,13 @@ export {
|
|
|
35
35
|
ROLE_TYPE_PRIORITY,
|
|
36
36
|
pickPortalRoleName,
|
|
37
37
|
} from "./portal-roles.js";
|
|
38
|
+
export {
|
|
39
|
+
MOBILE_LOGIN_SOURCES,
|
|
40
|
+
CLIENT_APP_LOGIN_SOURCES,
|
|
41
|
+
PARTNER_APP_HATS,
|
|
42
|
+
APP_BUNDLE_ID_PATTERN,
|
|
43
|
+
appBundleIdOf,
|
|
44
|
+
} from "./mobile-app-identity.js";
|
|
38
45
|
export {
|
|
39
46
|
DEFAULT_ACCESS_FLAGS,
|
|
40
47
|
ACCESS_FLAG_ENV_NAMES,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which mobile app a login came from, and what it may record — @i4e/invest4edu-access-core.
|
|
3
|
+
*
|
|
4
|
+
* Two apps sign in to the same backends: the partner app (NeoFinDesk) and the client app
|
|
5
|
+
* (InvestValue AI). A person's hat names the app: a client hat is the client app, every other
|
|
6
|
+
* hat the partner app. Both backends read these to decide which hat a mobile login flags and
|
|
7
|
+
* what bundle id its session row keeps.
|
|
8
|
+
*
|
|
9
|
+
* ── Why this lives in the package rather than in each backend ─────────────────────────
|
|
10
|
+
* nfd-api-node declared these in src/api/common/mobileAppIdentity.js and nfd-api-node-v2 in
|
|
11
|
+
* services/user/user.js, held together by tests pinning each copy's literals (BRI-1263). The
|
|
12
|
+
* sets are fixed by the domain — the mobile platforms and the entity kinds — so they are
|
|
13
|
+
* constants, not reference data; but two copies of a constant are still two definitions.
|
|
14
|
+
*/
|
|
15
|
+
import { ENTITY } from './entity-status.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Login / creation `source` values that mean "a mobile app" without naming which one. The
|
|
19
|
+
* partner app sends these, and so did the client app before it had its own values.
|
|
20
|
+
*/
|
|
21
|
+
export const MOBILE_LOGIN_SOURCES = new Set(['MOBILE_ANDROID', 'MOBILE_IOS', 'MOBILE_APP']);
|
|
22
|
+
|
|
23
|
+
/** Sources only the client app (InvestValue AI) sends: they name the app, not just "mobile". */
|
|
24
|
+
export const CLIENT_APP_LOGIN_SOURCES = new Set(['IV_CLIENT_APP_IOS', 'IV_CLIENT_APP_ANDROID']);
|
|
25
|
+
|
|
26
|
+
/** Hats the partner app (NeoFinDesk) signs in as — every hat except the client one. */
|
|
27
|
+
export const PARTNER_APP_HATS = Object.values(ENTITY).filter((t) => t !== ENTITY.CLIENT);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A bundle id / package name: iOS reverse-DNS (letters, digits, `.`, `-`) and Android
|
|
31
|
+
* (letters, digits, `.`, `_`), at most 255 characters. Anything else — markup, a newline, a
|
|
32
|
+
* spreadsheet formula — is not an app identifier and is not stored.
|
|
33
|
+
*/
|
|
34
|
+
export const APP_BUNDLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The calling app's bundle id for a session row, or null.
|
|
38
|
+
*
|
|
39
|
+
* A login whose source names a non-mobile channel (WEB, PARTNER_API, …) stores null whatever
|
|
40
|
+
* it sends. A login with NO source keeps it: the client app sends none on its OTP, MPIN and
|
|
41
|
+
* biometric logins, and those are exactly the logins the bundle id is for. Checked for shape
|
|
42
|
+
* only, not against an allowlist — a stale app build must still be able to log in.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} body the login request body; reads `source` (or `creation_source`) and `appBundleId`
|
|
45
|
+
* @returns {string|null}
|
|
46
|
+
*/
|
|
47
|
+
export function appBundleIdOf(body) {
|
|
48
|
+
const source = String(body?.source || body?.creation_source || '').trim().toUpperCase();
|
|
49
|
+
if (source && !MOBILE_LOGIN_SOURCES.has(source) && !CLIENT_APP_LOGIN_SOURCES.has(source)) return null;
|
|
50
|
+
const value = typeof body?.appBundleId === 'string' ? body.appBundleId.trim() : '';
|
|
51
|
+
return APP_BUNDLE_ID_PATTERN.test(value) ? value : null;
|
|
52
|
+
}
|
package/src/report-audience.js
CHANGED
|
@@ -58,6 +58,17 @@ export const AUDIENCE = Object.freeze({
|
|
|
58
58
|
*/
|
|
59
59
|
export const SECTION_KEYS = Object.freeze([
|
|
60
60
|
"actions", // the 3-step action plan / recommendations
|
|
61
|
+
/*
|
|
62
|
+
* The per-holding VERDICT — a fund's Buy/Hold/Watch/Exit chip and a stock's signal.
|
|
63
|
+
*
|
|
64
|
+
* Separate from `actions` because they are withheld for the same reason but live in different
|
|
65
|
+
* places: `actions` is one panel, this is a chip on every fund row and every stock row across
|
|
66
|
+
* several tabs. A client who cannot see the action plan but can see "EXIT" against a fund has
|
|
67
|
+
* been given the recommendation anyway, just without the reasoning.
|
|
68
|
+
*
|
|
69
|
+
* Covers BOTH vocabularies — funds carry `action`, stocks carry `signal`.
|
|
70
|
+
*/
|
|
71
|
+
"fundActions",
|
|
61
72
|
"phc", // portfolio health-check detail body
|
|
62
73
|
"chat", // AI chat
|
|
63
74
|
"performance", // performance / gains
|
|
@@ -70,6 +81,7 @@ export const SECTION_KEYS = Object.freeze([
|
|
|
70
81
|
/** Everything on. The answer whenever we cannot prove a narrower one is correct. */
|
|
71
82
|
export const ALL_VISIBLE = Object.freeze({
|
|
72
83
|
actions: true,
|
|
84
|
+
fundActions: true,
|
|
73
85
|
phc: true,
|
|
74
86
|
chat: true,
|
|
75
87
|
performance: true,
|
|
@@ -81,17 +93,36 @@ export const ALL_VISIBLE = Object.freeze({
|
|
|
81
93
|
tone: "adviser",
|
|
82
94
|
});
|
|
83
95
|
|
|
84
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* The reduced view — REVISED 21 Sep 2026.
|
|
98
|
+
*
|
|
99
|
+
* It used to withhold almost the whole report. It now withholds exactly the RECOMMENDATION and
|
|
100
|
+
* nothing else, because the reason for withholding was never "this client should see less": it is
|
|
101
|
+
* that an employee services them and that conversation belongs to the RM. Everything the client
|
|
102
|
+
* can read for themselves — the health detail, performance, fund analysis, the score breakdown,
|
|
103
|
+
* per-fund detail — tells them where they stand without telling them what to do.
|
|
104
|
+
*
|
|
105
|
+
* Two keys carry it, and both are needed: the action plan is one panel, but the per-holding
|
|
106
|
+
* verdict is a chip on every fund and stock row. Withholding the panel alone would hand over the
|
|
107
|
+
* recommendation anyway, minus the reasoning.
|
|
108
|
+
*
|
|
109
|
+
* `chat` stays locked for the same reason, and it is the subtle one: a client can simply ASK the
|
|
110
|
+
* chat whether to sell a fund. Leaving it open would route around both keys above.
|
|
111
|
+
*
|
|
112
|
+
* `tone: b2c` stays: the narrative still speaks to a client rather than an adviser, and it must
|
|
113
|
+
* keep describing rather than prescribing now that it is the main thing they read.
|
|
114
|
+
*/
|
|
85
115
|
const REDUCED = Object.freeze({
|
|
86
116
|
actions: false,
|
|
87
|
-
|
|
117
|
+
fundActions: false,
|
|
88
118
|
chat: false,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
phc: true,
|
|
120
|
+
performance: true,
|
|
121
|
+
keyInsights: true,
|
|
122
|
+
fundAnalysis: true,
|
|
123
|
+
scoreBreakdownFunds: true,
|
|
93
124
|
advisorCta: false,
|
|
94
|
-
fundDetail: "
|
|
125
|
+
fundDetail: "full",
|
|
95
126
|
tone: "b2c",
|
|
96
127
|
});
|
|
97
128
|
|