@microsoft/rayfin-app-state-fabric 1.35.0-alpha.1412
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/LICENSE +21 -0
- package/README.md +140 -0
- package/assets/docs/index.md +211 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +54 -0
- package/dist/fabricAppState.d.ts +32 -0
- package/dist/fabricAppState.js +311 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -0
- package/dist/launchState.d.ts +56 -0
- package/dist/launchState.js +116 -0
- package/dist/protocol.d.ts +22 -0
- package/dist/protocol.js +22 -0
- package/dist/types.d.ts +183 -0
- package/dist/types.js +5 -0
- package/dist/validation.d.ts +23 -0
- package/dist/validation.js +117 -0
- package/package.json +50 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the deep-link application state client.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A single JSON-compatible value permitted inside {@link FabricAppState}.
|
|
6
|
+
*
|
|
7
|
+
* `undefined`, functions, symbols, `NaN`, and `Infinity` are excluded
|
|
8
|
+
* because they cannot survive a JSON round trip through the URL.
|
|
9
|
+
*/
|
|
10
|
+
export type FabricAppStateValue = string | number | boolean | null | FabricAppStateValue[] | {
|
|
11
|
+
[key: string]: FabricAppStateValue;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Application state persisted into the shareable Fabric portal URL.
|
|
15
|
+
*
|
|
16
|
+
* **Never place secrets, access tokens, or personal data here.** The
|
|
17
|
+
* value is visible in browser history, screenshots, copied links,
|
|
18
|
+
* corporate proxy logs, and anywhere the link is pasted. Use an opaque
|
|
19
|
+
* identifier that maps to server-side data when the state is sensitive
|
|
20
|
+
* or large.
|
|
21
|
+
*/
|
|
22
|
+
export interface FabricAppState {
|
|
23
|
+
[key: string]: FabricAppStateValue;
|
|
24
|
+
}
|
|
25
|
+
/** Options accepted by `createFabricAppStateClient`. */
|
|
26
|
+
export interface FabricAppStateClientOptions {
|
|
27
|
+
/** Host window. Defaults to `window.parent`. */
|
|
28
|
+
target?: Window;
|
|
29
|
+
/**
|
|
30
|
+
* Expected origin of the Fabric host.
|
|
31
|
+
*
|
|
32
|
+
* Strongly recommended. When omitted, messages are posted with `"*"`
|
|
33
|
+
* and inbound events are not origin-checked.
|
|
34
|
+
*/
|
|
35
|
+
targetOrigin?: string;
|
|
36
|
+
/** Per-request timeout in milliseconds. Defaults to the bridge default. */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
/** Override for the default encoded-size ceiling. */
|
|
39
|
+
maxEncodedBytes?: number;
|
|
40
|
+
/** Override for the default nesting-depth ceiling. */
|
|
41
|
+
maxDepth?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Query string to read seeded launch state from.
|
|
44
|
+
*
|
|
45
|
+
* Defaults to the live `window.location.search`. Injectable for
|
|
46
|
+
* tests and for hosts that supply the value by another route.
|
|
47
|
+
*/
|
|
48
|
+
launchSearch?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Whether to remove the seeded parameter from the app's own URL once
|
|
51
|
+
* it has been read. Defaults to `true`.
|
|
52
|
+
*
|
|
53
|
+
* Leaving it in place would resend the state to the app's own server
|
|
54
|
+
* on every reload and expose it in referrers to subresources.
|
|
55
|
+
*/
|
|
56
|
+
scrubLaunchParam?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Limits and features reported by the Fabric host.
|
|
60
|
+
*
|
|
61
|
+
* The host is authoritative. The client's own limits exist only to fail
|
|
62
|
+
* fast before a round trip, so treat these values as the real contract.
|
|
63
|
+
*/
|
|
64
|
+
export interface FabricAppStateCapabilities {
|
|
65
|
+
/** App-state protocol version implemented by the host. */
|
|
66
|
+
version: number;
|
|
67
|
+
/** Largest encoded state the host will accept, in bytes. */
|
|
68
|
+
maxEncodedBytes: number;
|
|
69
|
+
/** Deepest nesting the host will accept. */
|
|
70
|
+
maxDepth: number;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the host can create browser history entries.
|
|
73
|
+
*
|
|
74
|
+
* When `false`, {@link FabricAppStateClient.setState} still updates the
|
|
75
|
+
* URL — so links stay shareable — but no history entry is created and
|
|
76
|
+
* in-app Back and Forward will not work. Hide affordances that depend
|
|
77
|
+
* on history when this is `false`.
|
|
78
|
+
*/
|
|
79
|
+
canPush: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Listener invoked when the host reports externally-changed state.
|
|
83
|
+
*
|
|
84
|
+
* Receives `undefined` when navigation reaches a URL that carries no
|
|
85
|
+
* state, which means the app should restore its own defaults. Treat the
|
|
86
|
+
* value as untrusted: anyone can edit a link before sharing it.
|
|
87
|
+
*/
|
|
88
|
+
export type FabricAppStateListener = (state: FabricAppState | undefined) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Typed client for reading and writing deep-link state.
|
|
91
|
+
*/
|
|
92
|
+
export interface FabricAppStateClient {
|
|
93
|
+
/**
|
|
94
|
+
* Read the state the app was launched with.
|
|
95
|
+
*
|
|
96
|
+
* Returns `undefined` when the URL carries no state, which is the
|
|
97
|
+
* normal case for a fresh navigation.
|
|
98
|
+
*
|
|
99
|
+
* Resolves **synchronously** from the seeded iframe URL when the host
|
|
100
|
+
* supports it, so awaiting this does not delay first render. Falls
|
|
101
|
+
* back to a bridge round trip only on hosts that do not seed the URL.
|
|
102
|
+
*
|
|
103
|
+
* Use {@link FabricAppStateClient.getLaunchStateSync} when the state
|
|
104
|
+
* is needed in a code path that cannot be asynchronous at all.
|
|
105
|
+
*/
|
|
106
|
+
getLaunchState(): Promise<FabricAppState | undefined>;
|
|
107
|
+
/**
|
|
108
|
+
* Read seeded launch state without awaiting.
|
|
109
|
+
*
|
|
110
|
+
* Returns `undefined` when the host did not seed the URL, in which
|
|
111
|
+
* case {@link FabricAppStateClient.getLaunchState} must be awaited.
|
|
112
|
+
* Intended for the first render path, where launch state must be
|
|
113
|
+
* available before paint.
|
|
114
|
+
*/
|
|
115
|
+
getLaunchStateSync(): FabricAppState | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the host's capabilities, or `undefined` when the host does
|
|
118
|
+
* not support deep-link state.
|
|
119
|
+
*
|
|
120
|
+
* Deep linking rolls out per tenant, so check this before showing a
|
|
121
|
+
* share button rather than letting the user click one that cannot
|
|
122
|
+
* work. The result is cached.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const capabilities = await appState.isSupported();
|
|
127
|
+
* if (capabilities?.canPush) enableBackForwardHints();
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
isSupported(): Promise<FabricAppStateCapabilities | undefined>;
|
|
131
|
+
/**
|
|
132
|
+
* Commit state as a new browser history entry.
|
|
133
|
+
*
|
|
134
|
+
* Choose between this and {@link FabricAppStateClient.replaceState} by
|
|
135
|
+
* **who caused the change**:
|
|
136
|
+
*
|
|
137
|
+
* - The *user* caused it — a click, a filter change, opening a record.
|
|
138
|
+
* Use `setState`, so Back returns them to where they were.
|
|
139
|
+
* - The *app* caused it — restoring, reconciling, or normalising state
|
|
140
|
+
* the user did not ask for. Use `replaceState`, so Back is not
|
|
141
|
+
* cluttered with entries the user never navigated to.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* // The user picked a region: Back should undo it.
|
|
146
|
+
* await appState.setState({ view: 'sales', region: 'AT' });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
setState(state: FabricAppState): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Commit state by replacing the current history entry.
|
|
152
|
+
*
|
|
153
|
+
* Use for app-initiated changes and for high-frequency updates such as
|
|
154
|
+
* a slider drag, where one history entry per update would make Back
|
|
155
|
+
* unusable. See {@link FabricAppStateClient.setState} for the rule.
|
|
156
|
+
*
|
|
157
|
+
* **Platform caveat:** the Fabric host downgrades a replace to a push
|
|
158
|
+
* when the previous history entry belongs to a different extension, to
|
|
159
|
+
* stop one extension overwriting another's history. The first
|
|
160
|
+
* `replaceState()` after the user arrives from elsewhere in Fabric may
|
|
161
|
+
* therefore create an entry. This is platform behaviour and cannot be
|
|
162
|
+
* overridden.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* // Continuous updates while dragging: do not grow history.
|
|
167
|
+
* await appState.replaceState({ view: 'sales', threshold: value });
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
replaceState(state: FabricAppState): Promise<void>;
|
|
171
|
+
/**
|
|
172
|
+
* Observe state changes the app did not initiate: browser Back or
|
|
173
|
+
* Forward, or a deep link opened in the current tab.
|
|
174
|
+
*
|
|
175
|
+
* Echoes of the app's own writes are suppressed by the host.
|
|
176
|
+
*
|
|
177
|
+
* @returns An unsubscribe function.
|
|
178
|
+
*/
|
|
179
|
+
onStateChange(listener: FabricAppStateListener): () => void;
|
|
180
|
+
/** Remove all listeners and release the underlying bridge subscription. */
|
|
181
|
+
dispose(): void;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State validation: what may be stored, how large, and how deeply
|
|
3
|
+
* nested.
|
|
4
|
+
*/
|
|
5
|
+
import type { FabricAppState } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Default ceiling on the *encoded* state, in bytes.
|
|
8
|
+
*
|
|
9
|
+
* Shared links must survive corporate proxies, mail gateways, Teams, and
|
|
10
|
+
* SharePoint, which commonly truncate beyond roughly 8 000 characters.
|
|
11
|
+
* The canonical item path plus existing portal parameters already
|
|
12
|
+
* consume several hundred, so 4 KiB of encoded state leaves headroom.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_MAX_ENCODED_BYTES = 4096;
|
|
15
|
+
/** Default ceiling on nesting depth; the root object counts as depth 1. */
|
|
16
|
+
export declare const DEFAULT_MAX_DEPTH = 20;
|
|
17
|
+
/**
|
|
18
|
+
* Validate a state object and return its serialised form.
|
|
19
|
+
*
|
|
20
|
+
* @internal Exported for unit tests.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateAppState(state: FabricAppState, maxEncodedBytes: number, maxDepth: number): string;
|
|
23
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State validation: what may be stored, how large, and how deeply
|
|
3
|
+
* nested.
|
|
4
|
+
*/
|
|
5
|
+
import { FabricAppStateError } from './errors.js';
|
|
6
|
+
import { LAUNCH_STATE_PREFIX } from './launchState.js';
|
|
7
|
+
/**
|
|
8
|
+
* Default ceiling on the *encoded* state, in bytes.
|
|
9
|
+
*
|
|
10
|
+
* Shared links must survive corporate proxies, mail gateways, Teams, and
|
|
11
|
+
* SharePoint, which commonly truncate beyond roughly 8 000 characters.
|
|
12
|
+
* The canonical item path plus existing portal parameters already
|
|
13
|
+
* consume several hundred, so 4 KiB of encoded state leaves headroom.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_MAX_ENCODED_BYTES = 4096;
|
|
16
|
+
/** Default ceiling on nesting depth; the root object counts as depth 1. */
|
|
17
|
+
export const DEFAULT_MAX_DEPTH = 20;
|
|
18
|
+
/**
|
|
19
|
+
* base64url expands binary by 4/3, so the raw JSON budget is three
|
|
20
|
+
* quarters of the encoded budget. Validating against the raw size lets
|
|
21
|
+
* the SDK fail fast without duplicating the host's encoder.
|
|
22
|
+
*/
|
|
23
|
+
const BASE64_EXPANSION_NUMERATOR = 3;
|
|
24
|
+
const BASE64_EXPANSION_DENOMINATOR = 4;
|
|
25
|
+
/**
|
|
26
|
+
* Reject values that cannot survive a JSON round trip, and enforce the
|
|
27
|
+
* depth limit.
|
|
28
|
+
*
|
|
29
|
+
* `JSON.stringify` is not sufficient on its own: it *silently drops*
|
|
30
|
+
* `undefined` and function-valued properties rather than failing, which
|
|
31
|
+
* would let an app believe it saved state that never reached the URL.
|
|
32
|
+
* The explicit walk also produces an actionable error path.
|
|
33
|
+
*
|
|
34
|
+
* @param seen - Ancestors on the current branch, used for cycle
|
|
35
|
+
* detection. A `Set` of the current path is correct here; a global
|
|
36
|
+
* `WeakSet` would wrongly reject the same object appearing twice as
|
|
37
|
+
* siblings, which serialises fine.
|
|
38
|
+
*/
|
|
39
|
+
function assertSerializable(value, path, depth, maxDepth, seen) {
|
|
40
|
+
if (value === null)
|
|
41
|
+
return;
|
|
42
|
+
const type = typeof value;
|
|
43
|
+
if (type === 'string' || type === 'boolean')
|
|
44
|
+
return;
|
|
45
|
+
if (type === 'number') {
|
|
46
|
+
if (!Number.isFinite(value)) {
|
|
47
|
+
throw new FabricAppStateError(`State value at "${path}" is NaN or Infinity, which cannot be represented in JSON.`, 'INVALID_STATE');
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (type === 'undefined') {
|
|
52
|
+
throw new FabricAppStateError(`State value at "${path}" is undefined. Omit the property or use null.`, 'INVALID_STATE');
|
|
53
|
+
}
|
|
54
|
+
if (type === 'function' || type === 'symbol' || type === 'bigint') {
|
|
55
|
+
throw new FabricAppStateError(`State value at "${path}" is of unsupported type "${type}".`, 'INVALID_STATE');
|
|
56
|
+
}
|
|
57
|
+
// Objects and arrays from here on.
|
|
58
|
+
if (depth > maxDepth) {
|
|
59
|
+
throw new FabricAppStateError(`State nesting exceeds the maximum depth of ${maxDepth} at "${path}".`, 'STATE_TOO_DEEP');
|
|
60
|
+
}
|
|
61
|
+
const obj = value;
|
|
62
|
+
if (seen.has(obj)) {
|
|
63
|
+
throw new FabricAppStateError(`State contains a circular reference at "${path}".`, 'INVALID_STATE');
|
|
64
|
+
}
|
|
65
|
+
// Dates, Maps, Sets, and class instances all survive structuredClone
|
|
66
|
+
// but lose their identity through JSON, so reject them explicitly
|
|
67
|
+
// rather than silently degrading them to {} or an ISO string.
|
|
68
|
+
if (!Array.isArray(obj) && Object.getPrototypeOf(obj) !== Object.prototype) {
|
|
69
|
+
throw new FabricAppStateError(`State value at "${path}" must be a plain object or array.`, 'INVALID_STATE');
|
|
70
|
+
}
|
|
71
|
+
seen.add(obj);
|
|
72
|
+
if (Array.isArray(obj)) {
|
|
73
|
+
// Indexed rather than `forEach`, which skips holes. `JSON.stringify`
|
|
74
|
+
// turns a hole into `null`, so skipping one would silently change the
|
|
75
|
+
// state the app believes it saved.
|
|
76
|
+
for (let index = 0; index < obj.length; index++) {
|
|
77
|
+
if (!(index in obj)) {
|
|
78
|
+
throw new FabricAppStateError(`State value at "${path}[${index}]" is an empty slot in a sparse array, which JSON turns into null. Use null explicitly.`, 'INVALID_STATE');
|
|
79
|
+
}
|
|
80
|
+
assertSerializable(obj[index], `${path}[${index}]`, depth + 1, maxDepth, seen);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const [key, item] of Object.entries(obj)) {
|
|
85
|
+
assertSerializable(item, `${path}.${key}`, depth + 1, maxDepth, seen);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
seen.delete(obj);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validate a state object and return its serialised form.
|
|
92
|
+
*
|
|
93
|
+
* @internal Exported for unit tests.
|
|
94
|
+
*/
|
|
95
|
+
export function validateAppState(state, maxEncodedBytes, maxDepth) {
|
|
96
|
+
if (typeof state !== 'object' ||
|
|
97
|
+
state === null ||
|
|
98
|
+
Array.isArray(state) ||
|
|
99
|
+
Object.getPrototypeOf(state) !== Object.prototype) {
|
|
100
|
+
throw new FabricAppStateError('State must be a plain object.', 'INVALID_STATE');
|
|
101
|
+
}
|
|
102
|
+
assertSerializable(state, 'state', 1, maxDepth, new Set());
|
|
103
|
+
const json = JSON.stringify(state);
|
|
104
|
+
// The host measures the prefix as part of its budget, so leaving it out
|
|
105
|
+
// here would pass states the host then rejects on the round trip.
|
|
106
|
+
const rawBudget = Math.floor(((maxEncodedBytes - LAUNCH_STATE_PREFIX.length) *
|
|
107
|
+
BASE64_EXPANSION_NUMERATOR) /
|
|
108
|
+
BASE64_EXPANSION_DENOMINATOR);
|
|
109
|
+
const byteLength = new TextEncoder().encode(json).length;
|
|
110
|
+
if (byteLength > rawBudget) {
|
|
111
|
+
throw new FabricAppStateError(`State is ${byteLength} bytes, which exceeds the ${rawBudget}-byte limit ` +
|
|
112
|
+
`(${maxEncodedBytes} bytes once encoded). Store large state server-side ` +
|
|
113
|
+
`and put an identifier in the URL instead.`, 'STATE_TOO_LARGE');
|
|
114
|
+
}
|
|
115
|
+
return json;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=validation.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@microsoft/rayfin-app-state-fabric",
|
|
3
|
+
"version": "1.35.0-alpha.1412",
|
|
4
|
+
"description": "Deep-link application state for Rayfin apps embedded in the Fabric portal",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*.js",
|
|
9
|
+
"dist/**/*.d.ts",
|
|
10
|
+
"assets/docs/**/*.md",
|
|
11
|
+
"!dist/**/__tests__/**",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@microsoft/fabric-embedded-host": "1.35.0-alpha.1412",
|
|
17
|
+
"@microsoft/rayfin-lib": "1.35.0-alpha.1412"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.8.3",
|
|
21
|
+
"vitest": "^3.2.3",
|
|
22
|
+
"@vitest/coverage-v8": "~3.2.4",
|
|
23
|
+
"rimraf": "~6.0.1"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"registry": "https://npm.pkg.github.com",
|
|
27
|
+
"access": "restricted"
|
|
28
|
+
},
|
|
29
|
+
"rayfinDocs": {
|
|
30
|
+
"version": 1,
|
|
31
|
+
"dir": "assets/docs",
|
|
32
|
+
"module": "rayfin-app-state-fabric",
|
|
33
|
+
"kind": "api-reference"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/microsoft/rayfin.git",
|
|
38
|
+
"directory": "packages/typescript-sdk/app-state-fabric"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [],
|
|
41
|
+
"author": "",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc && node ../scripts/fix-esm-extensions.mjs ./dist",
|
|
45
|
+
"build:watch": "tsc --watch",
|
|
46
|
+
"clean": "rimraf dist && rimraf .tsbuildinfo",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest"
|
|
49
|
+
}
|
|
50
|
+
}
|