@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.
Files changed (106) hide show
  1. package/README.md +8 -0
  2. package/android/react_viro/react_viro-release.aar +0 -0
  3. package/android/viro_renderer/viro_renderer-release.aar +0 -0
  4. package/components/AR/ViroARPlane.tsx +1 -4
  5. package/components/AR/ViroARPlaneSelector.tsx +10 -5
  6. package/components/AR/ViroARScene.tsx +19 -4
  7. package/components/Studio/StudioARScene.tsx +243 -48
  8. package/components/Studio/StudioSceneNavigator.tsx +15 -0
  9. package/components/Studio/VRTStudioModule.ts +10 -0
  10. package/components/Studio/domain/StudioSounds.tsx +46 -0
  11. package/components/Studio/domain/apiRequestHelpers.ts +612 -0
  12. package/components/Studio/domain/collisionBindingsRuntime.ts +17 -2
  13. package/components/Studio/domain/defaultApiRequestExecutor.ts +61 -0
  14. package/components/Studio/domain/expressionEvaluator.ts +603 -0
  15. package/components/Studio/domain/sceneNavigationHandler.ts +770 -20
  16. package/components/Studio/domain/soundManager.ts +141 -0
  17. package/components/Studio/domain/utils.ts +50 -0
  18. package/components/Studio/domain/variableStore.ts +74 -0
  19. package/components/Studio/domain/viroNodeFactory.tsx +175 -44
  20. package/components/Studio/domain/visibilityStore.ts +66 -0
  21. package/components/Studio/index.ts +1 -0
  22. package/components/Studio/types.ts +178 -1
  23. package/components/Utilities/VRModuleOpenXR.ts +35 -0
  24. package/components/Utilities/ViroVersion.ts +1 -1
  25. package/components/ViroAnimatedImage.tsx +1 -0
  26. package/components/ViroFlexView.tsx +1 -0
  27. package/components/ViroImage.tsx +1 -0
  28. package/components/ViroObjectDetector.tsx +230 -0
  29. package/components/ViroParticleEmitter.tsx +2 -0
  30. package/components/ViroPolygon.tsx +2 -1
  31. package/components/ViroQuad.tsx +2 -1
  32. package/components/ViroSurface.tsx +1 -0
  33. package/components/ViroVideo.tsx +1 -0
  34. package/components/ViroXRSceneNavigator.tsx +8 -0
  35. package/dist/components/AR/ViroARPlane.d.ts +1 -1
  36. package/dist/components/AR/ViroARPlane.js +1 -5
  37. package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
  38. package/dist/components/AR/ViroARPlaneSelector.js +10 -5
  39. package/dist/components/AR/ViroARScene.d.ts +13 -1
  40. package/dist/components/AR/ViroARScene.js +10 -6
  41. package/dist/components/Studio/StudioARScene.d.ts +3 -0
  42. package/dist/components/Studio/StudioARScene.js +142 -15
  43. package/dist/components/Studio/StudioSceneNavigator.js +14 -0
  44. package/dist/components/Studio/VRTStudioModule.d.ts +6 -0
  45. package/dist/components/Studio/VRTStudioModule.js +10 -0
  46. package/dist/components/Studio/domain/StudioSounds.d.ts +11 -0
  47. package/dist/components/Studio/domain/StudioSounds.js +69 -0
  48. package/dist/components/Studio/domain/apiRequestHelpers.d.ts +180 -0
  49. package/dist/components/Studio/domain/apiRequestHelpers.js +496 -0
  50. package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +3 -1
  51. package/dist/components/Studio/domain/collisionBindingsRuntime.js +4 -3
  52. package/dist/components/Studio/domain/defaultApiRequestExecutor.d.ts +14 -0
  53. package/dist/components/Studio/domain/defaultApiRequestExecutor.js +58 -0
  54. package/dist/components/Studio/domain/expressionEvaluator.d.ts +108 -0
  55. package/dist/components/Studio/domain/expressionEvaluator.js +520 -0
  56. package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +35 -8
  57. package/dist/components/Studio/domain/sceneNavigationHandler.js +564 -22
  58. package/dist/components/Studio/domain/soundManager.d.ts +52 -0
  59. package/dist/components/Studio/domain/soundManager.js +118 -0
  60. package/dist/components/Studio/domain/utils.d.ts +16 -0
  61. package/dist/components/Studio/domain/utils.js +48 -0
  62. package/dist/components/Studio/domain/variableStore.d.ts +21 -0
  63. package/dist/components/Studio/domain/variableStore.js +65 -0
  64. package/dist/components/Studio/domain/viroNodeFactory.d.ts +4 -9
  65. package/dist/components/Studio/domain/viroNodeFactory.js +84 -25
  66. package/dist/components/Studio/domain/visibilityStore.d.ts +33 -0
  67. package/dist/components/Studio/domain/visibilityStore.js +66 -0
  68. package/dist/components/Studio/index.d.ts +1 -1
  69. package/dist/components/Studio/types.d.ts +150 -1
  70. package/dist/components/Utilities/VRModuleOpenXR.d.ts +19 -0
  71. package/dist/components/Utilities/VRModuleOpenXR.js +16 -0
  72. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  73. package/dist/components/Utilities/ViroVersion.js +1 -1
  74. package/dist/components/ViroAnimatedImage.js +1 -0
  75. package/dist/components/ViroFlexView.js +1 -0
  76. package/dist/components/ViroImage.js +1 -0
  77. package/dist/components/ViroObjectDetector.d.ts +146 -0
  78. package/dist/components/ViroObjectDetector.js +93 -0
  79. package/dist/components/ViroParticleEmitter.js +2 -0
  80. package/dist/components/ViroPolygon.js +1 -0
  81. package/dist/components/ViroQuad.js +1 -0
  82. package/dist/components/ViroSurface.js +1 -0
  83. package/dist/components/ViroVideo.js +1 -0
  84. package/dist/components/ViroXRSceneNavigator.d.ts +8 -0
  85. package/dist/index.d.ts +6 -4
  86. package/dist/index.js +5 -2
  87. package/dist/plugins/withViro.js +2 -15
  88. package/dist/plugins/withViroAndroid.js +18 -5
  89. package/dist/plugins/withViroIos.js +0 -6
  90. package/index.ts +7 -3
  91. package/ios/ViroReact.podspec +25 -6
  92. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
  93. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
  94. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +9 -0
  95. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARScene.h +14 -1
  96. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSceneDelegateiOS.h +8 -0
  97. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROGLTFLoader.h +5 -1
  98. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMonocularDepthEstimator.h +10 -0
  99. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROObjectDetector.h +134 -0
  100. package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
  101. package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
  102. package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
  103. package/ios/dist/ViroRendererVisionOS/ViroKit.podspec +27 -0
  104. package/ios/dist/include/VRTARScene.h +1 -0
  105. package/ios/dist/lib/libViroReact.a +0 -0
  106. 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;