@stardeck-customer-apps/testing 0.20.0 → 0.22.0

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
@@ -348,6 +348,11 @@ var state = globalSingleton("state", () => ({
348
348
  boltIntentCounter: 0,
349
349
  boltCharges: [],
350
350
  boltChargeCounter: 0,
351
+ directCharges: /* @__PURE__ */ new Map(),
352
+ directChargeCounter: 0,
353
+ cardTokens: /* @__PURE__ */ new Map(),
354
+ cardTokenCounter: 0,
355
+ cardTokenizationEnabled: true,
351
356
  uploads: [],
352
357
  uploadCounter: 0,
353
358
  storageFiles: /* @__PURE__ */ new Map(),
@@ -1715,6 +1720,311 @@ function createDirectory() {
1715
1720
  };
1716
1721
  }
1717
1722
 
1723
+ // src/simulator/beam-direct-charges.ts
1724
+ var SIM_BEAM_HOST = "beam.stardeck.test";
1725
+ var SIM_BEAM_MERCHANT_ID = "sim_merchant";
1726
+ var SIM_BEAM_PUBLISHABLE_KEY = "pk_test_sim";
1727
+ var DIRECT_CHARGE_METHODS = /* @__PURE__ */ new Set([
1728
+ "CARD",
1729
+ "CARD_INSTALLMENTS",
1730
+ "CARD_TOKEN",
1731
+ "CARD_TOKEN_INSTALLMENTS",
1732
+ "QR_PROMPT_PAY",
1733
+ "ALIPAY",
1734
+ "ALIPAY_PLUS",
1735
+ "WECHAT_PAY",
1736
+ "TRUE_MONEY",
1737
+ "LINE_PAY",
1738
+ "SHOPEE_PAY",
1739
+ "KPLUS",
1740
+ "MAKE",
1741
+ "SCB_EASY",
1742
+ "KRUNGSRI_APP",
1743
+ "BANGKOK_BANK_APP",
1744
+ "SPAY_LATER"
1745
+ ]);
1746
+ var TEST_CARDS = {
1747
+ "4111111111111111": { outcome: "SUCCEEDED" },
1748
+ "5372074248113841": { outcome: "SUCCEEDED" },
1749
+ "378282246310005": { outcome: "SUCCEEDED" },
1750
+ "4953261730509988": { outcome: "REDIRECT" },
1751
+ "4389783280144506": { outcome: "REDIRECT" },
1752
+ "4943129900084541": { outcome: "FAILED", failureCode: "CH_INSUFFICIENT_FUNDS" },
1753
+ "4111111111000025": { outcome: "FAILED", failureCode: "CH_CARD_DECLINED" },
1754
+ "4111111111000033": { outcome: "FAILED", failureCode: "CH_CARD_RESTRICTED" }
1755
+ };
1756
+ var SIM_QR_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
1757
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1758
+ function invalid(issues) {
1759
+ return json({ error: "Invalid request body", details: issues }, 400);
1760
+ }
1761
+ var DEVICE_TYPES = /* @__PURE__ */ new Set(["ANDROID", "IOS", "WEB"]);
1762
+ function issue(path, message, code = "custom") {
1763
+ return { path, message, code };
1764
+ }
1765
+ function validateChargeFields(body) {
1766
+ const issues = [];
1767
+ const isInt = (v) => typeof v === "number" && Number.isInteger(v);
1768
+ if (!isInt(body.amount) || body.amount < 1) {
1769
+ issues.push(issue(["amount"], "Number must be greater than 0", "too_small"));
1770
+ }
1771
+ if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
1772
+ issues.push(issue(["deploymentId"], "Invalid deployment ID", "invalid_string"));
1773
+ }
1774
+ if (body.referenceId !== void 0 && (typeof body.referenceId !== "string" || body.referenceId.length > 100)) {
1775
+ issues.push(issue(["referenceId"], "Too long", "too_big"));
1776
+ }
1777
+ const expiry = body.expiryDurationInSec;
1778
+ if (expiry !== void 0 && (!isInt(expiry) || expiry < 60 || expiry > 3600)) {
1779
+ issues.push(issue(["expiryDurationInSec"], "Must be an integer from 60 to 3600", "too_big"));
1780
+ }
1781
+ if (body.returnUrl !== void 0 && (typeof body.returnUrl !== "string" || !URL.canParse(body.returnUrl))) {
1782
+ issues.push(issue(["returnUrl"], "Invalid URL", "invalid_format"));
1783
+ }
1784
+ if (body.deviceType !== void 0 && !DEVICE_TYPES.has(String(body.deviceType))) {
1785
+ issues.push(issue(["deviceType"], "Invalid enum value", "invalid_value"));
1786
+ }
1787
+ const key = body.idempotencyKey;
1788
+ if (key !== void 0 && (typeof key !== "string" || key.length < 1 || key.length > 255)) {
1789
+ issues.push(issue(["idempotencyKey"], "Must be 1 to 255 characters", "too_small"));
1790
+ }
1791
+ return issues;
1792
+ }
1793
+ function validateChargeMethod(body) {
1794
+ const method = body.paymentMethod;
1795
+ const type = method?.paymentMethodType;
1796
+ if (typeof type !== "string" || !DIRECT_CHARGE_METHODS.has(type)) {
1797
+ return [issue(["paymentMethod", "paymentMethodType"], "Invalid enum value", "invalid_value")];
1798
+ }
1799
+ const issues = [];
1800
+ const isToken = type === "CARD_TOKEN" || type === "CARD_TOKEN_INSTALLMENTS";
1801
+ const token = method?.cardToken;
1802
+ if (isToken && (typeof token?.cardTokenId !== "string" || token.cardTokenId === "")) {
1803
+ issues.push(
1804
+ issue(["paymentMethod", "cardToken"], `${type} requires paymentMethod.cardToken.cardTokenId`)
1805
+ );
1806
+ }
1807
+ if (token?.securityCode !== void 0 && !/^\d{3,4}$/.test(String(token.securityCode))) {
1808
+ issues.push(
1809
+ issue(
1810
+ ["paymentMethod", "cardToken", "securityCode"],
1811
+ "Invalid security code",
1812
+ "invalid_format"
1813
+ )
1814
+ );
1815
+ }
1816
+ if (type === "CARD_TOKEN_INSTALLMENTS" && ![3, 4, 6, 10].includes(method?.installmentPeriod)) {
1817
+ issues.push(
1818
+ issue(
1819
+ ["paymentMethod", "installmentPeriod"],
1820
+ "CARD_TOKEN_INSTALLMENTS requires paymentMethod.installmentPeriod (3, 4, 6 or 10)"
1821
+ )
1822
+ );
1823
+ }
1824
+ if (type !== "QR_PROMPT_PAY" && !body.returnUrl) {
1825
+ issues.push(
1826
+ issue(
1827
+ ["returnUrl"],
1828
+ `${type} sends the shopper away to pay; returnUrl is required to bring them back`
1829
+ )
1830
+ );
1831
+ }
1832
+ if (type === "CARD" || type === "CARD_INSTALLMENTS") {
1833
+ issues.push(
1834
+ issue(
1835
+ ["paymentMethod", "paymentMethodType"],
1836
+ `${type} takes raw card data; tokenize the card in the browser and charge CARD_TOKEN instead`
1837
+ )
1838
+ );
1839
+ }
1840
+ return issues;
1841
+ }
1842
+ function validateDirectCharge(body) {
1843
+ return [...validateChargeFields(body), ...validateChargeMethod(body)];
1844
+ }
1845
+ function chargeResponse(charge) {
1846
+ const response = {
1847
+ chargeId: charge.id,
1848
+ actionRequired: charge.actionRequired,
1849
+ paymentMethodType: charge.paymentMethodType
1850
+ };
1851
+ if (charge.actionRequired === "ENCODED_IMAGE") {
1852
+ response.encodedImage = {
1853
+ imageBase64Encoded: SIM_QR_PNG,
1854
+ rawData: `00020101021230SIM${charge.id}`,
1855
+ expiry: charge.expiresAt
1856
+ };
1857
+ }
1858
+ if (charge.actionRequired === "REDIRECT") {
1859
+ response.redirect = { redirectUrl: charge.redirectUrl };
1860
+ response.redirectUrl = charge.redirectUrl;
1861
+ }
1862
+ return response;
1863
+ }
1864
+ function nextChargeId() {
1865
+ state.directChargeCounter += 1;
1866
+ return `ch_sim_${state.directChargeCounter}`;
1867
+ }
1868
+ function createCharge(body) {
1869
+ const method = body.paymentMethod;
1870
+ const type = method.paymentMethodType;
1871
+ const id = nextChargeId();
1872
+ const now3 = /* @__PURE__ */ new Date();
1873
+ const expirySec = typeof body.expiryDurationInSec === "number" ? body.expiryDurationInSec : 30 * 60;
1874
+ const returnUrl = typeof body.returnUrl === "string" ? body.returnUrl : null;
1875
+ let actionRequired = "REDIRECT";
1876
+ let status = "PENDING";
1877
+ let failureCode = null;
1878
+ let cardLast4 = null;
1879
+ if (type === "QR_PROMPT_PAY") {
1880
+ actionRequired = "ENCODED_IMAGE";
1881
+ } else if (type === "CARD_TOKEN" || type === "CARD_TOKEN_INSTALLMENTS") {
1882
+ const token = state.cardTokens.get(method.cardToken.cardTokenId);
1883
+ if (!token) {
1884
+ return json({ error: "Card token not found", code: "CARD_TOKEN_NOT_FOUND" }, 404);
1885
+ }
1886
+ cardLast4 = token.last4;
1887
+ const card = TEST_CARDS[token.pan] ?? {
1888
+ outcome: "FAILED",
1889
+ failureCode: "CH_PROCESSING_FAILED"
1890
+ };
1891
+ if (card.outcome === "REDIRECT") {
1892
+ actionRequired = "REDIRECT";
1893
+ } else {
1894
+ actionRequired = "NONE";
1895
+ status = card.outcome;
1896
+ failureCode = card.failureCode ?? null;
1897
+ }
1898
+ }
1899
+ const charge = {
1900
+ id,
1901
+ status,
1902
+ amount: body.amount,
1903
+ currency: String(body.currency ?? "THB"),
1904
+ paymentMethodType: type,
1905
+ referenceId: typeof body.referenceId === "string" ? body.referenceId : null,
1906
+ deviceType: typeof body.deviceType === "string" ? body.deviceType : null,
1907
+ returnUrl,
1908
+ actionRequired,
1909
+ redirectUrl: actionRequired === "REDIRECT" ? `https://${SIM_BEAM_HOST}/pay/${id}${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ""}` : null,
1910
+ expiresAt: new Date(now3.getTime() + expirySec * 1e3).toISOString(),
1911
+ failureCode,
1912
+ cardLast4,
1913
+ idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : null,
1914
+ createdAt: now3.toISOString()
1915
+ };
1916
+ state.directCharges.set(id, charge);
1917
+ return charge;
1918
+ }
1919
+ async function handleDirectChargeRequest(request, pathname) {
1920
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/charges$/.test(pathname)) {
1921
+ const body = await readJsonBody(request);
1922
+ const issues = validateDirectCharge(body);
1923
+ if (issues.length > 0) return invalid(issues);
1924
+ const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : null;
1925
+ if (key) {
1926
+ const existing = [...state.directCharges.values()].find((c) => c.idempotencyKey === key);
1927
+ if (existing) return json(chargeResponse(existing));
1928
+ }
1929
+ const created = createCharge(body);
1930
+ if (created instanceof Response) return created;
1931
+ return json(chargeResponse(created));
1932
+ }
1933
+ const getMatch = pathname.match(/^\/api\/store\/beam\/[^/]+\/charges\/([^/]+)$/);
1934
+ if (request.method === "GET" && getMatch) {
1935
+ const charge = state.directCharges.get(decodeURIComponent(getMatch[1]));
1936
+ if (!charge) return json({ error: "Charge not found", code: "NOT_FOUND" }, 404);
1937
+ return json({ charge: toChargeDetail(charge) });
1938
+ }
1939
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/card-tokenization$/.test(pathname)) {
1940
+ if (!state.cardTokenizationEnabled) {
1941
+ return json(
1942
+ {
1943
+ error: "Add the Beam publishable key to this merchant account to take cards in your own checkout",
1944
+ code: "CARD_TOKENIZATION_NOT_CONFIGURED"
1945
+ },
1946
+ 409
1947
+ );
1948
+ }
1949
+ return json({
1950
+ merchantId: SIM_BEAM_MERCHANT_ID,
1951
+ publishableKey: SIM_BEAM_PUBLISHABLE_KEY,
1952
+ tokenizeUrl: `https://${SIM_BEAM_HOST}/client/v1/card-tokens`,
1953
+ isSandbox: true
1954
+ });
1955
+ }
1956
+ return null;
1957
+ }
1958
+ function toChargeDetail(charge) {
1959
+ return {
1960
+ id: charge.id,
1961
+ chargeId: charge.id,
1962
+ status: charge.status,
1963
+ amount: charge.amount,
1964
+ currency: charge.currency,
1965
+ paymentMethodType: charge.paymentMethodType,
1966
+ ...charge.referenceId !== null ? { referenceId: charge.referenceId } : {},
1967
+ ...charge.failureCode ? { failureCode: charge.failureCode } : {},
1968
+ source: "API",
1969
+ createdAt: charge.createdAt
1970
+ };
1971
+ }
1972
+ async function handleCardTokenRequest(request, url) {
1973
+ if (request.method !== "POST" || url.pathname !== "/client/v1/card-tokens") {
1974
+ return json({ error: `No Beam simulator for ${request.method} ${url.pathname}` }, 404);
1975
+ }
1976
+ const expected = `Basic ${btoa(`${SIM_BEAM_MERCHANT_ID}:${SIM_BEAM_PUBLISHABLE_KEY}`)}`;
1977
+ if (request.headers.get("Authorization") !== expected) {
1978
+ return json(
1979
+ {
1980
+ message: "invalid credentials",
1981
+ error: { errorCode: "INVALID_CREDENTIALS_ERROR", errorMessage: "invalid credentials" }
1982
+ },
1983
+ 401
1984
+ );
1985
+ }
1986
+ const body = await readJsonBody(request);
1987
+ const pan = typeof body.pan === "string" ? body.pan : "";
1988
+ const month = body.expiryMonth;
1989
+ const year = body.expiryYear;
1990
+ if (!/^\d{12,19}$/.test(pan) || typeof month !== "number" || month < 1 || month > 12 || typeof year !== "number" || year < 0 || year > 99) {
1991
+ return json(
1992
+ {
1993
+ message: "inputs are failing validation",
1994
+ error: {
1995
+ errorCode: "API_VALIDATION_ERROR",
1996
+ errorMessage: "inputs are failing validation"
1997
+ }
1998
+ },
1999
+ 400
2000
+ );
2001
+ }
2002
+ state.cardTokenCounter += 1;
2003
+ const token = {
2004
+ id: `ctok_sim_${state.cardTokenCounter}`,
2005
+ pan,
2006
+ last4: pan.slice(-4),
2007
+ expiryMonth: month,
2008
+ expiryYear: year
2009
+ };
2010
+ state.cardTokens.set(token.id, token);
2011
+ return json({ id: token.id });
2012
+ }
2013
+ function chargeWebhookPayload(charge) {
2014
+ return {
2015
+ chargeId: charge.id,
2016
+ merchantId: SIM_BEAM_MERCHANT_ID,
2017
+ ...charge.referenceId !== null ? { referenceId: charge.referenceId } : {},
2018
+ status: charge.status,
2019
+ currency: charge.currency,
2020
+ amount: charge.amount,
2021
+ source: "API",
2022
+ sourceId: charge.id,
2023
+ ...charge.failureCode ? { failureCode: charge.failureCode } : {},
2024
+ paymentMethod: { paymentMethodType: charge.paymentMethodType }
2025
+ };
2026
+ }
2027
+
1718
2028
  // src/simulator/payments.ts
