@reactvision/react-viro 2.56.0 → 2.57.2
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 +8 -0
- package/android/react_viro/react_viro-release.aar +0 -0
- package/android/viro_renderer/viro_renderer-release.aar +0 -0
- package/components/AR/ViroARPlane.tsx +1 -4
- package/components/AR/ViroARPlaneSelector.tsx +10 -5
- package/components/AR/ViroARScene.tsx +19 -4
- package/components/Studio/StudioARScene.tsx +243 -48
- package/components/Studio/StudioSceneNavigator.tsx +15 -0
- package/components/Studio/VRTStudioModule.ts +10 -0
- package/components/Studio/domain/StudioSounds.tsx +46 -0
- package/components/Studio/domain/apiRequestHelpers.ts +612 -0
- package/components/Studio/domain/collisionBindingsRuntime.ts +17 -2
- package/components/Studio/domain/defaultApiRequestExecutor.ts +61 -0
- package/components/Studio/domain/expressionEvaluator.ts +603 -0
- package/components/Studio/domain/sceneNavigationHandler.ts +770 -20
- package/components/Studio/domain/soundManager.ts +141 -0
- package/components/Studio/domain/utils.ts +50 -0
- package/components/Studio/domain/variableStore.ts +74 -0
- package/components/Studio/domain/viroNodeFactory.tsx +175 -44
- package/components/Studio/domain/visibilityStore.ts +66 -0
- package/components/Studio/index.ts +1 -0
- package/components/Studio/types.ts +178 -1
- package/components/Utilities/VRModuleOpenXR.ts +35 -0
- package/components/Utilities/ViroVersion.ts +1 -1
- package/components/ViroAnimatedImage.tsx +1 -0
- package/components/ViroFlexView.tsx +1 -0
- package/components/ViroImage.tsx +1 -0
- package/components/ViroObjectDetector.tsx +230 -0
- package/components/ViroParticleEmitter.tsx +2 -0
- package/components/ViroPolygon.tsx +2 -1
- package/components/ViroQuad.tsx +2 -1
- package/components/ViroSurface.tsx +1 -0
- package/components/ViroVideo.tsx +1 -0
- package/components/ViroXRSceneNavigator.tsx +8 -0
- package/dist/components/AR/ViroARPlane.d.ts +1 -1
- package/dist/components/AR/ViroARPlane.js +1 -5
- package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
- package/dist/components/AR/ViroARPlaneSelector.js +10 -5
- package/dist/components/AR/ViroARScene.d.ts +13 -1
- package/dist/components/AR/ViroARScene.js +10 -6
- package/dist/components/Studio/StudioARScene.d.ts +3 -0
- package/dist/components/Studio/StudioARScene.js +142 -15
- package/dist/components/Studio/StudioSceneNavigator.js +14 -0
- package/dist/components/Studio/VRTStudioModule.d.ts +6 -0
- package/dist/components/Studio/VRTStudioModule.js +10 -0
- package/dist/components/Studio/domain/StudioSounds.d.ts +11 -0
- package/dist/components/Studio/domain/StudioSounds.js +69 -0
- package/dist/components/Studio/domain/apiRequestHelpers.d.ts +180 -0
- package/dist/components/Studio/domain/apiRequestHelpers.js +496 -0
- package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +3 -1
- package/dist/components/Studio/domain/collisionBindingsRuntime.js +4 -3
- package/dist/components/Studio/domain/defaultApiRequestExecutor.d.ts +14 -0
- package/dist/components/Studio/domain/defaultApiRequestExecutor.js +58 -0
- package/dist/components/Studio/domain/expressionEvaluator.d.ts +108 -0
- package/dist/components/Studio/domain/expressionEvaluator.js +520 -0
- package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +35 -8
- package/dist/components/Studio/domain/sceneNavigationHandler.js +564 -22
- package/dist/components/Studio/domain/soundManager.d.ts +52 -0
- package/dist/components/Studio/domain/soundManager.js +118 -0
- package/dist/components/Studio/domain/utils.d.ts +16 -0
- package/dist/components/Studio/domain/utils.js +48 -0
- package/dist/components/Studio/domain/variableStore.d.ts +21 -0
- package/dist/components/Studio/domain/variableStore.js +65 -0
- package/dist/components/Studio/domain/viroNodeFactory.d.ts +4 -9
- package/dist/components/Studio/domain/viroNodeFactory.js +84 -25
- package/dist/components/Studio/domain/visibilityStore.d.ts +33 -0
- package/dist/components/Studio/domain/visibilityStore.js +66 -0
- package/dist/components/Studio/index.d.ts +1 -1
- package/dist/components/Studio/types.d.ts +150 -1
- package/dist/components/Utilities/VRModuleOpenXR.d.ts +19 -0
- package/dist/components/Utilities/VRModuleOpenXR.js +16 -0
- package/dist/components/Utilities/ViroVersion.d.ts +1 -1
- package/dist/components/Utilities/ViroVersion.js +1 -1
- package/dist/components/ViroAnimatedImage.js +1 -0
- package/dist/components/ViroFlexView.js +1 -0
- package/dist/components/ViroImage.js +1 -0
- package/dist/components/ViroObjectDetector.d.ts +146 -0
- package/dist/components/ViroObjectDetector.js +93 -0
- package/dist/components/ViroParticleEmitter.js +2 -0
- package/dist/components/ViroPolygon.js +1 -0
- package/dist/components/ViroQuad.js +1 -0
- package/dist/components/ViroSurface.js +1 -0
- package/dist/components/ViroVideo.js +1 -0
- package/dist/components/ViroXRSceneNavigator.d.ts +8 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -2
- package/dist/plugins/withViro.js +2 -15
- package/dist/plugins/withViroAndroid.js +18 -5
- package/dist/plugins/withViroIos.js +0 -6
- package/index.ts +7 -3
- package/ios/ViroReact.podspec +25 -6
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +9 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARScene.h +14 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSceneDelegateiOS.h +8 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROGLTFLoader.h +5 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMonocularDepthEstimator.h +10 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROObjectDetector.h +134 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
- package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
- package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
- package/ios/dist/ViroRendererVisionOS/ViroKit.podspec +27 -0
- package/ios/dist/include/VRTARScene.h +1 -0
- package/ios/dist/lib/libViroReact.a +0 -0
- package/package.json +17 -20
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Request helpers: template validation + interpolation (URL / headers /
|
|
3
|
+
* JSON body), allowlist host matching, dot-path response extraction, and
|
|
4
|
+
* response->variable binding application. Pure logic, no platform APIs (no
|
|
5
|
+
* URL constructor — Hermes-safe), shared by the Studio editor, the
|
|
6
|
+
* scene-api-request egress proxy, and the scene runtime.
|
|
7
|
+
*
|
|
8
|
+
* Dual-maintained: this file exists in the Viro repo
|
|
9
|
+
* (components/Studio/domain/) and the ReactVisionStudio monorepo
|
|
10
|
+
* (packages/common/scene-assets/). Keep the two copies identical.
|
|
11
|
+
*
|
|
12
|
+
* Interpolation rules (fail closed — unknown or malformed placeholders are
|
|
13
|
+
* errors, never substituted with empty strings):
|
|
14
|
+
* - Placeholders are written {{name}} where name matches the
|
|
15
|
+
* scene_variables name constraint.
|
|
16
|
+
* - URL: the origin (scheme://host[:port]) must be static; placeholders are
|
|
17
|
+
* only legal after it and substitute percent-encoded.
|
|
18
|
+
* - Headers: values substitute as strings; control characters are rejected.
|
|
19
|
+
* - Body: the template must parse as JSON. A string value that is EXACTLY
|
|
20
|
+
* one placeholder substitutes as the typed value (number stays number);
|
|
21
|
+
* embedded placeholders substitute stringified; placeholders outside
|
|
22
|
+
* string values are invalid JSON and rejected. Re-serialisation makes
|
|
23
|
+
* structural injection impossible.
|
|
24
|
+
*/
|
|
25
|
+
export type ApiVariableType = "BOOLEAN" | "NUMBER" | "STRING";
|
|
26
|
+
export type ApiPrimitiveValue = boolean | number | string;
|
|
27
|
+
export type ApiRequestBindingSource = "BODY" | "STATUS" | "OK" | "ERROR_MESSAGE";
|
|
28
|
+
export type ApiHeaderTemplate = {
|
|
29
|
+
key: string;
|
|
30
|
+
value_template: string;
|
|
31
|
+
};
|
|
32
|
+
export declare const API_REQUEST_TIMEOUT_MIN_MS = 1000;
|
|
33
|
+
export declare const API_REQUEST_TIMEOUT_MAX_MS = 30000;
|
|
34
|
+
export declare const API_REQUEST_TIMEOUT_DEFAULT_MS = 10000;
|
|
35
|
+
export declare const API_REQUEST_MAX_URL_LENGTH = 2048;
|
|
36
|
+
export declare const API_REQUEST_MAX_BODY_LENGTH = 65536;
|
|
37
|
+
export declare const API_REQUEST_MAX_SELECTOR_LENGTH = 256;
|
|
38
|
+
export declare const API_REQUEST_MAX_VARIABLES = 32;
|
|
39
|
+
export declare const API_REQUEST_MAX_VARIABLE_VALUE_LENGTH = 4096;
|
|
40
|
+
export declare const API_REQUEST_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
41
|
+
export type ApiRequestMethod = (typeof API_REQUEST_METHODS)[number];
|
|
42
|
+
export type TemplateValidation = {
|
|
43
|
+
ok: true;
|
|
44
|
+
names: string[];
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
type Fail = {
|
|
50
|
+
ok: false;
|
|
51
|
+
error: string;
|
|
52
|
+
};
|
|
53
|
+
/** Names of every well-formed placeholder, in order of appearance. */
|
|
54
|
+
export declare const extractPlaceholders: (template: string) => string[];
|
|
55
|
+
/**
|
|
56
|
+
* A template string is valid when every "{{" / "}}" belongs to a well-formed
|
|
57
|
+
* placeholder. Malformed names ({{9x}}, {{a b}}) and stray braces are errors.
|
|
58
|
+
*/
|
|
59
|
+
export declare const validateTemplateString: (template: string) => TemplateValidation;
|
|
60
|
+
export type ValueLookup = (name: string) => ApiPrimitiveValue | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Fail-soft interpolation for display strings (alert title / message). Unlike
|
|
63
|
+
* the request templates this never errors: a well-formed {{name}} whose value
|
|
64
|
+
* is missing is left verbatim, so a stale or mistyped reference stays visible
|
|
65
|
+
* rather than blanking the field or suppressing the whole alert. Malformed
|
|
66
|
+
* braces (not a valid placeholder) are left untouched.
|
|
67
|
+
*/
|
|
68
|
+
export declare const interpolateDisplayTemplate: (template: string, get: ValueLookup) => string;
|
|
69
|
+
export type UrlTemplateValidation = {
|
|
70
|
+
ok: true;
|
|
71
|
+
host: string;
|
|
72
|
+
port: string | null;
|
|
73
|
+
names: string[];
|
|
74
|
+
} | Fail;
|
|
75
|
+
/**
|
|
76
|
+
* Validates scheme + static origin (no placeholders, no userinfo, no IPv6
|
|
77
|
+
* literal) and the placeholder syntax of the whole template. Returns the
|
|
78
|
+
* pinned host (lowercase, no port) and explicit port if present.
|
|
79
|
+
*/
|
|
80
|
+
export declare const validateUrlTemplate: (template: string) => UrlTemplateValidation;
|
|
81
|
+
export type UrlInterpolation = {
|
|
82
|
+
ok: true;
|
|
83
|
+
url: string;
|
|
84
|
+
} | Fail;
|
|
85
|
+
/**
|
|
86
|
+
* Substitutes placeholders percent-encoded into the path/query part. The
|
|
87
|
+
* origin is template-static by validation; the post-substitution recheck is
|
|
88
|
+
* belt-and-braces.
|
|
89
|
+
*/
|
|
90
|
+
export declare const interpolateUrlTemplate: (template: string, get: ValueLookup) => UrlInterpolation;
|
|
91
|
+
export declare const HEADER_NAME_PATTERN: RegExp;
|
|
92
|
+
export declare const isForbiddenHeaderName: (name: string) => boolean;
|
|
93
|
+
export type HeadersValidation = {
|
|
94
|
+
ok: true;
|
|
95
|
+
} | Fail;
|
|
96
|
+
export declare const validateHeaderTemplates: (headers: ApiHeaderTemplate[], options?: {
|
|
97
|
+
reservedNames?: string[];
|
|
98
|
+
}) => HeadersValidation;
|
|
99
|
+
export type HeadersInterpolation = {
|
|
100
|
+
ok: true;
|
|
101
|
+
headers: {
|
|
102
|
+
key: string;
|
|
103
|
+
value: string;
|
|
104
|
+
}[];
|
|
105
|
+
} | Fail;
|
|
106
|
+
export declare const interpolateHeaders: (headers: ApiHeaderTemplate[], get: ValueLookup, options?: {
|
|
107
|
+
reservedNames?: string[];
|
|
108
|
+
}) => HeadersInterpolation;
|
|
109
|
+
export type BodyValidation = {
|
|
110
|
+
ok: true;
|
|
111
|
+
names: string[];
|
|
112
|
+
} | Fail;
|
|
113
|
+
/**
|
|
114
|
+
* The body template must be JSON; placeholders may only appear inside string
|
|
115
|
+
* values (a bare {{n}} outside a string is invalid JSON by construction).
|
|
116
|
+
*/
|
|
117
|
+
export declare const validateBodyTemplate: (template: string) => BodyValidation;
|
|
118
|
+
export type BodyInterpolation = {
|
|
119
|
+
ok: true;
|
|
120
|
+
body: string;
|
|
121
|
+
} | Fail;
|
|
122
|
+
/**
|
|
123
|
+
* Parse -> walk -> substitute (typed for whole-placeholder strings) ->
|
|
124
|
+
* re-stringify. The serialiser guarantees escaping, so variable values can
|
|
125
|
+
* never change the JSON structure.
|
|
126
|
+
*/
|
|
127
|
+
export declare const interpolateJsonBody: (template: string, get: ValueLookup) => BodyInterpolation;
|
|
128
|
+
/**
|
|
129
|
+
* Pattern grammar: "host.tld" or "*.host.tld", optional ":port". A pattern
|
|
130
|
+
* without a port matches the default HTTPS port only (no explicit port in the
|
|
131
|
+
* URL); a pattern with a port matches exactly that port. The wildcard matches
|
|
132
|
+
* subdomains at a label boundary, never the apex itself.
|
|
133
|
+
*/
|
|
134
|
+
export declare const matchHostPattern: (host: string, port: string | null, pattern: string) => boolean;
|
|
135
|
+
export declare const isHostAllowlisted: (host: string, port: string | null, patterns: string[]) => boolean;
|
|
136
|
+
export declare const validateSelector: (selector: string) => boolean;
|
|
137
|
+
export type ExtractionResult = {
|
|
138
|
+
found: true;
|
|
139
|
+
value: unknown;
|
|
140
|
+
} | {
|
|
141
|
+
found: false;
|
|
142
|
+
};
|
|
143
|
+
/** Walks `data.items[0].name`-style selectors. Missing path -> not found. */
|
|
144
|
+
export declare const extractByPath: (value: unknown, selector: string) => ExtractionResult;
|
|
145
|
+
export type ApiRequestBindingInput = {
|
|
146
|
+
source: ApiRequestBindingSource;
|
|
147
|
+
selector: string | null;
|
|
148
|
+
variable_name: string;
|
|
149
|
+
variable_type: ApiVariableType;
|
|
150
|
+
};
|
|
151
|
+
export type ApiRequestOutcomeInput = {
|
|
152
|
+
ok: boolean;
|
|
153
|
+
status: number | null;
|
|
154
|
+
body?: unknown;
|
|
155
|
+
error_message?: string | null;
|
|
156
|
+
};
|
|
157
|
+
export type CoercionResult = {
|
|
158
|
+
ok: true;
|
|
159
|
+
value: ApiPrimitiveValue;
|
|
160
|
+
} | {
|
|
161
|
+
ok: false;
|
|
162
|
+
};
|
|
163
|
+
/** Primitive-only, lossless-ish coercion into the variable's declared type. */
|
|
164
|
+
export declare const coerceBindingValue: (value: unknown, type: ApiVariableType) => CoercionResult;
|
|
165
|
+
export type ApiRequestBindingWrite = {
|
|
166
|
+
name: string;
|
|
167
|
+
type: ApiVariableType;
|
|
168
|
+
value: ApiPrimitiveValue;
|
|
169
|
+
};
|
|
170
|
+
export type BindingApplication = {
|
|
171
|
+
writes: ApiRequestBindingWrite[];
|
|
172
|
+
warnings: string[];
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Pure: resolves each applicable binding against the outcome and returns the
|
|
176
|
+
* variable writes plus warnings for skipped bindings (missing path, type
|
|
177
|
+
* mismatch). The caller applies writes to its variable store.
|
|
178
|
+
*/
|
|
179
|
+
export declare const applyBindings: (bindings: ApiRequestBindingInput[], outcome: ApiRequestOutcomeInput) => BindingApplication;
|
|
180
|
+
export {};
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API Request helpers: template validation + interpolation (URL / headers /
|
|
4
|
+
* JSON body), allowlist host matching, dot-path response extraction, and
|
|
5
|
+
* response->variable binding application. Pure logic, no platform APIs (no
|
|
6
|
+
* URL constructor — Hermes-safe), shared by the Studio editor, the
|
|
7
|
+
* scene-api-request egress proxy, and the scene runtime.
|
|
8
|
+
*
|
|
9
|
+
* Dual-maintained: this file exists in the Viro repo
|
|
10
|
+
* (components/Studio/domain/) and the ReactVisionStudio monorepo
|
|
11
|
+
* (packages/common/scene-assets/). Keep the two copies identical.
|
|
12
|
+
*
|
|
13
|
+
* Interpolation rules (fail closed — unknown or malformed placeholders are
|
|
14
|
+
* errors, never substituted with empty strings):
|
|
15
|
+
* - Placeholders are written {{name}} where name matches the
|
|
16
|
+
* scene_variables name constraint.
|
|
17
|
+
* - URL: the origin (scheme://host[:port]) must be static; placeholders are
|
|
18
|
+
* only legal after it and substitute percent-encoded.
|
|
19
|
+
* - Headers: values substitute as strings; control characters are rejected.
|
|
20
|
+
* - Body: the template must parse as JSON. A string value that is EXACTLY
|
|
21
|
+
* one placeholder substitutes as the typed value (number stays number);
|
|
22
|
+
* embedded placeholders substitute stringified; placeholders outside
|
|
23
|
+
* string values are invalid JSON and rejected. Re-serialisation makes
|
|
24
|
+
* structural injection impossible.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.applyBindings = exports.coerceBindingValue = exports.extractByPath = exports.validateSelector = exports.isHostAllowlisted = exports.matchHostPattern = exports.interpolateJsonBody = exports.validateBodyTemplate = exports.interpolateHeaders = exports.validateHeaderTemplates = exports.isForbiddenHeaderName = exports.HEADER_NAME_PATTERN = exports.interpolateUrlTemplate = exports.validateUrlTemplate = exports.interpolateDisplayTemplate = exports.validateTemplateString = exports.extractPlaceholders = exports.API_REQUEST_METHODS = exports.API_REQUEST_MAX_VARIABLE_VALUE_LENGTH = exports.API_REQUEST_MAX_VARIABLES = exports.API_REQUEST_MAX_SELECTOR_LENGTH = exports.API_REQUEST_MAX_BODY_LENGTH = exports.API_REQUEST_MAX_URL_LENGTH = exports.API_REQUEST_TIMEOUT_DEFAULT_MS = exports.API_REQUEST_TIMEOUT_MAX_MS = exports.API_REQUEST_TIMEOUT_MIN_MS = void 0;
|
|
28
|
+
exports.API_REQUEST_TIMEOUT_MIN_MS = 1000;
|
|
29
|
+
exports.API_REQUEST_TIMEOUT_MAX_MS = 30000;
|
|
30
|
+
exports.API_REQUEST_TIMEOUT_DEFAULT_MS = 10000;
|
|
31
|
+
exports.API_REQUEST_MAX_URL_LENGTH = 2048;
|
|
32
|
+
exports.API_REQUEST_MAX_BODY_LENGTH = 65536;
|
|
33
|
+
exports.API_REQUEST_MAX_SELECTOR_LENGTH = 256;
|
|
34
|
+
exports.API_REQUEST_MAX_VARIABLES = 32;
|
|
35
|
+
exports.API_REQUEST_MAX_VARIABLE_VALUE_LENGTH = 4096;
|
|
36
|
+
exports.API_REQUEST_METHODS = [
|
|
37
|
+
"GET",
|
|
38
|
+
"POST",
|
|
39
|
+
"PUT",
|
|
40
|
+
"PATCH",
|
|
41
|
+
"DELETE",
|
|
42
|
+
];
|
|
43
|
+
const PLACEHOLDER_NAME = "[A-Za-z_][A-Za-z0-9_]{0,63}";
|
|
44
|
+
const PLACEHOLDER_GLOBAL = new RegExp(`\\{\\{(${PLACEHOLDER_NAME})\\}\\}`, "g");
|
|
45
|
+
const WHOLE_PLACEHOLDER = new RegExp(`^\\{\\{(${PLACEHOLDER_NAME})\\}\\}$`);
|
|
46
|
+
const fail = (error) => ({ ok: false, error });
|
|
47
|
+
/** Names of every well-formed placeholder, in order of appearance. */
|
|
48
|
+
const extractPlaceholders = (template) => {
|
|
49
|
+
const names = [];
|
|
50
|
+
for (const match of template.matchAll(PLACEHOLDER_GLOBAL)) {
|
|
51
|
+
names.push(match[1]);
|
|
52
|
+
}
|
|
53
|
+
return names;
|
|
54
|
+
};
|
|
55
|
+
exports.extractPlaceholders = extractPlaceholders;
|
|
56
|
+
/**
|
|
57
|
+
* A template string is valid when every "{{" / "}}" belongs to a well-formed
|
|
58
|
+
* placeholder. Malformed names ({{9x}}, {{a b}}) and stray braces are errors.
|
|
59
|
+
*/
|
|
60
|
+
const validateTemplateString = (template) => {
|
|
61
|
+
const names = (0, exports.extractPlaceholders)(template);
|
|
62
|
+
const residue = template.replace(PLACEHOLDER_GLOBAL, "");
|
|
63
|
+
if (residue.includes("{{") || residue.includes("}}")) {
|
|
64
|
+
return fail("Malformed placeholder; use {{variableName}}");
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, names };
|
|
67
|
+
};
|
|
68
|
+
exports.validateTemplateString = validateTemplateString;
|
|
69
|
+
const substitute = (template, get, encode) => {
|
|
70
|
+
const validation = (0, exports.validateTemplateString)(template);
|
|
71
|
+
if (!validation.ok)
|
|
72
|
+
return validation;
|
|
73
|
+
let missing = null;
|
|
74
|
+
const value = template.replace(PLACEHOLDER_GLOBAL, (_, name) => {
|
|
75
|
+
const resolved = get(name);
|
|
76
|
+
if (resolved === undefined) {
|
|
77
|
+
missing = missing ?? name;
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
const raw = String(resolved);
|
|
81
|
+
return encode ? encode(raw) : raw;
|
|
82
|
+
});
|
|
83
|
+
if (missing)
|
|
84
|
+
return fail(`Unknown variable {{${missing}}}`);
|
|
85
|
+
return { ok: true, value };
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Fail-soft interpolation for display strings (alert title / message). Unlike
|
|
89
|
+
* the request templates this never errors: a well-formed {{name}} whose value
|
|
90
|
+
* is missing is left verbatim, so a stale or mistyped reference stays visible
|
|
91
|
+
* rather than blanking the field or suppressing the whole alert. Malformed
|
|
92
|
+
* braces (not a valid placeholder) are left untouched.
|
|
93
|
+
*/
|
|
94
|
+
const interpolateDisplayTemplate = (template, get) => template.replace(PLACEHOLDER_GLOBAL, (whole, name) => {
|
|
95
|
+
const resolved = get(name);
|
|
96
|
+
return resolved === undefined ? whole : String(resolved);
|
|
97
|
+
});
|
|
98
|
+
exports.interpolateDisplayTemplate = interpolateDisplayTemplate;
|
|
99
|
+
// ── URL templates ───────────────────────────────────────────────────────────
|
|
100
|
+
// Static origin grammar: https://host[:port] with >= 2 lowercase ASCII labels
|
|
101
|
+
// (same grammar as allowlist patterns; IDNs must be pre-punycoded).
|
|
102
|
+
const URL_ORIGIN_PATTERN = /^https:\/\/(([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]*[a-z0-9])?)(:([0-9]{1,5}))?$/;
|
|
103
|
+
const splitOrigin = (template) => {
|
|
104
|
+
const afterScheme = "https://".length;
|
|
105
|
+
const pathStart = template.slice(afterScheme).search(/[/?#]/);
|
|
106
|
+
if (pathStart === -1)
|
|
107
|
+
return { origin: template, rest: "" };
|
|
108
|
+
return {
|
|
109
|
+
origin: template.slice(0, afterScheme + pathStart),
|
|
110
|
+
rest: template.slice(afterScheme + pathStart),
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Validates scheme + static origin (no placeholders, no userinfo, no IPv6
|
|
115
|
+
* literal) and the placeholder syntax of the whole template. Returns the
|
|
116
|
+
* pinned host (lowercase, no port) and explicit port if present.
|
|
117
|
+
*/
|
|
118
|
+
const validateUrlTemplate = (template) => {
|
|
119
|
+
if (template.length > exports.API_REQUEST_MAX_URL_LENGTH) {
|
|
120
|
+
return fail("URL is too long");
|
|
121
|
+
}
|
|
122
|
+
if (!/^https:\/\//i.test(template)) {
|
|
123
|
+
return fail("URL must start with https://");
|
|
124
|
+
}
|
|
125
|
+
const { origin } = splitOrigin(template);
|
|
126
|
+
if (origin.includes("{") || origin.includes("}")) {
|
|
127
|
+
return fail("Placeholders are not allowed in the URL host");
|
|
128
|
+
}
|
|
129
|
+
if (origin.includes("@")) {
|
|
130
|
+
return fail("Credentials are not allowed in the URL");
|
|
131
|
+
}
|
|
132
|
+
const originMatch = URL_ORIGIN_PATTERN.exec(origin.toLowerCase());
|
|
133
|
+
if (!originMatch) {
|
|
134
|
+
return fail("Invalid URL host");
|
|
135
|
+
}
|
|
136
|
+
const whole = (0, exports.validateTemplateString)(template);
|
|
137
|
+
if (!whole.ok)
|
|
138
|
+
return whole;
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
host: originMatch[1],
|
|
142
|
+
port: originMatch[6] ?? null,
|
|
143
|
+
names: whole.names,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
exports.validateUrlTemplate = validateUrlTemplate;
|
|
147
|
+
/**
|
|
148
|
+
* Substitutes placeholders percent-encoded into the path/query part. The
|
|
149
|
+
* origin is template-static by validation; the post-substitution recheck is
|
|
150
|
+
* belt-and-braces.
|
|
151
|
+
*/
|
|
152
|
+
const interpolateUrlTemplate = (template, get) => {
|
|
153
|
+
const validation = (0, exports.validateUrlTemplate)(template);
|
|
154
|
+
if (!validation.ok)
|
|
155
|
+
return validation;
|
|
156
|
+
const { origin, rest } = splitOrigin(template);
|
|
157
|
+
const substituted = substitute(rest, get, encodeURIComponent);
|
|
158
|
+
if (!substituted.ok)
|
|
159
|
+
return substituted;
|
|
160
|
+
const url = origin + substituted.value;
|
|
161
|
+
const recheck = splitOrigin(url);
|
|
162
|
+
if (recheck.origin !== origin || recheck.origin.includes("@")) {
|
|
163
|
+
return fail("URL host changed after interpolation");
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, url };
|
|
166
|
+
};
|
|
167
|
+
exports.interpolateUrlTemplate = interpolateUrlTemplate;
|
|
168
|
+
// ── Header templates ────────────────────────────────────────────────────────
|
|
169
|
+
exports.HEADER_NAME_PATTERN = /^[A-Za-z0-9-]{1,64}$/;
|
|
170
|
+
// Never author-controllable: request framing, proxy semantics, and anything
|
|
171
|
+
// the platform itself sets. The connection auth header is appended per call.
|
|
172
|
+
const FORBIDDEN_HEADER_NAMES = new Set([
|
|
173
|
+
"host",
|
|
174
|
+
"content-length",
|
|
175
|
+
"transfer-encoding",
|
|
176
|
+
"connection",
|
|
177
|
+
"upgrade",
|
|
178
|
+
"te",
|
|
179
|
+
"trailer",
|
|
180
|
+
"expect",
|
|
181
|
+
"forwarded",
|
|
182
|
+
]);
|
|
183
|
+
const FORBIDDEN_HEADER_PREFIXES = ["proxy-", "sec-", "x-forwarded-"];
|
|
184
|
+
const isForbiddenHeaderName = (name) => {
|
|
185
|
+
const lower = name.toLowerCase();
|
|
186
|
+
if (FORBIDDEN_HEADER_NAMES.has(lower))
|
|
187
|
+
return true;
|
|
188
|
+
return FORBIDDEN_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
189
|
+
};
|
|
190
|
+
exports.isForbiddenHeaderName = isForbiddenHeaderName;
|
|
191
|
+
const validateHeaderTemplates = (headers, options) => {
|
|
192
|
+
const reserved = new Set((options?.reservedNames ?? []).map((name) => name.toLowerCase()));
|
|
193
|
+
for (const header of headers) {
|
|
194
|
+
if (!exports.HEADER_NAME_PATTERN.test(header.key)) {
|
|
195
|
+
return fail(`Invalid header name "${header.key}"`);
|
|
196
|
+
}
|
|
197
|
+
if ((0, exports.isForbiddenHeaderName)(header.key)) {
|
|
198
|
+
return fail(`Header "${header.key}" is not allowed`);
|
|
199
|
+
}
|
|
200
|
+
if (reserved.has(header.key.toLowerCase())) {
|
|
201
|
+
return fail(`Header "${header.key}" is set by the connection`);
|
|
202
|
+
}
|
|
203
|
+
const value = (0, exports.validateTemplateString)(header.value_template);
|
|
204
|
+
if (!value.ok)
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
return { ok: true };
|
|
208
|
+
};
|
|
209
|
+
exports.validateHeaderTemplates = validateHeaderTemplates;
|
|
210
|
+
const interpolateHeaders = (headers, get, options) => {
|
|
211
|
+
const validation = (0, exports.validateHeaderTemplates)(headers, options);
|
|
212
|
+
if (!validation.ok)
|
|
213
|
+
return validation;
|
|
214
|
+
const result = [];
|
|
215
|
+
for (const header of headers) {
|
|
216
|
+
const substituted = substitute(header.value_template, get);
|
|
217
|
+
if (!substituted.ok)
|
|
218
|
+
return substituted;
|
|
219
|
+
// eslint-disable-next-line no-control-regex
|
|
220
|
+
if (/[\x00-\x1F\x7F]/.test(substituted.value)) {
|
|
221
|
+
return fail(`Header "${header.key}" value contains control characters`);
|
|
222
|
+
}
|
|
223
|
+
result.push({ key: header.key, value: substituted.value });
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, headers: result };
|
|
226
|
+
};
|
|
227
|
+
exports.interpolateHeaders = interpolateHeaders;
|
|
228
|
+
const collectBodyPlaceholders = (node, names) => {
|
|
229
|
+
if (typeof node === "string") {
|
|
230
|
+
const validation = (0, exports.validateTemplateString)(node);
|
|
231
|
+
if (!validation.ok)
|
|
232
|
+
return validation;
|
|
233
|
+
names.push(...validation.names);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (Array.isArray(node)) {
|
|
237
|
+
for (const item of node) {
|
|
238
|
+
const error = collectBodyPlaceholders(item, names);
|
|
239
|
+
if (error)
|
|
240
|
+
return error;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (node !== null && typeof node === "object") {
|
|
245
|
+
for (const [key, value] of Object.entries(node)) {
|
|
246
|
+
if (key.includes("{{") || key.includes("}}")) {
|
|
247
|
+
return fail("Placeholders are not allowed in JSON keys");
|
|
248
|
+
}
|
|
249
|
+
const error = collectBodyPlaceholders(value, names);
|
|
250
|
+
if (error)
|
|
251
|
+
return error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* The body template must be JSON; placeholders may only appear inside string
|
|
258
|
+
* values (a bare {{n}} outside a string is invalid JSON by construction).
|
|
259
|
+
*/
|
|
260
|
+
const validateBodyTemplate = (template) => {
|
|
261
|
+
if (template.length > exports.API_REQUEST_MAX_BODY_LENGTH) {
|
|
262
|
+
return fail("Body is too long");
|
|
263
|
+
}
|
|
264
|
+
let parsed;
|
|
265
|
+
try {
|
|
266
|
+
parsed = JSON.parse(template);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return fail("Body must be valid JSON (placeholders only inside strings)");
|
|
270
|
+
}
|
|
271
|
+
const names = [];
|
|
272
|
+
const error = collectBodyPlaceholders(parsed, names);
|
|
273
|
+
if (error)
|
|
274
|
+
return error;
|
|
275
|
+
return { ok: true, names };
|
|
276
|
+
};
|
|
277
|
+
exports.validateBodyTemplate = validateBodyTemplate;
|
|
278
|
+
const interpolateBodyNode = (node, get) => {
|
|
279
|
+
if (typeof node === "string") {
|
|
280
|
+
const whole = WHOLE_PLACEHOLDER.exec(node);
|
|
281
|
+
if (whole) {
|
|
282
|
+
const resolved = get(whole[1]);
|
|
283
|
+
if (resolved === undefined)
|
|
284
|
+
return fail(`Unknown variable {{${whole[1]}}}`);
|
|
285
|
+
return { ok: true, value: resolved };
|
|
286
|
+
}
|
|
287
|
+
const substituted = substitute(node, get);
|
|
288
|
+
if (!substituted.ok)
|
|
289
|
+
return substituted;
|
|
290
|
+
return { ok: true, value: substituted.value };
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(node)) {
|
|
293
|
+
const items = [];
|
|
294
|
+
for (const item of node) {
|
|
295
|
+
const result = interpolateBodyNode(item, get);
|
|
296
|
+
if (!result.ok)
|
|
297
|
+
return result;
|
|
298
|
+
items.push(result.value);
|
|
299
|
+
}
|
|
300
|
+
return { ok: true, value: items };
|
|
301
|
+
}
|
|
302
|
+
if (node !== null && typeof node === "object") {
|
|
303
|
+
const out = {};
|
|
304
|
+
for (const [key, value] of Object.entries(node)) {
|
|
305
|
+
const result = interpolateBodyNode(value, get);
|
|
306
|
+
if (!result.ok)
|
|
307
|
+
return result;
|
|
308
|
+
out[key] = result.value;
|
|
309
|
+
}
|
|
310
|
+
return { ok: true, value: out };
|
|
311
|
+
}
|
|
312
|
+
return { ok: true, value: node };
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Parse -> walk -> substitute (typed for whole-placeholder strings) ->
|
|
316
|
+
* re-stringify. The serialiser guarantees escaping, so variable values can
|
|
317
|
+
* never change the JSON structure.
|
|
318
|
+
*/
|
|
319
|
+
const interpolateJsonBody = (template, get) => {
|
|
320
|
+
const validation = (0, exports.validateBodyTemplate)(template);
|
|
321
|
+
if (!validation.ok)
|
|
322
|
+
return validation;
|
|
323
|
+
const parsed = JSON.parse(template);
|
|
324
|
+
const result = interpolateBodyNode(parsed, get);
|
|
325
|
+
if (!result.ok)
|
|
326
|
+
return result;
|
|
327
|
+
return { ok: true, body: JSON.stringify(result.value) };
|
|
328
|
+
};
|
|
329
|
+
exports.interpolateJsonBody = interpolateJsonBody;
|
|
330
|
+
// ── Allowlist matching ──────────────────────────────────────────────────────
|
|
331
|
+
/**
|
|
332
|
+
* Pattern grammar: "host.tld" or "*.host.tld", optional ":port". A pattern
|
|
333
|
+
* without a port matches the default HTTPS port only (no explicit port in the
|
|
334
|
+
* URL); a pattern with a port matches exactly that port. The wildcard matches
|
|
335
|
+
* subdomains at a label boundary, never the apex itself.
|
|
336
|
+
*/
|
|
337
|
+
const matchHostPattern = (host, port, pattern) => {
|
|
338
|
+
const colon = pattern.indexOf(":");
|
|
339
|
+
const patternHost = colon === -1 ? pattern : pattern.slice(0, colon);
|
|
340
|
+
const patternPort = colon === -1 ? null : pattern.slice(colon + 1);
|
|
341
|
+
if (patternPort === null ? port !== null : port !== patternPort) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
const lowerHost = host.toLowerCase();
|
|
345
|
+
if (patternHost.startsWith("*.")) {
|
|
346
|
+
const suffix = patternHost.slice(2);
|
|
347
|
+
return lowerHost.endsWith(`.${suffix}`) && lowerHost.length > suffix.length + 1;
|
|
348
|
+
}
|
|
349
|
+
return lowerHost === patternHost;
|
|
350
|
+
};
|
|
351
|
+
exports.matchHostPattern = matchHostPattern;
|
|
352
|
+
const isHostAllowlisted = (host, port, patterns) => patterns.some((pattern) => (0, exports.matchHostPattern)(host, port, pattern));
|
|
353
|
+
exports.isHostAllowlisted = isHostAllowlisted;
|
|
354
|
+
// ── Response extraction (dot-path subset) ───────────────────────────────────
|
|
355
|
+
// Segment: optional key followed by any number of [n] indices. The selector
|
|
356
|
+
// field stays a plain string so richer JSONPath can be introduced later.
|
|
357
|
+
const SELECTOR_SEGMENT_PATTERN = /^([A-Za-z0-9_$-]+)?((?:\[\d+\])*)$/;
|
|
358
|
+
const parseSelector = (selector) => {
|
|
359
|
+
if (selector.length === 0 ||
|
|
360
|
+
selector.length > exports.API_REQUEST_MAX_SELECTOR_LENGTH) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const tokens = [];
|
|
364
|
+
for (const segment of selector.split(".")) {
|
|
365
|
+
const match = SELECTOR_SEGMENT_PATTERN.exec(segment);
|
|
366
|
+
if (!match || segment.length === 0)
|
|
367
|
+
return null;
|
|
368
|
+
if (match[1])
|
|
369
|
+
tokens.push({ kind: "key", key: match[1] });
|
|
370
|
+
if (match[2]) {
|
|
371
|
+
for (const index of match[2].matchAll(/\[(\d+)\]/g)) {
|
|
372
|
+
tokens.push({ kind: "index", index: Number(index[1]) });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return tokens.length > 0 ? tokens : null;
|
|
377
|
+
};
|
|
378
|
+
const validateSelector = (selector) => parseSelector(selector) !== null;
|
|
379
|
+
exports.validateSelector = validateSelector;
|
|
380
|
+
/** Walks `data.items[0].name`-style selectors. Missing path -> not found. */
|
|
381
|
+
const extractByPath = (value, selector) => {
|
|
382
|
+
const tokens = parseSelector(selector);
|
|
383
|
+
if (!tokens)
|
|
384
|
+
return { found: false };
|
|
385
|
+
let current = value;
|
|
386
|
+
for (const token of tokens) {
|
|
387
|
+
if (token.kind === "key") {
|
|
388
|
+
if (current === null ||
|
|
389
|
+
typeof current !== "object" ||
|
|
390
|
+
Array.isArray(current) ||
|
|
391
|
+
!(token.key in current)) {
|
|
392
|
+
return { found: false };
|
|
393
|
+
}
|
|
394
|
+
current = current[token.key];
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
if (!Array.isArray(current) || token.index >= current.length) {
|
|
398
|
+
return { found: false };
|
|
399
|
+
}
|
|
400
|
+
current = current[token.index];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { found: true, value: current };
|
|
404
|
+
};
|
|
405
|
+
exports.extractByPath = extractByPath;
|
|
406
|
+
/** Primitive-only, lossless-ish coercion into the variable's declared type. */
|
|
407
|
+
const coerceBindingValue = (value, type) => {
|
|
408
|
+
if (type === "STRING") {
|
|
409
|
+
if (typeof value === "string")
|
|
410
|
+
return { ok: true, value };
|
|
411
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
412
|
+
return { ok: true, value: String(value) };
|
|
413
|
+
}
|
|
414
|
+
if (typeof value === "boolean")
|
|
415
|
+
return { ok: true, value: String(value) };
|
|
416
|
+
return { ok: false };
|
|
417
|
+
}
|
|
418
|
+
if (type === "NUMBER") {
|
|
419
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
420
|
+
return { ok: true, value };
|
|
421
|
+
}
|
|
422
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
423
|
+
const parsed = Number(value);
|
|
424
|
+
if (Number.isFinite(parsed))
|
|
425
|
+
return { ok: true, value: parsed };
|
|
426
|
+
}
|
|
427
|
+
return { ok: false };
|
|
428
|
+
}
|
|
429
|
+
if (typeof value === "boolean")
|
|
430
|
+
return { ok: true, value };
|
|
431
|
+
if (value === "true")
|
|
432
|
+
return { ok: true, value: true };
|
|
433
|
+
if (value === "false")
|
|
434
|
+
return { ok: true, value: false };
|
|
435
|
+
return { ok: false };
|
|
436
|
+
};
|
|
437
|
+
exports.coerceBindingValue = coerceBindingValue;
|
|
438
|
+
// Which sources are meaningful per outcome path.
|
|
439
|
+
const SUCCESS_SOURCES = ["BODY", "STATUS", "OK"];
|
|
440
|
+
const FAILURE_SOURCES = [
|
|
441
|
+
"STATUS",
|
|
442
|
+
"OK",
|
|
443
|
+
"ERROR_MESSAGE",
|
|
444
|
+
];
|
|
445
|
+
/**
|
|
446
|
+
* Pure: resolves each applicable binding against the outcome and returns the
|
|
447
|
+
* variable writes plus warnings for skipped bindings (missing path, type
|
|
448
|
+
* mismatch). The caller applies writes to its variable store.
|
|
449
|
+
*/
|
|
450
|
+
const applyBindings = (bindings, outcome) => {
|
|
451
|
+
const applicable = outcome.ok ? SUCCESS_SOURCES : FAILURE_SOURCES;
|
|
452
|
+
const writes = [];
|
|
453
|
+
const warnings = [];
|
|
454
|
+
for (const binding of bindings) {
|
|
455
|
+
if (!applicable.includes(binding.source))
|
|
456
|
+
continue;
|
|
457
|
+
let raw;
|
|
458
|
+
if (binding.source === "BODY") {
|
|
459
|
+
if (!binding.selector) {
|
|
460
|
+
warnings.push(`Binding to ${binding.variable_name}: missing selector`);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const extracted = (0, exports.extractByPath)(outcome.body, binding.selector);
|
|
464
|
+
if (!extracted.found) {
|
|
465
|
+
warnings.push(`Binding to ${binding.variable_name}: "${binding.selector}" not found in response`);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
raw = extracted.value;
|
|
469
|
+
}
|
|
470
|
+
else if (binding.source === "STATUS") {
|
|
471
|
+
if (outcome.status === null) {
|
|
472
|
+
warnings.push(`Binding to ${binding.variable_name}: no status code`);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
raw = outcome.status;
|
|
476
|
+
}
|
|
477
|
+
else if (binding.source === "OK") {
|
|
478
|
+
raw = outcome.ok;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
raw = outcome.error_message ?? "";
|
|
482
|
+
}
|
|
483
|
+
const coerced = (0, exports.coerceBindingValue)(raw, binding.variable_type);
|
|
484
|
+
if (!coerced.ok) {
|
|
485
|
+
warnings.push(`Binding to ${binding.variable_name}: value is not a ${binding.variable_type}`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
writes.push({
|
|
489
|
+
name: binding.variable_name,
|
|
490
|
+
type: binding.variable_type,
|
|
491
|
+
value: coerced.value,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return { writes, warnings };
|
|
495
|
+
};
|
|
496
|
+
exports.applyBindings = applyBindings;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MutableRefObject } from "react";
|
|
2
2
|
import { StudioAnimation, StudioCollisionBinding } from "../types";
|
|
3
|
+
import { SequenceRuntimeContext } from "./sceneNavigationHandler";
|
|
3
4
|
/**
|
|
4
5
|
* Dispatches scene functions for collision bindings matching the canonical pair.
|
|
5
6
|
* Cooldown prevents per-frame spam while physics contacts overlap.
|
|
@@ -14,8 +15,9 @@ export declare function dispatchCollisionBindingActions(params: {
|
|
|
14
15
|
onAnimationTrigger?: (targetAssetId: string, animationKey: string) => void;
|
|
15
16
|
cooldownMs?: number;
|
|
16
17
|
lastFiredRef: MutableRefObject<Map<string, number>>;
|
|
18
|
+
runtimeCtx?: SequenceRuntimeContext;
|
|
17
19
|
}): void;
|
|
18
20
|
/**
|
|
19
21
|
* Returns an onCollision handler for a given placement asset ID.
|
|
20
22
|
*/
|
|
21
|
-
export declare function createPlacementCollisionHandler(placementId: string, bindingsByPairKey: Map<string, StudioCollisionBinding[]>, sceneNavigator: unknown, animations: StudioAnimation[], lastFiredRef: MutableRefObject<Map<string, number>>, onAnimationTrigger?: (targetAssetId: string, animationKey: string) => void, onSceneChange?: (sceneId: string, sceneName: string) => void): (viroTag: string, collidedPoint: [number, number, number], collidedNormal: [number, number, number]) => void;
|
|
23
|
+
export declare function createPlacementCollisionHandler(placementId: string, bindingsByPairKey: Map<string, StudioCollisionBinding[]>, sceneNavigator: unknown, animations: StudioAnimation[], lastFiredRef: MutableRefObject<Map<string, number>>, onAnimationTrigger?: (targetAssetId: string, animationKey: string) => void, onSceneChange?: (sceneId: string, sceneName: string) => void, runtimeCtx?: SequenceRuntimeContext): (viroTag: string, collidedPoint: [number, number, number], collidedNormal: [number, number, number]) => void;
|