@reactvision/react-viro 2.57.0 → 2.57.3

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 (101) 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 +2 -4
  7. package/components/AR/ViroARSceneNavigator.tsx +11 -3
  8. package/components/Studio/StudioARScene.tsx +243 -48
  9. package/components/Studio/StudioSceneNavigator.tsx +15 -0
  10. package/components/Studio/VRTStudioModule.ts +10 -0
  11. package/components/Studio/domain/StudioSounds.tsx +46 -0
  12. package/components/Studio/domain/apiRequestHelpers.ts +612 -0
  13. package/components/Studio/domain/collisionBindingsRuntime.ts +17 -2
  14. package/components/Studio/domain/defaultApiRequestExecutor.ts +61 -0
  15. package/components/Studio/domain/expressionEvaluator.ts +603 -0
  16. package/components/Studio/domain/sceneNavigationHandler.ts +770 -20
  17. package/components/Studio/domain/soundManager.ts +141 -0
  18. package/components/Studio/domain/utils.ts +50 -0
  19. package/components/Studio/domain/variableStore.ts +74 -0
  20. package/components/Studio/domain/viroNodeFactory.tsx +175 -44
  21. package/components/Studio/domain/visibilityStore.ts +66 -0
  22. package/components/Studio/index.ts +1 -0
  23. package/components/Studio/types.ts +178 -1
  24. package/components/Utilities/VRModuleOpenXR.ts +35 -0
  25. package/components/Utilities/ViroVersion.ts +1 -1
  26. package/components/ViroAnimatedImage.tsx +1 -0
  27. package/components/ViroFlexView.tsx +1 -0
  28. package/components/ViroImage.tsx +1 -0
  29. package/components/ViroObjectDetector.tsx +13 -5
  30. package/components/ViroParticleEmitter.tsx +2 -0
  31. package/components/ViroPolygon.tsx +2 -1
  32. package/components/ViroQuad.tsx +2 -1
  33. package/components/ViroSurface.tsx +1 -0
  34. package/components/ViroVideo.tsx +1 -0
  35. package/components/ViroXRSceneNavigator.tsx +8 -0
  36. package/dist/components/AR/ViroARPlane.d.ts +1 -1
  37. package/dist/components/AR/ViroARPlane.js +1 -5
  38. package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
  39. package/dist/components/AR/ViroARPlaneSelector.js +10 -5
  40. package/dist/components/AR/ViroARScene.d.ts +1 -1
  41. package/dist/components/AR/ViroARScene.js +2 -5
  42. package/dist/components/AR/ViroARSceneNavigator.d.ts +11 -3
  43. package/dist/components/Studio/StudioARScene.d.ts +3 -0
  44. package/dist/components/Studio/StudioARScene.js +142 -15
  45. package/dist/components/Studio/StudioSceneNavigator.js +14 -0
  46. package/dist/components/Studio/VRTStudioModule.d.ts +6 -0
  47. package/dist/components/Studio/VRTStudioModule.js +10 -0
  48. package/dist/components/Studio/domain/StudioSounds.d.ts +11 -0
  49. package/dist/components/Studio/domain/StudioSounds.js +69 -0
  50. package/dist/components/Studio/domain/apiRequestHelpers.d.ts +180 -0
  51. package/dist/components/Studio/domain/apiRequestHelpers.js +496 -0
  52. package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +3 -1
  53. package/dist/components/Studio/domain/collisionBindingsRuntime.js +4 -3
  54. package/dist/components/Studio/domain/defaultApiRequestExecutor.d.ts +14 -0
  55. package/dist/components/Studio/domain/defaultApiRequestExecutor.js +58 -0
  56. package/dist/components/Studio/domain/expressionEvaluator.d.ts +108 -0
  57. package/dist/components/Studio/domain/expressionEvaluator.js +520 -0
  58. package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +35 -8
  59. package/dist/components/Studio/domain/sceneNavigationHandler.js +564 -22
  60. package/dist/components/Studio/domain/soundManager.d.ts +52 -0
  61. package/dist/components/Studio/domain/soundManager.js +118 -0
  62. package/dist/components/Studio/domain/utils.d.ts +16 -0
  63. package/dist/components/Studio/domain/utils.js +48 -0
  64. package/dist/components/Studio/domain/variableStore.d.ts +21 -0
  65. package/dist/components/Studio/domain/variableStore.js +65 -0
  66. package/dist/components/Studio/domain/viroNodeFactory.d.ts +4 -9
  67. package/dist/components/Studio/domain/viroNodeFactory.js +84 -25
  68. package/dist/components/Studio/domain/visibilityStore.d.ts +33 -0
  69. package/dist/components/Studio/domain/visibilityStore.js +66 -0
  70. package/dist/components/Studio/index.d.ts +1 -1
  71. package/dist/components/Studio/types.d.ts +150 -1
  72. package/dist/components/Utilities/VRModuleOpenXR.d.ts +19 -0
  73. package/dist/components/Utilities/VRModuleOpenXR.js +16 -0
  74. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  75. package/dist/components/Utilities/ViroVersion.js +1 -1
  76. package/dist/components/ViroAnimatedImage.js +1 -0
  77. package/dist/components/ViroFlexView.js +1 -0
  78. package/dist/components/ViroImage.js +1 -0
  79. package/dist/components/ViroObjectDetector.d.ts +13 -5
  80. package/dist/components/ViroObjectDetector.js +13 -5
  81. package/dist/components/ViroParticleEmitter.js +2 -0
  82. package/dist/components/ViroPolygon.js +1 -0
  83. package/dist/components/ViroQuad.js +1 -0
  84. package/dist/components/ViroSurface.js +1 -0
  85. package/dist/components/ViroVideo.js +1 -0
  86. package/dist/components/ViroXRSceneNavigator.d.ts +8 -0
  87. package/dist/index.d.ts +4 -4
  88. package/dist/index.js +2 -1
  89. package/dist/plugins/withViroAndroid.js +18 -0
  90. package/index.ts +4 -3
  91. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
  92. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
  93. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARFrameiOS.h +8 -0
  94. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSessioniOS.h +14 -3
  95. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROFrontCameraProvider.h +34 -0
  96. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROViewAR.h +3 -1
  97. package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
  98. package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
  99. package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
  100. package/ios/dist/lib/libViroReact.a +0 -0
  101. package/package.json +11 -11