1719
2029
  var import_node_crypto6 = __toESM(require("crypto"));
1720
2030
 
@@ -1829,7 +2139,7 @@ var BEAM_BOLT_PAYMENT_METHODS = [
1829
2139
  "SPAY_LATER"
1830
2140
  ];
1831
2141
  var BEAM_BOLT_INTENT_STATUSES = ["PENDING", "PAID", "FAILED", "CANCELED", "EXPIRED"];
1832
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2142
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1833
2143
  function payErr(error, status = 400, code, details) {
1834
2144
  const body = { error };
1835
2145
  if (code) body.code = code;
@@ -1843,6 +2153,12 @@ function paymentOperationForRequest(request, pathname) {
1843
2153
  if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/payment-links\/[^/]+$/.test(pathname)) {
1844
2154
  return "beam.getPaymentLinkStatus";
1845
2155
  }
2156
+ if (request.method === "POST" && /^\/api\/store\/beam\/[^/]+\/charges$/.test(pathname)) {
2157
+ return "beam.createDirectCharge";
2158
+ }
2159
+ if (request.method === "GET" && /^\/api\/store\/beam\/[^/]+\/charges\/[^/]+$/.test(pathname)) {
2160
+ return "beam.getCharge";
2161
+ }
1846
2162
  if (request.method === "POST" && /^\/api\/store\/(?!beam\/)[^/]+\/checkout$/.test(pathname)) {
1847
2163
  return "stripe.createCheckout";
1848
2164
  }
@@ -2018,6 +2334,8 @@ async function handlePaymentsRequest(request, url) {
2018
2334
  const pathname = url.pathname;
2019
2335
  const injectedFailure = consumePaymentFailure(request, pathname);
2020
2336
  if (injectedFailure) return injectedFailure;
2337
+ const directChargeResponse = await handleDirectChargeRequest(request, pathname);
2338
+ if (directChargeResponse) return directChargeResponse;
2021
2339
  if (/\/billing-portal$/.test(pathname)) {
2022
2340
  return payErr("Not found", 404, "NOT_FOUND");
2023
2341
  }
@@ -2114,7 +2432,7 @@ async function handlePaymentsRequest(request, url) {
2114
2432
  code: "too_small"
2115
2433
  });
2116
2434
  }
