@assembly-js/app-foundation 0.2.4 → 0.2.6
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/CHANGELOG.md +9 -1
- package/bin/client-safe-sdk-methods.mjs +30 -20
- package/bin/foundation-exports.mjs +1 -1
- package/bin/scan-client-calls.mjs +66 -8
- package/dist/origin-bootstrap.d.ts +4 -6
- package/dist/origin-bootstrap.js +21 -30
- package/dist/paint-beacon.js +1 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,7 +15,15 @@ from every bullet except `[guidance]`, agent work from the check scripts those `
|
|
|
15
15
|
bullets ship. Write `[fix]`, `[behavior]` and `[action]` bullets for the app's owner.
|
|
16
16
|
|
|
17
17
|
A patch release never changes what an app's build requires; a minor release may add or change a
|
|
18
|
-
`bin/check-*.mjs` script.
|
|
18
|
+
`bin/check-*.mjs` script. Sections are written by the release workflow from the files in `changes/`,
|
|
19
|
+
not by hand; see `changes/README.md`. CI fails a PR that changes a check script without a
|
|
20
|
+
`bump: minor` change file.
|
|
21
|
+
|
|
22
|
+
## 0.2.6
|
|
23
|
+
- [fix] The app keeps connecting to a portal on a custom domain, and keeps telling the editor when it has loaded, without relying on a platform setting that is being retired. Nothing visible changes.
|
|
24
|
+
|
|
25
|
+
## 0.2.5
|
|
26
|
+
- [new] The client-call scan no longer reports reading the session's token details, a client's allowed fetch routes, or a harmless local namespace import as breaking; nothing in the app changes.
|
|
19
27
|
|
|
20
28
|
## 0.2.4
|
|
21
29
|
- [new] Add `@assembly-js/app-foundation/codegen`, which turns another app's API description into a typed client the app can call by verb and path, and `appProxyOptions` in `/server`, which that client uses to reach the other app through the platform. Nothing in the app changes until a client is generated for it.
|
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
// The routes a client bearer may keep calling once clients stop acting as admin
|
|
2
|
-
// as SDK method names (the SDK is flat: `retrieveClient`, not `clients.retrieve`).
|
|
1
|
+
// The routes a client bearer may keep calling once clients stop acting as admin.
|
|
3
2
|
// Mirrors every route the platform marks with ClientAccessOpen, ClientAccessSelf or
|
|
4
3
|
// ClientAccessGuarded under backend/platform-api; scan-client-calls.test.mjs fails when
|
|
5
|
-
// the marker count drifts from this list
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// GET /v1/clients/{id} -> retrieveClient (the client's own record only)
|
|
9
|
-
// GET /v1/notifications -> listNotifications
|
|
10
|
-
// POST /v1/notifications/{id}/read -> markNotificationRead
|
|
11
|
-
// POST /v1/notifications/{id}/unread -> markNotificationUnread
|
|
12
|
-
// GET /v1/session/flags -> no SDK method yet, so nothing to list; add it
|
|
13
|
-
// here when the SDK gains one.
|
|
4
|
+
// the marker count drifts from this table. The SDK method list, the count of routes with
|
|
5
|
+
// no SDK method and the `fetch` route check are all derived from the one table, so they
|
|
6
|
+
// cannot disagree. The SDK is flat: `retrieveClient`, not `clients.retrieve`.
|
|
14
7
|
//
|
|
8
|
+
// A route with no SDK method yet (`sdkMethod: null`) is reachable only through `fetch`;
|
|
9
|
+
// give it a method here when the SDK gains one.
|
|
10
|
+
export const CLIENT_SAFE_ROUTES = Object.freeze([
|
|
11
|
+
{ method: 'GET', path: '/v1/clients/{id}', sdkMethod: 'retrieveClient' }, // the client's own record only
|
|
12
|
+
{ method: 'GET', path: '/v1/me', sdkMethod: 'retrieveMe' },
|
|
13
|
+
{ method: 'GET', path: '/v1/notifications', sdkMethod: 'listNotifications' },
|
|
14
|
+
{ method: 'POST', path: '/v1/notifications/{id}/read', sdkMethod: 'markNotificationRead' },
|
|
15
|
+
{ method: 'POST', path: '/v1/notifications/{id}/unread', sdkMethod: 'markNotificationUnread' },
|
|
16
|
+
{ method: 'GET', path: '/v1/session/flags', sdkMethod: null },
|
|
17
|
+
].map(Object.freeze));
|
|
18
|
+
|
|
15
19
|
// retrieveClient is allowlisted by name: whether the id is the viewer's own is a runtime
|
|
16
20
|
// fact a static scan cannot see. The notification routes are open to clients, and the
|
|
17
21
|
// platform pins a client caller to their own notifications. Creating, retrieving and
|
|
18
22
|
// deleting a notification stay off the list: those routes are not open to clients.
|
|
19
|
-
export const CLIENT_SAFE_SDK_METHODS = Object.freeze(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export const CLIENT_SAFE_SDK_METHODS = Object.freeze(
|
|
24
|
+
CLIENT_SAFE_ROUTES.flatMap((r) => (r.sdkMethod ? [r.sdkMethod] : [])),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Marked client-safe routes that no SDK method reaches yet.
|
|
28
|
+
export const CLIENT_SAFE_ROUTES_WITHOUT_SDK_METHOD = CLIENT_SAFE_ROUTES.filter((r) => !r.sdkMethod).length;
|
|
29
|
+
|
|
30
|
+
// `{id}` stands for exactly one non-empty path segment.
|
|
31
|
+
// The id segment excludes `.` and `%` so `.`, `..` and `%2e` forms that URL normalisation resolves elsewhere fail closed.
|
|
32
|
+
const routePattern = (route) =>
|
|
33
|
+
new RegExp(`^${route.path.split('/').map((seg) => (seg === '{id}' ? '[A-Za-z0-9_-]+' : seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))).join('/')}$`);
|
|
34
|
+
const ROUTE_MATCHERS = CLIENT_SAFE_ROUTES.map((route) => ({ method: route.method, pattern: routePattern(route) }));
|
|
26
35
|
|
|
27
|
-
//
|
|
28
|
-
export const
|
|
36
|
+
// Whether `fetch(path, { method })` hits a client-safe route; `path` has no query string.
|
|
37
|
+
export const isClientSafeRoute = (method, path) =>
|
|
38
|
+
ROUTE_MATCHERS.some((m) => m.method === method.toUpperCase() && m.pattern.test(path));
|
|
@@ -30,7 +30,7 @@ export const NON_CLIENT_EXPORTS = new Set([
|
|
|
30
30
|
'InternalUserAccess', 'LEGACY_APP_IDS', 'MintFailure', 'MintOutcome', 'MintSessionOptions', 'MintedAppBearer',
|
|
31
31
|
'MintedBearer', 'NETWORK_CAPTURE_MESSAGE_TYPE', 'NetworkCaptureHeaderFingerprint',
|
|
32
32
|
'NetworkCaptureRecord', 'PAINTED_BEACON_EVENT', 'PAINTED_BEACON_PATH',
|
|
33
|
-
'PORTAL_ORIGIN_COOKIE_NAME', '
|
|
33
|
+
'PORTAL_ORIGIN_COOKIE_NAME', 'PdfText', 'PrincipalAllow', 'PrincipalKind',
|
|
34
34
|
'Provider', 'ProxyOpts', 'RENEW_OUTCOME_EVENT', 'RenewOutcome', 'ReplaceChildRowsResult',
|
|
35
35
|
'RequestSource', 'ResolveResourceOpts', 'ResourceError', 'ResourceFieldMap',
|
|
36
36
|
'ResourceOption', 'ResourcePage', 'ScheduleCadence', 'SlackChannel',
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
24
24
|
import { createRequire } from 'node:module';
|
|
25
25
|
import path from 'node:path';
|
|
26
|
-
import { CLIENT_SAFE_SDK_METHODS } from './client-safe-sdk-methods.mjs';
|
|
26
|
+
import { CLIENT_SAFE_SDK_METHODS, isClientSafeRoute } from './client-safe-sdk-methods.mjs';
|
|
27
27
|
import { NON_CLIENT_EXPORTS } from './foundation-exports.mjs';
|
|
28
28
|
import {
|
|
29
29
|
EXCLUDING_WRAPPERS,
|
|
@@ -94,6 +94,11 @@ export const SDK_METHOD_SHAPE = /^(?:(?:list|retrieve|create|update|delete|send|
|
|
|
94
94
|
// `fetch` and `sendWebhook` are the flat client methods the codegen services do not declare, and
|
|
95
95
|
// the verb pattern cannot match; `fetch` is how apps reach non-SDK endpoints such as the AI proxy.
|
|
96
96
|
export const SDK_METHOD_EXTRAS = new Set(['fetch', 'sendWebhook']);
|
|
97
|
+
// SDK methods that only read the token already in hand, so a viewer calling them reaches no endpoint.
|
|
98
|
+
// Skipped at projection, not collection, so per-file facts stay reusable.
|
|
99
|
+
const NON_NETWORK_SDK_METHODS = new Set(['getTokenPayload']);
|
|
100
|
+
const FETCH_METHOD = 'fetch';
|
|
101
|
+
const DEFAULT_FETCH_HTTP_METHOD = 'GET';
|
|
97
102
|
const SDK_SERVICES_DIR = '../codegen/api/services';
|
|
98
103
|
const SDK_SERVICE_FILE = /Service\.d\.ts$/;
|
|
99
104
|
const SDK_STATIC_METHOD = /static (\w+)\(/g;
|
|
@@ -178,7 +183,9 @@ function scriptKind(ts, file) {
|
|
|
178
183
|
// `calls` are every method call on a tagged receiver, `defeaters` are the
|
|
179
184
|
// constructs that stop the scan following a viewer receiver, `specifiers` the
|
|
180
185
|
// module specifiers it imports.
|
|
181
|
-
|
|
186
|
+
// `localExportsFactory(spec)` says whether a local module exports an SDK factory; without it every
|
|
187
|
+
// local namespace import is tracked.
|
|
188
|
+
export function collectFileFacts(ts, file, text, sdkMethods = null, localExportsFactory = null) {
|
|
182
189
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKind(ts, file));
|
|
183
190
|
const calls = [];
|
|
184
191
|
const defeaters = [];
|
|
@@ -194,6 +201,26 @@ export function collectFileFacts(ts, file, text, sdkMethods = null) {
|
|
|
194
201
|
|
|
195
202
|
const isLiteralText = (n) => n && (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n));
|
|
196
203
|
|
|
204
|
+
// The literal route and HTTP method of a `fetch(path, init?)` call, or null when either is not
|
|
205
|
+
// literal, so the projection fails closed. A missing method option is a GET.
|
|
206
|
+
const fetchRoute = (call) => {
|
|
207
|
+
const [pathArg, init] = call.arguments;
|
|
208
|
+
if (!isLiteralText(pathArg)) return null;
|
|
209
|
+
let method = DEFAULT_FETCH_HTTP_METHOD;
|
|
210
|
+
if (init) {
|
|
211
|
+
if (!ts.isObjectLiteralExpression(init)) return null;
|
|
212
|
+
for (const prop of init.properties) {
|
|
213
|
+
if (ts.isSpreadAssignment(prop) || (prop.name && ts.isComputedPropertyName(prop.name))) return null;
|
|
214
|
+
if (prop.name?.text !== 'method') continue;
|
|
215
|
+
if (!ts.isPropertyAssignment(prop) || !isLiteralText(prop.initializer)) return null;
|
|
216
|
+
method = prop.initializer.text;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// The SDK prepends the slash a path is missing, so `v1/me` requests `/v1/me`.
|
|
220
|
+
const routePath = pathArg.text.startsWith('/') ? pathArg.text : `/${pathArg.text}`;
|
|
221
|
+
return { path: routePath.replace(/[?#].*$/, ''), method };
|
|
222
|
+
};
|
|
223
|
+
|
|
197
224
|
// --- imports ------------------------------------------------------------
|
|
198
225
|
const acceptsFactories = (spec) => LOCAL_SPECIFIER.test(spec) || SDK_SPECIFIER.test(spec);
|
|
199
226
|
for (const stmt of sf.statements) {
|
|
@@ -214,7 +241,12 @@ export function collectFileFacts(ts, file, text, sdkMethods = null) {
|
|
|
214
241
|
}
|
|
215
242
|
}
|
|
216
243
|
if (!bindings || !acceptsFactories(spec)) continue;
|
|
217
|
-
if (ts.isNamespaceImport(bindings))
|
|
244
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
245
|
+
// A namespace of a module with no factory (a drizzle schema) cannot hand out a client.
|
|
246
|
+
if (!localExportsFactory || !LOCAL_SPECIFIER.test(spec) || localExportsFactory(spec)) {
|
|
247
|
+
namespaceLocals.add(bindings.name.text);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
218
250
|
else {
|
|
219
251
|
for (const el of bindings.elements) {
|
|
220
252
|
const imported = (el.propertyName ?? el.name).text;
|
|
@@ -337,7 +369,12 @@ export function collectFileFacts(ts, file, text, sdkMethods = null) {
|
|
|
337
369
|
const parent = cur.parent;
|
|
338
370
|
if (!parent) return;
|
|
339
371
|
|
|
340
|
-
const recordCall = (method, at) => calls.push({
|
|
372
|
+
const recordCall = (method, at, call) => calls.push({
|
|
373
|
+
line: lineOf(sf, at),
|
|
374
|
+
method,
|
|
375
|
+
provenance,
|
|
376
|
+
...(method === FETCH_METHOD ? { route: fetchRoute(call) } : {}),
|
|
377
|
+
});
|
|
341
378
|
|
|
342
379
|
if (ts.isPropertyAccessExpression(parent) && parent.expression === cur) {
|
|
343
380
|
const call = parent.parent;
|
|
@@ -352,14 +389,14 @@ export function collectFileFacts(ts, file, text, sdkMethods = null) {
|
|
|
352
389
|
if (viewer) defeat(parent, DEFEATER.PROMISE_CHAIN);
|
|
353
390
|
return;
|
|
354
391
|
}
|
|
355
|
-
recordCall(name, parent.name);
|
|
392
|
+
recordCall(name, parent.name, call);
|
|
356
393
|
return;
|
|
357
394
|
}
|
|
358
395
|
if (ts.isElementAccessExpression(parent) && parent.expression === cur) {
|
|
359
396
|
const arg = parent.argumentExpression;
|
|
360
397
|
const call = parent.parent;
|
|
361
398
|
if (isLiteralText(arg) && call && ts.isCallExpression(call) && call.expression === parent) {
|
|
362
|
-
recordCall(arg.text, arg);
|
|
399
|
+
recordCall(arg.text, arg, call);
|
|
363
400
|
} else if (viewer) defeat(parent, DEFEATER.DYNAMIC_ACCESS);
|
|
364
401
|
return;
|
|
365
402
|
}
|
|
@@ -880,9 +917,28 @@ export function createAnalysis(root) {
|
|
|
880
917
|
return null;
|
|
881
918
|
};
|
|
882
919
|
|
|
920
|
+
// Over-approximates (a mention plus any `export`): tracking too much only costs a defeater.
|
|
921
|
+
const FACTORY_NAME = new RegExp(`\\b(?:${[...Object.keys(SDK_FACTORIES), AFTER_ASSEMBLY, REQUEST_CONTEXT].join('|')})\\b`);
|
|
922
|
+
const STAR_REEXPORT = /export\s*(?:\*(?:\s*as\s+\w+)?|\{[^}]*\})\s*from\s*['"]([^'"]+)['"]/g;
|
|
923
|
+
const exportsFactory = (file, seen = new Set()) => {
|
|
924
|
+
if (seen.has(file)) return false;
|
|
925
|
+
seen.add(file);
|
|
926
|
+
const text = read(file);
|
|
927
|
+
if (/\bexport\b/.test(text) && FACTORY_NAME.test(text)) return true;
|
|
928
|
+
return [...text.matchAll(STAR_REEXPORT)].some(([, spec]) => {
|
|
929
|
+
if (SDK_SPECIFIER.test(spec)) return true;
|
|
930
|
+
const target = LOCAL_SPECIFIER.test(spec) ? resolve(file, spec) : null;
|
|
931
|
+
return target ? exportsFactory(target, seen) : false;
|
|
932
|
+
});
|
|
933
|
+
};
|
|
934
|
+
|
|
883
935
|
const factsFor = (file) => {
|
|
884
936
|
if (!facts.has(file)) {
|
|
885
|
-
const
|
|
937
|
+
const localExportsFactory = (spec) => {
|
|
938
|
+
const target = resolve(file, spec);
|
|
939
|
+
return target ? exportsFactory(target) : true;
|
|
940
|
+
};
|
|
941
|
+
const raw = collectFileFacts(ts, file, read(file), sdkMethods, localExportsFactory);
|
|
886
942
|
const imports = [];
|
|
887
943
|
const defeaters = [...raw.defeaters];
|
|
888
944
|
for (const spec of raw.specifiers) {
|
|
@@ -938,7 +994,9 @@ export function projectClientReachable(analysis, allowlist = CLIENT_SAFE_SDK_MET
|
|
|
938
994
|
const gated = [...reaching].every((e) => gatedSet.has(e));
|
|
939
995
|
const { calls, defeaters } = analysis.facts(file);
|
|
940
996
|
for (const c of calls) {
|
|
941
|
-
if (c.provenance
|
|
997
|
+
if (c.provenance !== PROVENANCE_VIEWER || NON_NETWORK_SDK_METHODS.has(c.method)) continue;
|
|
998
|
+
if (c.method === FETCH_METHOD && c.route && isClientSafeRoute(c.route.method, c.route.path)) continue;
|
|
999
|
+
if (!allowed.has(c.method)) {
|
|
942
1000
|
findings.push({ file, line: c.line, method: c.method, provenance: c.provenance, entry, gated });
|
|
943
1001
|
}
|
|
944
1002
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export declare const PORTAL_ORIGIN_COOKIE_NAME = "assembly-portal-origin";
|
|
2
2
|
export declare const CLIENT_FLAGS_COOKIE_NAME = "assembly-flags";
|
|
3
|
-
export declare const PREVIEW_V2_FLAG = "app-builder-preview-v2";
|
|
4
3
|
/**
|
|
5
4
|
* Reads a single cookie by name from a cookie string (defaults to
|
|
6
5
|
* `document.cookie`). Shared by every module that reads a proxy-mirrored
|
|
@@ -9,14 +8,13 @@ export declare const PREVIEW_V2_FLAG = "app-builder-preview-v2";
|
|
|
9
8
|
export declare function readCookie(name: string, cookieString?: string): string | null;
|
|
10
9
|
/**
|
|
11
10
|
* Whether the given flag key is present in the proxy-mirrored flags cookie.
|
|
12
|
-
* Exported
|
|
13
|
-
* cookie without re-parsing it themselves.
|
|
11
|
+
* Exported so client modules can gate on a flag without re-parsing the cookie.
|
|
14
12
|
*/
|
|
15
13
|
export declare function hasClientFlag(key: string, cookieString?: string): boolean;
|
|
16
14
|
/**
|
|
17
15
|
* Register the workspace's portal origin with the bridge allowlist — from the
|
|
18
|
-
* proxy-set cookie when
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* proxy-set cookie when it is readable, from the framing origin otherwise.
|
|
17
|
+
* Returns the origin it registered, or null when there was nothing to do.
|
|
18
|
+
* Exported for tests; runs once at module load in the browser.
|
|
21
19
|
*/
|
|
22
20
|
export declare function bootstrapPortalOrigin(cookieString?: string): string | null;
|
package/dist/origin-bootstrap.js
CHANGED
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
// evaluation precedes hydration — so the workspace's portal origin joins the
|
|
3
3
|
// bridge allowlist before any component's first header/session-token call.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
// never selects whether anything registers at all (flag-off must keep custom
|
|
7
|
-
// portal domains working exactly as pre-vNext package versions did):
|
|
5
|
+
// Two trust sources, in order:
|
|
8
6
|
//
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// -
|
|
14
|
-
// framing origin (ancestorOrigins, referrer
|
|
15
|
-
// sound because the proxy's CSP frame-ancestors
|
|
16
|
-
// frame the app, but it rides navigation state —
|
|
17
|
-
// modes and poisoned by in-iframe reloads — which is
|
|
18
|
-
//
|
|
7
|
+
// - the proxy-set cookie. The value is platform-resolved, never inferred
|
|
8
|
+
// client-side — the proxy already fetches workspace.portalUrl for its CSP
|
|
9
|
+
// frame-ancestors header and mirrors it into this client-readable cookie.
|
|
10
|
+
// Immune to the navigation-state poisoning below.
|
|
11
|
+
// - when that cookie is unreadable (a browser blocking the app's cookies):
|
|
12
|
+
// the browser-reported framing origin (ancestorOrigins, referrer
|
|
13
|
+
// fallback). Trusting it is sound because the proxy's CSP frame-ancestors
|
|
14
|
+
// already gates who may frame the app, but it rides navigation state —
|
|
15
|
+
// strip-able by privacy modes and poisoned by in-iframe reloads — which is
|
|
16
|
+
// why the cookie comes first.
|
|
19
17
|
//
|
|
20
18
|
// The bridge has no trust bypass, so one of these registrations is the only
|
|
21
19
|
// way a custom portal domain becomes a valid parent (dashboard/platform
|
|
@@ -29,7 +27,6 @@ export const PORTAL_ORIGIN_COOKIE_NAME = 'assembly-portal-origin';
|
|
|
29
27
|
// plain, comma-separated cookie on every request (set or cleared, never
|
|
30
28
|
// left stale).
|
|
31
29
|
export const CLIENT_FLAGS_COOKIE_NAME = 'assembly-flags';
|
|
32
|
-
export const PREVIEW_V2_FLAG = 'app-builder-preview-v2';
|
|
33
30
|
/**
|
|
34
31
|
* Reads a single cookie by name from a cookie string (defaults to
|
|
35
32
|
* `document.cookie`). Shared by every module that reads a proxy-mirrored
|
|
@@ -51,8 +48,7 @@ export function readCookie(name, cookieString) {
|
|
|
51
48
|
}
|
|
52
49
|
/**
|
|
53
50
|
* Whether the given flag key is present in the proxy-mirrored flags cookie.
|
|
54
|
-
* Exported
|
|
55
|
-
* cookie without re-parsing it themselves.
|
|
51
|
+
* Exported so client modules can gate on a flag without re-parsing the cookie.
|
|
56
52
|
*/
|
|
57
53
|
export function hasClientFlag(key, cookieString) {
|
|
58
54
|
const raw = readCookie(CLIENT_FLAGS_COOKIE_NAME, cookieString);
|
|
@@ -61,11 +57,11 @@ export function hasClientFlag(key, cookieString) {
|
|
|
61
57
|
return raw.split(',').includes(key);
|
|
62
58
|
}
|
|
63
59
|
/**
|
|
64
|
-
*
|
|
60
|
+
* Framing-origin resolution for when the portal-origin cookie is unreadable: `location.ancestorOrigins`
|
|
65
61
|
* (browser-attested; Chromium/WebKit), falling back to `document.referrer`
|
|
66
62
|
* (Firefox). Null outside an iframe or when neither source is available.
|
|
67
63
|
*/
|
|
68
|
-
function
|
|
64
|
+
function resolveFramingOrigin() {
|
|
69
65
|
if (typeof window === 'undefined')
|
|
70
66
|
return null;
|
|
71
67
|
if (window.parent === window)
|
|
@@ -85,25 +81,20 @@ function resolveLegacyParentOrigin() {
|
|
|
85
81
|
}
|
|
86
82
|
/**
|
|
87
83
|
* Register the workspace's portal origin with the bridge allowlist — from the
|
|
88
|
-
* proxy-set cookie when
|
|
89
|
-
*
|
|
90
|
-
*
|
|
84
|
+
* proxy-set cookie when it is readable, from the framing origin otherwise.
|
|
85
|
+
* Returns the origin it registered, or null when there was nothing to do.
|
|
86
|
+
* Exported for tests; runs once at module load in the browser.
|
|
91
87
|
*/
|
|
92
88
|
export function bootstrapPortalOrigin(cookieString) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
// already on the default allowlist.
|
|
98
|
-
const origin = resolveLegacyParentOrigin();
|
|
89
|
+
const record = readCookie(PORTAL_ORIGIN_COOKIE_NAME, cookieString);
|
|
90
|
+
if (!record) {
|
|
91
|
+
// Https-only: local dev origins are already on the default allowlist.
|
|
92
|
+
const origin = resolveFramingOrigin();
|
|
99
93
|
if (!origin || !origin.startsWith('https://'))
|
|
100
94
|
return null;
|
|
101
95
|
AssemblyBridge.configure({ additionalOrigins: [origin] });
|
|
102
96
|
return origin;
|
|
103
97
|
}
|
|
104
|
-
const record = readCookie(PORTAL_ORIGIN_COOKIE_NAME, cookieString);
|
|
105
|
-
if (!record)
|
|
106
|
-
return null;
|
|
107
98
|
// Accept only a bare https origin — anything else (paths, http, garbage)
|
|
108
99
|
// is not a value the proxy writes and must not reach the allowlist.
|
|
109
100
|
try {
|
package/dist/paint-beacon.js
CHANGED
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
// The HTTP route is handled in proxy.ts via the helpers below (same
|
|
7
7
|
// package-delegation pattern as session-exchange). The bridge post rides
|
|
8
8
|
// buffer-until-learned, so a paint that beats the parent's hello is not lost.
|
|
9
|
-
//
|
|
10
|
-
// Gated on the preview-v2 flag (see origin-bootstrap.ts's hasClientFlag):
|
|
11
|
-
// installPaintBeacon() no-ops for a flag-off workspace, so the client never
|
|
12
|
-
// calls sendBeacon and the proxy's route never sees a request — no need to
|
|
13
|
-
// separately gate handlePaintedBeacon() below.
|
|
14
9
|
import { AssemblyBridge } from '@assembly-js/app-bridge';
|
|
15
|
-
import { hasClientFlag, PREVIEW_V2_FLAG } from './origin-bootstrap.js';
|
|
16
10
|
export const PAINTED_BEACON_PATH = '/__assembly/painted';
|
|
17
11
|
export const PAINTED_BEACON_EVENT = 'assembly.painted';
|
|
18
12
|
let scheduled = false;
|
|
@@ -46,9 +40,7 @@ function firePaintBeacon() {
|
|
|
46
40
|
* hydration + rAF wins. Does nothing on the server.
|
|
47
41
|
*/
|
|
48
42
|
export function installPaintBeacon() {
|
|
49
|
-
if (typeof window === 'undefined' || scheduled)
|
|
50
|
-
return;
|
|
51
|
-
if (!hasClientFlag(PREVIEW_V2_FLAG))
|
|
43
|
+
if (typeof window === 'undefined' || typeof document === 'undefined' || scheduled)
|
|
52
44
|
return;
|
|
53
45
|
scheduled = true;
|
|
54
46
|
const afterPaint = () => {
|