@@ -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;
@@ -13,7 +13,7 @@ function pairCooldownKey(pairKey, functionId) {
13
13
  * Cooldown prevents per-frame spam while physics contacts overlap.
14
14
  */
15
15
  function dispatchCollisionBindingActions(params) {
16
- const { selfPlacementId, otherTag, bindingsByPairKey, sceneNavigator, animations, onSceneChange, onAnimationTrigger, cooldownMs = DEFAULT_COOLDOWN_MS, lastFiredRef, } = params;
16
+ const { selfPlacementId, otherTag, bindingsByPairKey, sceneNavigator, animations, onSceneChange, onAnimationTrigger, cooldownMs = DEFAULT_COOLDOWN_MS, lastFiredRef, runtimeCtx, } = params;
17
17
  if (!otherTag)
18
18
  return;
19
19
  const { asset_x_id, asset_y_id } = (0, collisionPairKey_1.canonicalizeCollisionAssetIds)(selfPlacementId, otherTag);
@@ -32,13 +32,13 @@ function dispatchCollisionBindingActions(params) {
32
32
  if (now - last < cooldownMs)
33
33
  continue;
34
34
  map.set(ck, now);
35
- (0, sceneNavigationHandler_1.executeFunctionWithRelations)(fn, sceneNavigator, animations, onAnimationTrigger, 0, onSceneChange);
35
+ (0, sceneNavigationHandler_1.executeFunctionWithRelations)(fn, sceneNavigator, animations, onAnimationTrigger, 0, onSceneChange, runtimeCtx);
36
36
  }
37
37
  }
38
38
  /**
39
39
  * Returns an onCollision handler for a given placement asset ID.
40
40
  */
41
- function createPlacementCollisionHandler(placementId, bindingsByPairKey, sceneNavigator, animations, lastFiredRef, onAnimationTrigger, onSceneChange) {
41
+ function createPlacementCollisionHandler(placementId, bindingsByPairKey, sceneNavigator, animations, lastFiredRef, onAnimationTrigger, onSceneChange, runtimeCtx) {
42
42
  return (viroTag) => {
43
43
  dispatchCollisionBindingActions({
44
44
  selfPlacementId: placementId,
@@ -49,6 +49,7 @@ function createPlacementCollisionHandler(placementId, bindingsByPairKey, sceneNa
49
49
  onSceneChange,
50
50
  onAnimationTrigger,
51
51
  lastFiredRef,
52
+ runtimeCtx,
52
53
  });
53
54
  };
54
55
  }
@@ -0,0 +1,14 @@
1
+ import { StudioApiRequestExecutor } from "../types";
2
+ /**
3
+ * Built-in API_REQUEST transport for production Viro apps: POSTs
4
+ * {function_id, variables} to the scene-api-request egress proxy through the
5
+ * native module, authenticated with the app's RVApiKey (same credential used
6
+ * to fetch scenes). Hosts with their own transport (e.g. StudioGo's JWT
7
+ * controller) override it via runtimeCtx.
8
+ *
9
+ * Always resolves with an outcome — transport-level problems (native module
10
+ * missing, RVApiKey unset, proxy auth/limit errors, malformed envelope)
11
+ * become a NETWORK_ERROR failure so the scene routes to the failure arm
12
+ * instead of crashing.
13
+ */
14
+ export declare const defaultApiRequestExecutor: StudioApiRequestExecutor;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultApiRequestExecutor = void 0;
4
+ const VRTStudioModule_1 = require("../VRTStudioModule");
5
+ /**
6
+ * Built-in API_REQUEST transport for production Viro apps: POSTs
7
+ * {function_id, variables} to the scene-api-request egress proxy through the
8
+ * native module, authenticated with the app's RVApiKey (same credential used
9
+ * to fetch scenes). Hosts with their own transport (e.g. StudioGo's JWT
10
+ * controller) override it via runtimeCtx.
11
+ *
12
+ * Always resolves with an outcome — transport-level problems (native module
13
+ * missing, RVApiKey unset, proxy auth/limit errors, malformed envelope)
14
+ * become a NETWORK_ERROR failure so the scene routes to the failure arm
15
+ * instead of crashing.
16
+ */
17
+ const defaultApiRequestExecutor = async (functionId, variables) => {
18
+ const result = await VRTStudioModule_1.VRTStudioModule.rvStudioApiRequest(JSON.stringify({ function_id: functionId, variables }));
19
+ if (!result.success || typeof result.data !== "string") {
20
+ return failureOutcome(extractErrorMessage(result.error));
21
+ }
22
+ try {
23
+ const envelope = JSON.parse(result.data);
24
+ return {
25
+ ok: envelope.ok === true,
26
+ status: typeof envelope.status === "number" ? envelope.status : null,
27
+ body: envelope.body,
28
+ error_code: envelope.error_code ?? null,
29
+ error_message: envelope.error_message ?? null,
30
+ };
31
+ }
32
+ catch {
33
+ return failureOutcome("Malformed scene-api-request response");
34
+ }
35
+ };
36
+ exports.defaultApiRequestExecutor = defaultApiRequestExecutor;
37
+ function failureOutcome(message) {
38
+ return {
39
+ ok: false,
40
+ status: null,
41
+ error_code: "NETWORK_ERROR",
42
+ error_message: message,
43
+ };
44
+ }
45
+ /** Proxy auth/contract errors arrive as {"error":{"code","message"}} JSON. */
46
+ function extractErrorMessage(error) {
47
+ if (!error)
48
+ return "Request failed";
49
+ try {
50
+ const parsed = JSON.parse(error);
51
+ if (typeof parsed.error?.message === "string")
52
+ return parsed.error.message;
53
+ }
54
+ catch {
55
+ // not JSON — use the raw transport error
56
+ }
57
+ return error;
58
+ }