2117
- if (typeof body.deploymentId !== "string" || !UUID_RE.test(body.deploymentId)) {
2435
+ if (typeof body.deploymentId !== "string" || !UUID_RE2.test(body.deploymentId)) {
2118
2436
  issues.push({
2119
2437
  path: ["deploymentId"],
2120
2438
  message: "Invalid uuid",
@@ -2301,6 +2619,18 @@ async function handlePaymentsRequest(request, url) {
2301
2619
  }
2302
2620
  return payErr(`No payments simulator for ${request.method} ${pathname}`, 404);
2303
2621
  }
2622
+ function requirePendingDirectCharge(chargeId) {
2623
+ const charge = state.directCharges.get(chargeId);
2624
+ if (!charge) {
2625
+ throw new Error(`[stardeck-testing] Unknown direct charge id: ${chargeId}`);
2626
+ }
2627
+ if (charge.status !== "PENDING") {
2628
+ throw new Error(
2629
+ `[stardeck-testing] Direct charge ${chargeId} is ${charge.status}, not PENDING`
2630
+ );
2631
+ }
2632
+ return charge;
2633
+ }
2304
2634
  function createPayments() {
2305
2635
  return {
2306
2636
  get checkouts() {
@@ -2419,6 +2749,26 @@ function createPayments() {
2419
2749
  if (!intent) return void 0;
2420
2750
  return toBoltIntentRecord(intent);
2421
2751
  },
2752
+ get directCharges() {
2753
+ return [...state.directCharges.values()];
2754
+ },
2755
+ getDirectCharge(chargeId) {
2756
+ return state.directCharges.get(chargeId);
2757
+ },
2758
+ completeDirectCharge(chargeId) {
2759
+ const charge = requirePendingDirectCharge(chargeId);
2760
+ charge.status = "SUCCEEDED";
2761
+ return chargeWebhookPayload(charge);
2762
+ },
2763
+ failDirectCharge(chargeId, failureCode = "CH_PROCESSING_FAILED") {
2764
+ const charge = requirePendingDirectCharge(chargeId);
2765
+ charge.status = "FAILED";
2766
+ charge.failureCode = failureCode;
2767
+ return chargeWebhookPayload(charge);
2768
+ },
2769
+ setCardTokenization(enabled) {
2770
+ state.cardTokenizationEnabled = enabled;
2771
+ },
2422
2772
  clear() {
2423
2773
  state.checkouts = [];
2424
2774
  state.checkoutCounter = 0;
@@ -2433,6 +2783,11 @@ function createPayments() {
2433
2783
  state.boltIntentCounter = 0;
2434
2784
  state.boltCharges = [];
2435
2785
  state.boltChargeCounter = 0;
2786
+ state.directCharges.clear();
2787
+ state.directChargeCounter = 0;
2788
+ state.cardTokens.clear();
2789
+ state.cardTokenCounter = 0;
2790
+ state.cardTokenizationEnabled = true;
2436
2791
  },
2437
2792
  get count() {
2438
2793
  return state.checkouts.length;
@@ -3102,6 +3457,7 @@ function createEdge() {
3102
3457
  }
3103
3458
 
3104
3459
  // src/simulator/starlens.ts
3460
+ var LATEST_VERSION = "v2";
3105
3461
  function starlensFailure(error, status, code) {
3106
3462
  return json({ success: false, error, code }, status);
3107
3463
  }
@@ -3120,11 +3476,18 @@ function nullFlagged(output, issues) {
3120
3476
  }
3121
3477
  return copy;
3122
3478
  }
3123
- async function handleStarlensExtract(request, signedDeploymentId) {
3124
- const body = await readJsonBody(request);
3125
- if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
3126
- return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
3127
- }
3479
+ function isInputShape(input) {
3480
+ if (!input || typeof input !== "object") return false;
3481
+ const i = input;
3482
+ return typeof i.url === "string" || typeof i.data === "string" && i.data.length > 0 && typeof i.mediaType === "string";
3483
+ }
3484
+ var NON_IMAGE_EXTENSION = /\.(pdf|mp3|m4a|wav|ogg|aac|flac|mp4|mov|webm|mkv)(\?|#|$)/i;
3485
+ function isCroppable(input) {
3486
+ if (typeof input.mediaType === "string") return input.mediaType.startsWith("image/");
3487
+ const name = typeof input.filename === "string" ? input.filename : input.url;
3488
+ return !(typeof name === "string" && NON_IMAGE_EXTENSION.test(name));
3489
+ }
3490
+ function validateStarlensBody(body) {
3128
3491
  const inputs = Array.isArray(body.inputs) ? body.inputs : [];
3129
3492
  if (inputs.length === 0) {
3130
3493
  return starlensFailure(
@@ -3133,6 +3496,13 @@ async function handleStarlensExtract(request, signedDeploymentId) {
3133
3496
  "VALIDATION_ERROR"
3134
3497
  );
3135
3498
  }
3499
+ if (!inputs.every(isInputShape)) {
3500
+ return starlensFailure(
3501
+ "Validation failed \u2014 inputs: each input needs a url, or data with a mediaType",
3502
+ 400,
3503
+ "VALIDATION_ERROR"
3504
+ );
3505
+ }
3136
3506
  const schema = body.schema;
3137
3507
  if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
3138
3508
  return starlensFailure(
@@ -3141,8 +3511,35 @@ async function handleStarlensExtract(request, signedDeploymentId) {
3141
3511
  "VALIDATION_ERROR"
3142
3512
  );
3143
3513
  }
3514
+ const version = body.version ?? "latest";
3515
+ if (version !== "v1" && version !== "v2" && version !== "latest") {
3516
+ return starlensFailure("Validation failed \u2014 version: unknown version", 400, "VALIDATION_ERROR");
3517
+ }
3518
+ const effort = body.effort ?? "low";
3519
+ if (effort !== "low" && effort !== "high") {
3520
+ return starlensFailure("Validation failed \u2014 effort: low or high", 400, "VALIDATION_ERROR");
3521
+ }
3522
+ const resolved = version === "latest" ? LATEST_VERSION : version;
3523
+ if (effort === "high" && resolved === "v1") {
3524
+ return starlensFailure(
3525
+ `Starlens v1 does not support effort "high"; use version "v2"`,
3526
+ 400,
3527
+ "unsupported_effort"
3528
+ );
3529
+ }
3530
+ return { version, resolved, effort, inputs, schema };
3531
+ }
3532
+ async function handleStarlensExtract(request, signedDeploymentId) {
3533
+ const body = await readJsonBody(request);
3534
+ const valid = validateStarlensBody(body);
3535
+ if (valid instanceof Response) return valid;
3536
+ const { version, resolved, effort, inputs, schema } = valid;
3537
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
3538
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
3539
+ }
3144
3540
  const call = {
3145
- version: typeof body.version === "string" ? body.version : "latest",
3541
+ version,
3542
+ effort,
3146
3543
  inputs: inputs.map((i) => ({
3147
3544
  name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
3148
3545
  mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
@@ -3164,8 +3561,10 @@ async function handleStarlensExtract(request, signedDeploymentId) {
3164
3561
  return starlensFailure(reply.reject, 422, "unsupported_document");
3165
3562
  }
3166
3563
  const issues = reply.issues ?? [];
3564
+ const ran = reply.effort ?? (effort === "high" && !inputs.some(isCroppable) ? "low" : effort);
3167
3565
  return success({
3168
- version: call.version === "latest" ? "v1" : call.version,
3566
+ version: resolved,
3567
+ effort: ran,
3169
3568
  output: nullFlagged(reply.output, issues),
3170
3569
  issues,
3171
3570
  usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
@@ -3268,6 +3667,9 @@ async function handleSimulatedRequest(request, url) {
3268
3667
  if (isStorageHost) {
3269
3668
  return handleStorageRequest(request, url);
3270
3669
  }
3670
+ if (url.hostname === SIM_BEAM_HOST) {
3671
+ return handleCardTokenRequest(request, url);
3672
+ }
3271
3673
  if (isEmail && request.method === "POST") {
3272
3674
  return handleEmailSend(request);
3273
3675
  }