@entros/pulse-sdk 1.2.0 → 1.4.1

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/dist/index.js CHANGED
@@ -155,14 +155,25 @@ async function captureAudio(options = {}) {
155
155
  autoGainControl: false
156
156
  }
157
157
  });
158
- const ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
159
- await ctx.resume();
160
- const capturedSampleRate = ctx.sampleRate;
161
- const source = ctx.createMediaStreamSource(stream);
158
+ let ctx;
159
+ let source;
160
+ let capturedSampleRate;
161
+ try {
162
+ ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
163
+ await ctx.resume();
164
+ capturedSampleRate = ctx.sampleRate;
165
+ source = ctx.createMediaStreamSource(stream);
166
+ } catch (err) {
167
+ if (!preAcquiredStream) {
168
+ stream.getTracks().forEach((t) => t.stop());
169
+ }
170
+ throw err;
171
+ }
162
172
  const chunks = [];
163
173
  const startTime = performance.now();
164
174
  return new Promise((resolve) => {
165
175
  let stopped = false;
176
+ let abortTimer = null;
166
177
  const bufferSize = 4096;
167
178
  const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
168
179
  processor.onaudioprocess = (e) => {
@@ -180,6 +191,7 @@ async function captureAudio(options = {}) {
180
191
  if (stopped) return;
181
192
  stopped = true;
182
193
  clearTimeout(maxTimer);
194
+ if (abortTimer !== null) clearTimeout(abortTimer);
183
195
  processor.disconnect();
184
196
  source.disconnect();
185
197
  stream.getTracks().forEach((t) => t.stop());
@@ -201,14 +213,14 @@ async function captureAudio(options = {}) {
201
213
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
202
214
  if (signal) {
203
215
  if (signal.aborted) {
204
- setTimeout(stopCapture, minDurationMs);
216
+ abortTimer = setTimeout(stopCapture, minDurationMs);
205
217
  } else {
206
218
  signal.addEventListener(
207
219
  "abort",
208
220
  () => {
209
221
  const elapsed = performance.now() - startTime;
210
222
  const remaining = Math.max(0, minDurationMs - elapsed);
211
- setTimeout(stopCapture, remaining);
223
+ abortTimer = setTimeout(stopCapture, remaining);
212
224
  },
213
225
  { once: true }
214
226
  );
@@ -260,6 +272,7 @@ async function captureMotion(options = {}) {
260
272
  const startTime = performance.now();
261
273
  return new Promise((resolve) => {
262
274
  let stopped = false;
275
+ let abortTimer = null;
263
276
  const handler = (e) => {
264
277
  samples.push({
265
278
  timestamp: performance.now(),
@@ -275,6 +288,7 @@ async function captureMotion(options = {}) {
275
288
  if (stopped) return;
276
289
  stopped = true;
277
290
  clearTimeout(maxTimer);
291
+ if (abortTimer !== null) clearTimeout(abortTimer);
278
292
  window.removeEventListener("devicemotion", handler);
279
293
  resolve(samples);
280
294
  }
@@ -282,14 +296,14 @@ async function captureMotion(options = {}) {
282
296
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
283
297
  if (signal) {
284
298
  if (signal.aborted) {
285
- setTimeout(stopCapture, minDurationMs);
299
+ abortTimer = setTimeout(stopCapture, minDurationMs);
286
300
  } else {
287
301
  signal.addEventListener(
288
302
  "abort",
289
303
  () => {
290
304
  const elapsed = performance.now() - startTime;
291
305
  const remaining = Math.max(0, minDurationMs - elapsed);
292
- setTimeout(stopCapture, remaining);
306
+ abortTimer = setTimeout(stopCapture, remaining);
293
307
  },
294
308
  { once: true }
295
309
  );
@@ -309,6 +323,7 @@ function captureTouch(element, options = {}) {
309
323
  const startTime = performance.now();
310
324
  return new Promise((resolve) => {
311
325
  let stopped = false;
326
+ let abortTimer = null;
312
327
  const handler = (e) => {
313
328
  samples.push({
314
329
  timestamp: performance.now(),
@@ -323,6 +338,7 @@ function captureTouch(element, options = {}) {
323
338
  if (stopped) return;
324
339
  stopped = true;
325
340
  clearTimeout(maxTimer);
341
+ if (abortTimer !== null) clearTimeout(abortTimer);
326
342
  element.removeEventListener("pointermove", handler);
327
343
  element.removeEventListener("pointerdown", handler);
328
344
  sdkLog(`[Entros SDK] Touch capture stopped: ${samples.length} samples collected`);
@@ -334,14 +350,14 @@ function captureTouch(element, options = {}) {
334
350
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
335
351
  if (signal) {
336
352
  if (signal.aborted) {
337
- setTimeout(stopCapture, minDurationMs);
353
+ abortTimer = setTimeout(stopCapture, minDurationMs);
338
354
  } else {
339
355
  signal.addEventListener(
340
356
  "abort",
341
357
  () => {
342
358
  const elapsed = performance.now() - startTime;
343
359
  const remaining = Math.max(0, minDurationMs - elapsed);
344
- setTimeout(stopCapture, remaining);
360
+ abortTimer = setTimeout(stopCapture, remaining);
345
361
  },
346
362
  { once: true }
347
363
  );
@@ -1348,6 +1364,45 @@ async function generateSolanaProof(current, previous, wasmPath, zkeyPath, thresh
1348
1364
  return serializeProof(proof, publicSignals);
1349
1365
  }
1350
1366
 
1367
+ // src/submit/receipt.ts
1368
+ var PUBKEY_BYTES = 32;
1369
+ var SIGNATURE_BYTES = 64;
1370
+ var MESSAGE_BYTES = 72;
1371
+ function bytesToHex(bytes) {
1372
+ let out = "";
1373
+ for (let i = 0; i < bytes.length; i += 1) {
1374
+ out += (bytes[i] ?? 0).toString(16).padStart(2, "0");
1375
+ }
1376
+ return out;
1377
+ }
1378
+ function hexToBytes(hex, expectedLen) {
1379
+ const trimmed = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
1380
+ if (trimmed.length !== expectedLen * 2) return null;
1381
+ if (!/^[0-9a-f]+$/.test(trimmed)) return null;
1382
+ const out = new Uint8Array(expectedLen);
1383
+ for (let i = 0; i < expectedLen; i += 1) {
1384
+ out[i] = parseInt(trimmed.substr(i * 2, 2), 16);
1385
+ }
1386
+ return out;
1387
+ }
1388
+ function decodeSignedReceipt(receipt) {
1389
+ const publicKey = hexToBytes(receipt.validator_pubkey_hex, PUBKEY_BYTES);
1390
+ const signature = hexToBytes(receipt.signature_hex, SIGNATURE_BYTES);
1391
+ const message = hexToBytes(receipt.message_hex, MESSAGE_BYTES);
1392
+ if (!publicKey || !signature || !message) return null;
1393
+ return { publicKey, signature, message };
1394
+ }
1395
+ async function buildEd25519ReceiptIx(receipt) {
1396
+ const decoded = decodeSignedReceipt(receipt);
1397
+ if (!decoded) return null;
1398
+ const { Ed25519Program } = await import("@solana/web3.js");
1399
+ return Ed25519Program.createInstructionWithPublicKey({
1400
+ publicKey: decoded.publicKey,
1401
+ message: decoded.message,
1402
+ signature: decoded.signature
1403
+ });
1404
+ }
1405
+
1351
1406
  // src/submit/wallet.ts
1352
1407
  async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerApiKey, serverNonce) {
1353
1408
  try {
@@ -1389,14 +1444,22 @@ async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerA
1389
1444
  return attestData.attestation_tx;
1390
1445
  }
1391
1446
  }
1392
- } catch {
1447
+ } catch (err) {
1448
+ const msg = err instanceof Error ? err.message : String(err);
1449
+ sdkWarn(`[Entros SDK] SAS attestation request failed: ${msg}`);
1393
1450
  }
1394
1451
  return void 0;
1395
1452
  }
1396
1453
  async function submitViaWallet(proof, commitment, options) {
1397
1454
  try {
1398
1455
  const anchor = await import("@coral-xyz/anchor");
1399
- const { PublicKey, SystemProgram, Transaction, ComputeBudgetProgram } = await import("@solana/web3.js");
1456
+ const {
1457
+ PublicKey,
1458
+ SystemProgram,
1459
+ Transaction,
1460
+ ComputeBudgetProgram,
1461
+ SYSVAR_INSTRUCTIONS_PUBKEY
1462
+ } = await import("@solana/web3.js");
1400
1463
  const provider = new anchor.AnchorProvider(
1401
1464
  options.connection,
1402
1465
  options.wallet,
@@ -1565,7 +1628,7 @@ async function submitViaWallet(proof, commitment, options) {
1565
1628
  false,
1566
1629
  TOKEN_2022_PROGRAM_ID
1567
1630
  );
1568
- await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
1631
+ const mintAnchorIx = await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
1569
1632
  user: provider.wallet.publicKey,
1570
1633
  identityState: identityPda,
1571
1634
  mint: mintPda,
@@ -1577,8 +1640,36 @@ async function submitViaWallet(proof, commitment, options) {
1577
1640
  tokenProgram: TOKEN_2022_PROGRAM_ID,
1578
1641
  systemProgram: SystemProgram.programId,
1579
1642
  protocolConfig: protocolConfigPda,
1580
- treasury: treasuryPda
1581
- }).rpc();
1643
+ treasury: treasuryPda,
1644
+ instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY
1645
+ }).instruction();
1646
+ let ed25519Ix = null;
1647
+ if (options.signedReceipt) {
1648
+ ed25519Ix = await buildEd25519ReceiptIx(options.signedReceipt);
1649
+ if (!ed25519Ix) {
1650
+ return {
1651
+ success: false,
1652
+ error: "Validator returned a signed receipt that failed to decode (malformed hex or wrong byte length). Refusing to mint without a valid binding. The validator service may be misconfigured \u2014 check the validation-service logs."
1653
+ };
1654
+ }
1655
+ sdkLog(
1656
+ "[Entros SDK] Bundling validator-signed mint receipt before mint_anchor"
1657
+ );
1658
+ } else {
1659
+ sdkLog(
1660
+ "[Entros SDK] No validator receipt available; minting without binding (on-chain check is log-only today)"
1661
+ );
1662
+ }
1663
+ const tx = new Transaction();
1664
+ tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 2e5 }));
1665
+ if (ed25519Ix) tx.add(ed25519Ix);
1666
+ tx.add(mintAnchorIx);
1667
+ tx.feePayer = provider.wallet.publicKey;
1668
+ tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
1669
+ txSig = await options.wallet.sendTransaction(tx, options.connection, {
1670
+ skipPreflight: true
1671
+ });
1672
+ await options.connection.confirmTransaction(txSig, "confirmed");
1582
1673
  }
1583
1674
  const attestationTx = options.relayerUrl ? await requestSasAttestation(
1584
1675
  options.wallet,
@@ -1820,11 +1911,24 @@ function fromBase64(b64) {
1820
1911
  var STORAGE_KEY = "entros-protocol-verification-data";
1821
1912
  var ENCRYPTED_VERSION = 2;
1822
1913
  var inMemoryStore = null;
1914
+ var privacyFallbackCallback = null;
1915
+ function setPrivacyFallback(cb) {
1916
+ privacyFallbackCallback = cb ?? null;
1917
+ }
1823
1918
  function isEncryptedEnvelope(obj) {
1824
- return typeof obj === "object" && obj !== null && obj.v === ENCRYPTED_VERSION && typeof obj.iv === "string" && typeof obj.ct === "string";
1919
+ if (typeof obj !== "object" || obj === null) return false;
1920
+ const o = obj;
1921
+ return o.v === ENCRYPTED_VERSION && typeof o.iv === "string" && o.iv.length > 0 && typeof o.ct === "string" && o.ct.length > 0;
1825
1922
  }
1826
1923
  function isPlaintextData(obj) {
1827
- return typeof obj === "object" && obj !== null && Array.isArray(obj.fingerprint);
1924
+ if (typeof obj !== "object" || obj === null) return false;
1925
+ const o = obj;
1926
+ if (!Array.isArray(o.fingerprint)) return false;
1927
+ if (!o.fingerprint.every((bit) => typeof bit === "number")) return false;
1928
+ if (typeof o.salt !== "string" || o.salt.length === 0) return false;
1929
+ if (typeof o.commitment !== "string" || o.commitment.length === 0) return false;
1930
+ if (typeof o.timestamp !== "number" || !Number.isFinite(o.timestamp)) return false;
1931
+ return true;
1828
1932
  }
1829
1933
  async function fetchIdentityState(walletPubkey, connection) {
1830
1934
  try {
@@ -1863,14 +1967,34 @@ async function fetchIdentityState(walletPubkey, connection) {
1863
1967
  async function storeVerificationData(data) {
1864
1968
  try {
1865
1969
  if (!hasCryptoSupport()) {
1866
- sdkWarn("[Entros SDK] Crypto unavailable \u2014 verification data stored unencrypted");
1867
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1970
+ const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
1971
+ if (allowPlaintext) {
1972
+ sdkWarn(
1973
+ "[Entros SDK] Crypto unavailable; user-approved plaintext storage"
1974
+ );
1975
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1976
+ } else {
1977
+ sdkWarn(
1978
+ "[Entros SDK] Crypto unavailable and no privacy-fallback approval \u2014 using in-memory storage (data lost on reload)"
1979
+ );
1980
+ inMemoryStore = data;
1981
+ }
1868
1982
  return;
1869
1983
  }
1870
1984
  const key = await getOrCreateEncryptionKey();
1871
1985
  if (!key) {
1872
- sdkWarn("[Entros SDK] Encryption key unavailable \u2014 storing unencrypted");
1873
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1986
+ const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
1987
+ if (allowPlaintext) {
1988
+ sdkWarn(
1989
+ "[Entros SDK] Encryption key unavailable; user-approved plaintext storage"
1990
+ );
1991
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1992
+ } else {
1993
+ sdkWarn(
1994
+ "[Entros SDK] Encryption key unavailable and no privacy-fallback approval \u2014 using in-memory storage"
1995
+ );
1996
+ inMemoryStore = data;
1997
+ }
1874
1998
  return;
1875
1999
  }
1876
2000
  const { iv, ct } = await encrypt(JSON.stringify(data), key);
@@ -1956,6 +2080,9 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1956
2080
  sdkLog(
1957
2081
  `[Entros SDK] Feature vector: ${features.length} dimensions, ${nonZero} non-zero. Audio[0..43]: ${features.slice(0, 44).filter((v) => v !== 0).length} non-zero. Motion/Mouse[44..97]: ${features.slice(44, 98).filter((v) => v !== 0).length} non-zero. Touch[98..133]: ${features.slice(98, 134).filter((v) => v !== 0).length} non-zero.`
1958
2082
  );
2083
+ const fingerprint = simhash(normalizedFeatures);
2084
+ const tbh = await generateTBH(fingerprint);
2085
+ let signedReceipt;
1959
2086
  onProgress?.("Validating...");
1960
2087
  if (config.relayerUrl && walletAddress) {
1961
2088
  try {
@@ -1967,6 +2094,7 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1967
2094
  }
1968
2095
  const audioSamplesB64 = sensorData.audio?.samples ? encodeAudioAsBase64(sensorData.audio.samples) : void 0;
1969
2096
  const audioSampleRateHz = sensorData.audio?.sampleRate;
2097
+ const commitmentNewHex = bytesToHex(tbh.commitmentBytes);
1970
2098
  const validateController = new AbortController();
1971
2099
  const validateTimer = setTimeout(() => validateController.abort(), 15e3);
1972
2100
  const validateResponse = await fetch(validateUrl, {
@@ -1978,32 +2106,43 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1978
2106
  accel_magnitude: accelMagnitude,
1979
2107
  wallet_id: walletAddress,
1980
2108
  audio_samples_b64: audioSamplesB64,
1981
- audio_sample_rate_hz: audioSampleRateHz
2109
+ audio_sample_rate_hz: audioSampleRateHz,
2110
+ commitment_new_hex: commitmentNewHex
1982
2111
  }),
1983
2112
  signal: validateController.signal
1984
2113
  });
1985
2114
  clearTimeout(validateTimer);
1986
2115
  if (!validateResponse.ok) {
1987
2116
  const errorBody = await validateResponse.json().catch(() => ({}));
1988
- const body = errorBody;
1989
- const reason = typeof body.reason === "string" ? body.reason : void 0;
1990
- sdkWarn(
1991
- `[Entros SDK] Feature validation rejected by server${reason ? ` (reason: ${reason})` : ""}`
1992
- );
2117
+ sdkWarn("[Entros SDK] Feature validation rejected by server");
1993
2118
  return {
1994
2119
  ok: false,
1995
- error: body.error || "Feature validation failed",
1996
- reason
2120
+ error: errorBody.error || "Feature validation failed",
2121
+ reason: errorBody.reason
1997
2122
  };
1998
2123
  }
2124
+ try {
2125
+ const successBody = await validateResponse.json();
2126
+ if (successBody.signed_receipt) {
2127
+ signedReceipt = successBody.signed_receipt;
2128
+ }
2129
+ } catch (err) {
2130
+ const msg = err instanceof Error ? err.message : String(err);
2131
+ sdkWarn(
2132
+ `[Entros SDK] /validate-features returned 200 but body was not parseable JSON; proceeding without receipt: ${msg}`
2133
+ );
2134
+ }
1999
2135
  } catch (err) {
2000
2136
  const msg = err instanceof Error ? err.message : String(err);
2001
- sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}, proceeding without server validation`);
2137
+ sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}`);
2138
+ return {
2139
+ ok: false,
2140
+ error: "Validation service unreachable. Please check your connection and try again.",
2141
+ reason: "validation_unavailable"
2142
+ };
2002
2143
  }
2003
2144
  }
2004
- const fingerprint = simhash(normalizedFeatures);
2005
- const tbh = await generateTBH(fingerprint);
2006
- return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh };
2145
+ return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh, signedReceipt };
2007
2146
  }
2008
2147
  async function processSensorData(sensorData, config, wallet, connection, onProgress) {
2009
2148
  const audioSamples = sensorData.audio?.samples.length ?? 0;
@@ -2074,7 +2213,7 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2074
2213
  reason: extraction.reason
2075
2214
  };
2076
2215
  }
2077
- const { fingerprint, tbh, features } = extraction;
2216
+ const { fingerprint, tbh, features, signedReceipt } = extraction;
2078
2217
  let isFirstVerification;
2079
2218
  const previousData = await loadVerificationData();
2080
2219
  if (wallet && connection) {
@@ -2164,7 +2303,14 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2164
2303
  submission = await submitViaWallet(
2165
2304
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
2166
2305
  tbh.commitmentBytes,
2167
- { wallet, connection, isFirstVerification: true, relayerUrl: config.relayerUrl, relayerApiKey: config.relayerApiKey }
2306
+ {
2307
+ wallet,
2308
+ connection,
2309
+ isFirstVerification: true,
2310
+ relayerUrl: config.relayerUrl,
2311
+ relayerApiKey: config.relayerApiKey,
2312
+ signedReceipt
2313
+ }
2168
2314
  );
2169
2315
  } else {
2170
2316
  submission = await submitViaWallet(solanaProof, tbh.commitmentBytes, {
@@ -2466,70 +2612,6 @@ var PulseSession = class {
2466
2612
  this.motionStageState = "captured";
2467
2613
  this.touchStageState = "captured";
2468
2614
  }
2469
- /**
2470
- * @internal
2471
- *
2472
- * Run the validation step of the verify pipeline only: feature extraction
2473
- * + `/validate-features` POST. Returns the validation outcome without ever
2474
- * touching the on-chain submission path. Mirrors the production user
2475
- * flow's pre-payment gate — the validation server runs without requiring
2476
- * the wallet to have SOL, just like a real user gets a validation result
2477
- * before being prompted to sign the on-chain mint.
2478
- *
2479
- * Note: this is a strict subset of `complete()`. It skips the data-quality
2480
- * gates and re-verification check that `processSensorData` performs. The
2481
- * validation server still runs its full pipeline (Tier 1 + Tier 2 +
2482
- * phrase binding); only the client-side pre-flight checks differ.
2483
- *
2484
- * Use case: red team campaigns measuring server-side validation at scale
2485
- * without per-attempt SOL funding. Build-time gated identically to
2486
- * `__injectSensorData`; throws in production builds.
2487
- */
2488
- async __validateOnly(walletAddress) {
2489
- if (true) {
2490
- throw new Error(
2491
- "PulseSession.__validateOnly is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
2492
- );
2493
- }
2494
- if (typeof walletAddress !== "string" || walletAddress.length === 0) {
2495
- throw new Error(
2496
- "__validateOnly requires a non-empty walletAddress string (used as wallet_id in the /validate-features payload)."
2497
- );
2498
- }
2499
- const active = [];
2500
- if (this.audioStageState === "capturing") active.push("audio");
2501
- if (this.motionStageState === "capturing") active.push("motion");
2502
- if (this.touchStageState === "capturing") active.push("touch");
2503
- if (active.length > 0) {
2504
- throw new Error(
2505
- `Cannot validate: stages still capturing: ${active.join(", ")}`
2506
- );
2507
- }
2508
- if (!this.audioData || this.motionData.length === 0 || this.touchData.length === 0) {
2509
- throw new Error(
2510
- "__validateOnly requires sensor data first \u2014 call __injectSensorData() before this."
2511
- );
2512
- }
2513
- const sensorData = {
2514
- audio: this.audioData,
2515
- motion: this.motionData,
2516
- touch: this.touchData,
2517
- modalities: {
2518
- audio: true,
2519
- motion: true,
2520
- touch: true
2521
- }
2522
- };
2523
- const extraction = await extractFingerprintAndValidate(
2524
- sensorData,
2525
- this.config,
2526
- walletAddress
2527
- );
2528
- if (!extraction.ok) {
2529
- return { validated: false, error: extraction.error, reason: extraction.reason };
2530
- }
2531
- return { validated: true };
2532
- }
2533
2615
  // --- Complete ---
2534
2616
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
2535
2617
  async complete(wallet, connection, onProgress) {
@@ -2604,6 +2686,7 @@ var PulseSDK = class {
2604
2686
  ...config
2605
2687
  };
2606
2688
  setDebug(config.debug ?? false);
2689
+ setPrivacyFallback(config.onPrivacyFallback);
2607
2690
  }
2608
2691
  /**
2609
2692
  * Create a staged capture session for event-driven control.