@pulog/contract 0.0.0-stage → 0.1.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 +13 -2
- package/dist/index.d.ts +196 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -4
package/README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pulog/contract
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Host <-> remote prop contract: `RemoteAppProps`, `CONTRACT_VERSION`, user/master types, maintenance types and the
|
|
4
|
+
standalone handoff protocol. Dependency-free.
|
|
5
|
+
|
|
6
|
+
Source: `src/contract.ts`, copied byte-for-byte from `tms_host/src/types/contract.ts`.
|
|
7
|
+
|
|
8
|
+
**Status:** prepared, not consumed yet. `tms_host/src/types/contract.ts` is still the master and remotes still use
|
|
9
|
+
`scripts/sync-contract.mjs`. When the host migrates, move the master here, delete the sync scripts, and bump
|
|
10
|
+
`CONTRACT_VERSION` only for breaking prop/semantics changes (see the workspace CLAUDE.md).
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { CONTRACT_VERSION, type RemoteAppProps } from '@pulog/contract'
|
|
14
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bump when Host↔Remote props/semantics change in a way remotes must notice.
|
|
3
|
+
* Remotes without a declared version are treated as compatible with v1 (non-breaking).
|
|
4
|
+
* Host must not hard-fail on missing/older versions in production without a migration plan.
|
|
5
|
+
*/
|
|
6
|
+
declare const CONTRACT_VERSION = 1;
|
|
7
|
+
interface User {
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
role?: string;
|
|
11
|
+
}
|
|
12
|
+
interface UserModule {
|
|
13
|
+
module: string;
|
|
14
|
+
role: string;
|
|
15
|
+
key_module: string;
|
|
16
|
+
url: string;
|
|
17
|
+
image_module: string;
|
|
18
|
+
platform: string;
|
|
19
|
+
id_module: number;
|
|
20
|
+
local_url: string;
|
|
21
|
+
api_url: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
interface DecodedUser {
|
|
25
|
+
username: string;
|
|
26
|
+
session_token: string;
|
|
27
|
+
full_name?: string;
|
|
28
|
+
nik?: string;
|
|
29
|
+
phone?: string;
|
|
30
|
+
wa_number?: string;
|
|
31
|
+
type?: string;
|
|
32
|
+
entity?: string;
|
|
33
|
+
division?: string;
|
|
34
|
+
department?: string;
|
|
35
|
+
position?: string;
|
|
36
|
+
email?: string;
|
|
37
|
+
address?: string;
|
|
38
|
+
image_url?: string;
|
|
39
|
+
project_name?: string;
|
|
40
|
+
work_location?: string;
|
|
41
|
+
[key: string]: any;
|
|
42
|
+
}
|
|
43
|
+
/** User profile returned by the SSO check-general-user endpoint. */
|
|
44
|
+
interface SSOUserData {
|
|
45
|
+
id: number;
|
|
46
|
+
username: string;
|
|
47
|
+
full_name: string;
|
|
48
|
+
email: string;
|
|
49
|
+
is_confirm: boolean;
|
|
50
|
+
device_id: string | null;
|
|
51
|
+
type: string;
|
|
52
|
+
entity: string;
|
|
53
|
+
nik: string;
|
|
54
|
+
fullname: string;
|
|
55
|
+
address: string | null;
|
|
56
|
+
phone: string;
|
|
57
|
+
wa_number: string;
|
|
58
|
+
is_active: boolean;
|
|
59
|
+
image_url: string | null;
|
|
60
|
+
division: string;
|
|
61
|
+
department: string;
|
|
62
|
+
position: string;
|
|
63
|
+
customer_slug: string;
|
|
64
|
+
last_update_password_date: string;
|
|
65
|
+
project_name: string | null;
|
|
66
|
+
work_location: string | null;
|
|
67
|
+
mst_client_id: number[];
|
|
68
|
+
mst_entity_id: number[];
|
|
69
|
+
[key: string]: any;
|
|
70
|
+
}
|
|
71
|
+
interface MasterClient {
|
|
72
|
+
id?: number;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
interface MasterEntity {
|
|
76
|
+
id: number;
|
|
77
|
+
mst_client_id: number;
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
}
|
|
80
|
+
interface MasterCustomer {
|
|
81
|
+
id: number;
|
|
82
|
+
erp_account_number: string;
|
|
83
|
+
erp_customer_id: string;
|
|
84
|
+
customer_name: string;
|
|
85
|
+
mst_entity_id?: string[];
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
interface MenuItem {
|
|
89
|
+
id: number;
|
|
90
|
+
remote_access: string;
|
|
91
|
+
base_url: string;
|
|
92
|
+
menu_name: string;
|
|
93
|
+
menu_url: string;
|
|
94
|
+
menu_icon: string;
|
|
95
|
+
menu_parent: number;
|
|
96
|
+
menu_order: number;
|
|
97
|
+
visibility: boolean | string;
|
|
98
|
+
component: string;
|
|
99
|
+
access: string[];
|
|
100
|
+
/** Menu-level maintenance flag set via tms_remote_access (Kelola Menu). */
|
|
101
|
+
maintenance?: boolean;
|
|
102
|
+
children?: MenuItem[];
|
|
103
|
+
}
|
|
104
|
+
type MaintenanceScope = 'remote' | 'menu';
|
|
105
|
+
interface MaintenanceSchedule {
|
|
106
|
+
scope: MaintenanceScope;
|
|
107
|
+
/** Remote slug (remote_access) for scope 'remote', or menu_url for scope 'menu'. */
|
|
108
|
+
target: string;
|
|
109
|
+
/** Server timestamp (epoch ms) when maintenance becomes active. */
|
|
110
|
+
startsAt: number;
|
|
111
|
+
/** Estimated completion timestamp (epoch ms). */
|
|
112
|
+
endsAt?: number;
|
|
113
|
+
/** Countdown length the admin configured (e.g. 60). */
|
|
114
|
+
noticeSeconds: number;
|
|
115
|
+
reason?: string;
|
|
116
|
+
/** Admin-authored maintenance message (HTML from the remark rich-text editor) — sanitize before rendering. */
|
|
117
|
+
remark?: string;
|
|
118
|
+
}
|
|
119
|
+
/** SSE event names broadcast by the backend (authoritative set). */
|
|
120
|
+
type MaintenanceEventType = 'warning' | 'countdown' | 'active' | 'resolved';
|
|
121
|
+
interface RemoteAppProps {
|
|
122
|
+
user: User | null;
|
|
123
|
+
token: string | null;
|
|
124
|
+
currentPath: string;
|
|
125
|
+
onLogout: () => void;
|
|
126
|
+
onNavigate?: (path: string) => void;
|
|
127
|
+
userModules?: UserModule[];
|
|
128
|
+
currentModule?: UserModule;
|
|
129
|
+
ssoData?: DecodedUser;
|
|
130
|
+
userData?: SSOUserData;
|
|
131
|
+
clientData?: MasterClient[];
|
|
132
|
+
entityData?: MasterEntity[];
|
|
133
|
+
customerData?: MasterCustomer[];
|
|
134
|
+
/** Active role selected via the host's role switcher (auth.roleActive). */
|
|
135
|
+
roleActive?: string;
|
|
136
|
+
menus?: MenuItem[];
|
|
137
|
+
debugSource?: 'host' | 'standalone';
|
|
138
|
+
isDark?: boolean;
|
|
139
|
+
palette?: string;
|
|
140
|
+
/** BCP 47 locale selected by the host (e.g. `en`, `id-ID`). */
|
|
141
|
+
locale?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Optional. Host may send CONTRACT_VERSION so remotes can log/adapt.
|
|
144
|
+
* Omitted by older hosts — remotes must treat missing as v1-compatible.
|
|
145
|
+
*/
|
|
146
|
+
contractVersion?: number;
|
|
147
|
+
}
|
|
148
|
+
/** Query param carrying only a correlation nonce — never auth data. */
|
|
149
|
+
declare const MFE_HANDOFF_QUERY_PARAM = "mfe_handoff";
|
|
150
|
+
/** remote -> host: "I'm loaded and ready to receive the payload." */
|
|
151
|
+
declare const MFE_HANDOFF_READY = "mfe:handoff-ready";
|
|
152
|
+
/** host -> remote: the actual session/masterdata payload. */
|
|
153
|
+
declare const MFE_HANDOFF_PAYLOAD = "mfe:handoff-payload";
|
|
154
|
+
/** How long the host waits for a "ready" reply (and the remote waits for the payload) before giving up. */
|
|
155
|
+
declare const MFE_HANDOFF_TIMEOUT_MS = 9000;
|
|
156
|
+
interface MfeHandoffPayload {
|
|
157
|
+
user?: User | null;
|
|
158
|
+
token?: string | null;
|
|
159
|
+
/** JWT from the host's auth/token API (localStorage `tms_jwt_token`). Persisted by the
|
|
160
|
+
* remote under its own `tms_jwt_token` key, never inside `tms_auth`. */
|
|
161
|
+
jwtToken?: string | null;
|
|
162
|
+
ssoData?: DecodedUser;
|
|
163
|
+
userData?: SSOUserData;
|
|
164
|
+
clientData?: MasterClient[];
|
|
165
|
+
entityData?: MasterEntity[];
|
|
166
|
+
customerData?: MasterCustomer[];
|
|
167
|
+
userModules?: UserModule[];
|
|
168
|
+
currentModule?: UserModule;
|
|
169
|
+
/** Active role selected via the host's role switcher (auth.roleActive). */
|
|
170
|
+
roleActive?: string;
|
|
171
|
+
menus?: MenuItem[];
|
|
172
|
+
/** Same trio the host already passes when embedding (RemoteAppProps) — sent here
|
|
173
|
+
* so a standalone tab opened from the sidebar starts from the host's state too.
|
|
174
|
+
* Remotes are free to ignore them (theme stays local in standalone). */
|
|
175
|
+
isDark?: boolean;
|
|
176
|
+
palette?: string;
|
|
177
|
+
/** BCP 47 locale selected by the host (e.g. `en`, `id-ID`). */
|
|
178
|
+
locale?: string;
|
|
179
|
+
/** Host contract version at handoff time; omit to mean v1-compatible. */
|
|
180
|
+
contractVersion?: number;
|
|
181
|
+
}
|
|
182
|
+
interface MfeHandoffReadyMessage {
|
|
183
|
+
type: typeof MFE_HANDOFF_READY;
|
|
184
|
+
nonce: string;
|
|
185
|
+
}
|
|
186
|
+
interface MfeHandoffPayloadMessage {
|
|
187
|
+
type: typeof MFE_HANDOFF_PAYLOAD;
|
|
188
|
+
nonce: string;
|
|
189
|
+
payload: MfeHandoffPayload;
|
|
190
|
+
}
|
|
191
|
+
declare function isMfeHandoffReadyMessage(data: unknown): data is MfeHandoffReadyMessage;
|
|
192
|
+
declare function isMfeHandoffPayloadMessage(data: unknown): data is MfeHandoffPayloadMessage;
|
|
193
|
+
declare function sanitizeHandoffPayload(payload: Record<string, unknown>): MfeHandoffPayload;
|
|
194
|
+
|
|
195
|
+
export { CONTRACT_VERSION, MFE_HANDOFF_PAYLOAD, MFE_HANDOFF_QUERY_PARAM, MFE_HANDOFF_READY, MFE_HANDOFF_TIMEOUT_MS, isMfeHandoffPayloadMessage, isMfeHandoffReadyMessage, sanitizeHandoffPayload };
|
|
196
|
+
export type { DecodedUser, MaintenanceEventType, MaintenanceSchedule, MaintenanceScope, MasterClient, MasterCustomer, MasterEntity, MenuItem, MfeHandoffPayload, MfeHandoffPayloadMessage, MfeHandoffReadyMessage, RemoteAppProps, SSOUserData, User, UserModule };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/contract.ts
|
|
2
|
+
var CONTRACT_VERSION = 1;
|
|
3
|
+
var MFE_HANDOFF_QUERY_PARAM = "mfe_handoff";
|
|
4
|
+
var MFE_HANDOFF_READY = "mfe:handoff-ready";
|
|
5
|
+
var MFE_HANDOFF_PAYLOAD = "mfe:handoff-payload";
|
|
6
|
+
var MFE_HANDOFF_TIMEOUT_MS = 9e3;
|
|
7
|
+
function isPlainRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function isMfeHandoffReadyMessage(data) {
|
|
11
|
+
return isPlainRecord(data) && data.type === MFE_HANDOFF_READY && typeof data.nonce === "string";
|
|
12
|
+
}
|
|
13
|
+
function isMfeHandoffPayloadMessage(data) {
|
|
14
|
+
return isPlainRecord(data) && data.type === MFE_HANDOFF_PAYLOAD && typeof data.nonce === "string" && isPlainRecord(data.payload);
|
|
15
|
+
}
|
|
16
|
+
function sanitizeHandoffPayload(payload) {
|
|
17
|
+
const clean = {};
|
|
18
|
+
for (const key of Object.keys(payload)) {
|
|
19
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
20
|
+
clean[key] = payload[key];
|
|
21
|
+
}
|
|
22
|
+
return clean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { CONTRACT_VERSION, MFE_HANDOFF_PAYLOAD, MFE_HANDOFF_QUERY_PARAM, MFE_HANDOFF_READY, MFE_HANDOFF_TIMEOUT_MS, isMfeHandoffPayloadMessage, isMfeHandoffReadyMessage, sanitizeHandoffPayload };
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contract.ts"],"names":[],"mappings":";AAsBO,IAAM,gBAAA,GAAmB;AA2MzB,IAAM,uBAAA,GAA0B;AAEhC,IAAM,iBAAA,GAAoB;AAE1B,IAAM,mBAAA,GAAsB;AAE5B,IAAM,sBAAA,GAAyB;AAwCtC,SAAS,cAAc,KAAA,EAAkD;AACvE,EAAA,OAAO,OAAO,UAAU,QAAA,IAAY,KAAA,KAAU,QAAQ,CAAC,KAAA,CAAM,QAAQ,KAAK,CAAA;AAC5E;AAQO,SAAS,yBAAyB,IAAA,EAA+C;AACtF,EAAA,OAAO,aAAA,CAAc,IAAI,CAAA,IAAK,IAAA,CAAK,SAAS,iBAAA,IAAqB,OAAO,KAAK,KAAA,KAAU,QAAA;AACzF;AAEO,SAAS,2BAA2B,IAAA,EAAiD;AAC1F,EAAA,OACE,aAAA,CAAc,IAAI,CAAA,IAClB,IAAA,CAAK,IAAA,KAAS,mBAAA,IACd,OAAO,IAAA,CAAK,KAAA,KAAU,QAAA,IACtB,aAAA,CAAc,IAAA,CAAK,OAAO,CAAA;AAE9B;AAMO,SAAS,uBAAuB,OAAA,EAAqD;AAC1F,EAAA,MAAM,QAAiC,EAAC;AACxC,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,EAAG;AACtC,IAAA,IAAI,GAAA,KAAQ,WAAA,IAAe,GAAA,KAAQ,aAAA,IAAiB,QAAQ,WAAA,EAAa;AACzE,IAAA,KAAA,CAAM,GAAG,CAAA,GAAI,OAAA,CAAQ,GAAG,CAAA;AAAA,EAC1B;AACA,EAAA,OAAO,KAAA;AACT","file":"index.js","sourcesContent":["// ─────────────────────────────────────────────────────────────────────────────\r\n// HOST ↔ REMOTE CONTRACT — SINGLE SOURCE OF TRUTH\r\n//\r\n// Master copy: mfe-host/src/types/contract.ts\r\n// Synced copies (must stay byte-identical):\r\n// - tms_billing/src/types/contract.ts\r\n// - tms_master_data/src/types/contract.ts\r\n// - tms_remote_access/src/types/contract.ts\r\n//\r\n// Do not edit a remote's copy directly — edit the master here, then run\r\n// `node scripts/sync-contract.mjs --write` in each remote. A sync-check\r\n// (`node scripts/sync-contract.mjs` or `npm run check:contract`) compares the\r\n// two and fails on drift.\r\n//\r\n// Keep this file dependency-free.\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Bump when Host↔Remote props/semantics change in a way remotes must notice.\r\n * Remotes without a declared version are treated as compatible with v1 (non-breaking).\r\n * Host must not hard-fail on missing/older versions in production without a migration plan.\r\n */\r\nexport const CONTRACT_VERSION = 1\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// PERFORMANCE CONTRACT (applies to every remote and to the host mount site)\r\n//\r\n// 1. Every context Provider mounted above/around a remote-facing tree MUST wrap\r\n// its `value={...}` in useMemo, and every function in that value MUST be\r\n// useCallback-wrapped. Reference implementations: tms_host's\r\n// src/components/ui/sidebar.tsx (SidebarContext) and tms_master_data's\r\n// src/context/MenuAccessContext.tsx (MenuAccessContext).\r\n// 2. Any prop crossing the Host→Remote boundary (RemoteAppProps below) that is\r\n// an array, object, or function MUST be referentially stable across\r\n// re-renders that do not change its logical value — i.e. it must come from\r\n// useMemo/useCallback or from state that is only replaced on real updates,\r\n// never from an inline literal or arrow function in JSX.\r\n// 3. The remote's exposed `./App` component MUST be wrapped in React.memo.\r\n// The host's <RemoteApp> mount wrapper MUST also be wrapped in React.memo.\r\n// 4. Background/polling work (analytics, telemetry, sync) MUST: (a) run via\r\n// requestIdleCallback (or an equivalent yield point), never synchronously\r\n// on the critical path of a mount effect; (b) enforce a retention/eviction\r\n// bound on any client-side storage it writes to; (c) use indexed queries\r\n// (e.g. Dexie .where()) instead of .filter() over an unbounded collection.\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n\r\nexport interface User {\r\n name: string\r\n email: string\r\n role?: string\r\n}\r\n\r\nexport interface UserModule {\r\n module: string\r\n role: string\r\n key_module: string\r\n url: string\r\n image_module: string\r\n platform: string\r\n id_module: number\r\n local_url: string\r\n api_url: string\r\n [key: string]: any\r\n}\r\n\r\nexport interface DecodedUser {\r\n username: string\r\n session_token: string\r\n full_name?: string\r\n nik?: string\r\n phone?: string\r\n wa_number?: string\r\n type?: string\r\n entity?: string\r\n division?: string\r\n department?: string\r\n position?: string\r\n email?: string\r\n address?: string\r\n image_url?: string\r\n project_name?: string\r\n work_location?: string\r\n [key: string]: any\r\n}\r\n\r\n/** User profile returned by the SSO check-general-user endpoint. */\r\nexport interface SSOUserData {\r\n id: number\r\n username: string\r\n full_name: string\r\n email: string\r\n is_confirm: boolean\r\n device_id: string | null\r\n type: string\r\n entity: string\r\n nik: string\r\n fullname: string\r\n address: string | null\r\n phone: string\r\n wa_number: string\r\n is_active: boolean\r\n image_url: string | null\r\n division: string\r\n department: string\r\n position: string\r\n customer_slug: string\r\n last_update_password_date: string\r\n project_name: string | null\r\n work_location: string | null\r\n mst_client_id: number[]\r\n mst_entity_id: number[]\r\n [key: string]: any\r\n}\r\n\r\nexport interface MasterClient {\r\n id?: number\r\n [key: string]: unknown\r\n}\r\n\r\nexport interface MasterEntity {\r\n id: number\r\n mst_client_id: number\r\n [key: string]: unknown\r\n}\r\n\r\nexport interface MasterCustomer {\r\n id: number\r\n erp_account_number: string\r\n erp_customer_id: string\r\n customer_name: string\r\n // API returns a Postgres array literal string (e.g. \"{1,2}\"); host normalizes it to\r\n // a plain string array (e.g. [\"1\",\"2\"]) before this reaches remotes.\r\n mst_entity_id?: string[]\r\n [key: string]: unknown\r\n}\r\n\r\n\r\nexport interface MenuItem {\r\n id: number\r\n remote_access: string\r\n base_url: string\r\n menu_name: string\r\n menu_url: string\r\n menu_icon: string\r\n menu_parent: number\r\n menu_order: number\r\n visibility: boolean | string\r\n component: string\r\n access: string[]\r\n /** Menu-level maintenance flag set via tms_remote_access (Kelola Menu). */\r\n maintenance?: boolean\r\n children?: MenuItem[]\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// MAINTENANCE SCHEDULING (host)\r\n//\r\n// Super admin schedules maintenance (remote- or menu-level) via tms_remote_access\r\n// by submitting the existing save endpoints with extra `countdown` +\r\n// `estimate_time` payload. The backend then broadcasts SSE events\r\n// (WARNING → COUNTDOWN ticks → ACTIVE → RESOLVED) carrying absolute server\r\n// timestamps; the host derives the schedule from those events.\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n\r\nexport type MaintenanceScope = 'remote' | 'menu'\r\n\r\nexport interface MaintenanceSchedule {\r\n scope: MaintenanceScope\r\n /** Remote slug (remote_access) for scope 'remote', or menu_url for scope 'menu'. */\r\n target: string\r\n /** Server timestamp (epoch ms) when maintenance becomes active. */\r\n startsAt: number\r\n /** Estimated completion timestamp (epoch ms). */\r\n endsAt?: number\r\n /** Countdown length the admin configured (e.g. 60). */\r\n noticeSeconds: number\r\n reason?: string\r\n /** Admin-authored maintenance message (HTML from the remark rich-text editor) — sanitize before rendering. */\r\n remark?: string\r\n}\r\n\r\n/** SSE event names broadcast by the backend (authoritative set). */\r\nexport type MaintenanceEventType = 'warning' | 'countdown' | 'active' | 'resolved'\r\n\r\n// Props passed Host → Remote (received by the remote's exposed `App`).\r\nexport interface RemoteAppProps {\r\n user: User | null\r\n token: string | null\r\n currentPath: string\r\n onLogout: () => void\r\n onNavigate?: (path: string) => void\r\n userModules?: UserModule[]\r\n currentModule?: UserModule\r\n ssoData?: DecodedUser\r\n userData?: SSOUserData\r\n clientData?: MasterClient[]\r\n entityData?: MasterEntity[]\r\n customerData?: MasterCustomer[]\r\n /** Active role selected via the host's role switcher (auth.roleActive). */\r\n roleActive?: string\r\n menus?: MenuItem[]\r\n debugSource?: 'host' | 'standalone'\r\n isDark?: boolean\r\n palette?: string\r\n /** BCP 47 locale selected by the host (e.g. `en`, `id-ID`). */\r\n locale?: string\r\n /**\r\n * Optional. Host may send CONTRACT_VERSION so remotes can log/adapt.\r\n * Omitted by older hosts — remotes must treat missing as v1-compatible.\r\n */\r\n contractVersion?: number\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// STANDALONE HANDOFF PROTOCOL (postMessage)\r\n//\r\n// When a host sidebar group is opened standalone (window.open, not federated),\r\n// session/masterdata is handed to the remote via a postMessage handshake instead\r\n// of a URL param — no data ever sits in the URL, and there is no practical size\r\n// limit. See tms_host/src/mf/remoteHandoff.ts (sender) and\r\n// tms_master_data/src/standalone/handoff.ts (receiver) for the implementation,\r\n// and that remote's src/standalone/README.md for the full sequence diagram.\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n\r\n/** Query param carrying only a correlation nonce — never auth data. */\r\nexport const MFE_HANDOFF_QUERY_PARAM = 'mfe_handoff'\r\n/** remote -> host: \"I'm loaded and ready to receive the payload.\" */\r\nexport const MFE_HANDOFF_READY = 'mfe:handoff-ready'\r\n/** host -> remote: the actual session/masterdata payload. */\r\nexport const MFE_HANDOFF_PAYLOAD = 'mfe:handoff-payload'\r\n/** How long the host waits for a \"ready\" reply (and the remote waits for the payload) before giving up. */\r\nexport const MFE_HANDOFF_TIMEOUT_MS = 9000\r\n\r\nexport interface MfeHandoffPayload {\r\n user?: User | null\r\n token?: string | null\r\n /** JWT from the host's auth/token API (localStorage `tms_jwt_token`). Persisted by the\r\n * remote under its own `tms_jwt_token` key, never inside `tms_auth`. */\r\n jwtToken?: string | null\r\n ssoData?: DecodedUser\r\n userData?: SSOUserData\r\n clientData?: MasterClient[]\r\n entityData?: MasterEntity[]\r\n customerData?: MasterCustomer[]\r\n userModules?: UserModule[]\r\n currentModule?: UserModule\r\n /** Active role selected via the host's role switcher (auth.roleActive). */\r\n roleActive?: string\r\n menus?: MenuItem[]\r\n /** Same trio the host already passes when embedding (RemoteAppProps) — sent here\r\n * so a standalone tab opened from the sidebar starts from the host's state too.\r\n * Remotes are free to ignore them (theme stays local in standalone). */\r\n isDark?: boolean\r\n palette?: string\r\n /** BCP 47 locale selected by the host (e.g. `en`, `id-ID`). */\r\n locale?: string\r\n /** Host contract version at handoff time; omit to mean v1-compatible. */\r\n contractVersion?: number\r\n}\r\n\r\nexport interface MfeHandoffReadyMessage {\r\n type: typeof MFE_HANDOFF_READY\r\n nonce: string\r\n}\r\n\r\nexport interface MfeHandoffPayloadMessage {\r\n type: typeof MFE_HANDOFF_PAYLOAD\r\n nonce: string\r\n payload: MfeHandoffPayload\r\n}\r\n\r\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\r\n return typeof value === 'object' && value !== null && !Array.isArray(value)\r\n}\r\n\r\n// The real security boundary for the handoff is the event.source/event.origin\r\n// check each side already does before ever calling these — postMessage's origin\r\n// is set by the browser from the actual sender and cannot be spoofed by a page\r\n// script. These are defense-in-depth on top of that: never touch event.data's\r\n// shape (even from a same-origin sender) without validating it first, so a bug\r\n// upstream can't smuggle an unexpected structure into localStorage.\r\nexport function isMfeHandoffReadyMessage(data: unknown): data is MfeHandoffReadyMessage {\r\n return isPlainRecord(data) && data.type === MFE_HANDOFF_READY && typeof data.nonce === 'string'\r\n}\r\n\r\nexport function isMfeHandoffPayloadMessage(data: unknown): data is MfeHandoffPayloadMessage {\r\n return (\r\n isPlainRecord(data) &&\r\n data.type === MFE_HANDOFF_PAYLOAD &&\r\n typeof data.nonce === 'string' &&\r\n isPlainRecord(data.payload)\r\n )\r\n}\r\n\r\n// Drop __proto__/constructor/prototype own-keys before the payload is destructured\r\n// or persisted. JSON.parse/object-spread don't let these mutate a real prototype by\r\n// themselves, but this keeps it that way even if a future change deep-merges this\r\n// object somewhere downstream.\r\nexport function sanitizeHandoffPayload(payload: Record<string, unknown>): MfeHandoffPayload {\r\n const clean: Record<string, unknown> = {}\r\n for (const key of Object.keys(payload)) {\r\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue\r\n clean[key] = payload[key]\r\n }\r\n return clean as MfeHandoffPayload\r\n}\r\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulog/contract",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Host <-> remote prop contract (RemoteAppProps, CONTRACT_VERSION, handoff protocol). Dependency-free.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public",
|
|
10
|
+
"registry": "https://registry.npmjs.org/"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
}
|
|
31
|
+
}
|