{
  "files": {
    "pre-user-registration.js": "/**\n * NoResponse risk check at account creation, for the Auth0 Pre User\n * Registration trigger.\n *\n * Sends the new account to your NoResponse integration endpoint and blocks it\n * only when NoResponse answers \"deny\". Any failure, slow answer or unexpected\n * reply lets the account through, so NoResponse can never stop your users\n * creating accounts on its own.\n *\n * Secrets (Action editor, Secrets tab):\n *   NORESPONSE_HOOK_URL     the integration URL, https://api.noresponse.org/hooks/<integration id>\n *   NORESPONSE_HOOK_SECRET  the shared secret shown when you created the integration (nri_...)\n *   NORESPONSE_TIMEOUT_MS   optional, how long to wait in milliseconds (default 5000, at most 8000)\n *\n * Runtime: Node 22. No npm dependencies.\n */\n\nconst HOOK_POINT = \"pre_registration\";\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst MAX_TIMEOUT_MS = 8000; // Auth0 can end a run at about 10 seconds.\nconst USER_MESSAGE = \"We could not create your account. Please contact support.\";\n\nexports.onExecutePreUserRegistration = async (event, api) => {\n  const verdict = await askNoResponse(event, HOOK_POINT);\n  if (verdict.deny) {\n    api.access.deny(\"noresponse_risk_block\", USER_MESSAGE);\n  }\n};\n\n// ---- Shared with post-login.js: keep the two copies identical. ----\n\nasync function askNoResponse(event, hookPoint) {\n  const secrets = event.secrets || {};\n  const url = secrets.NORESPONSE_HOOK_URL;\n  const secret = secrets.NORESPONSE_HOOK_SECRET;\n  if (!url || !secret) {\n    return allow(hookPoint, \"not configured\");\n  }\n  if (!safeHookUrl(url)) {\n    return allow(hookPoint, \"hook URL must be https\");\n  }\n  const timeoutMs = timeoutFrom(secrets.NORESPONSE_TIMEOUT_MS);\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n  let response;\n  let text;\n  try {\n    response = await fetch(url, {\n      method: \"POST\",\n      headers: {\n        authorization: `Bearer ${secret}`,\n        \"content-type\": \"application/json\",\n        accept: \"application/json\",\n      },\n      body: JSON.stringify(buildBody(event, hookPoint)),\n      signal: controller.signal,\n      redirect: \"error\", // never resend the body to another host\n    });\n    text = await response.text();\n  } catch (err) {\n    return allow(hookPoint, controller.signal.aborted ? `timeout after ${timeoutMs} ms` : \"network error\");\n  } finally {\n    clearTimeout(timer);\n  }\n  if (response.status !== 200) {\n    return allow(hookPoint, `status ${response.status}`);\n  }\n  let answer;\n  try {\n    answer = JSON.parse(text);\n  } catch (err) {\n    return allow(hookPoint, \"unreadable response\");\n  }\n  if (!answer || typeof answer !== \"object\" || (answer.decision !== \"allow\" && answer.decision !== \"deny\")) {\n    return allow(hookPoint, \"unexpected response\");\n  }\n  const assessment = typeof answer.assessment_id === \"string\" ? answer.assessment_id.slice(0, 64) : \"none\";\n  if (answer.decision === \"deny\") {\n    console.log(`noresponse ${hookPoint}: deny, assessment ${assessment}`);\n    return { deny: true, assessmentId: answer.assessment_id };\n  }\n  return { deny: false, assessmentId: answer.assessment_id };\n}\n\nfunction allow(hookPoint, reason) {\n  console.log(`noresponse ${hookPoint}: allowed without a decision (${reason})`);\n  return { deny: false };\n}\n\n// The secret only ever travels over HTTPS; plain HTTP is accepted for a\n// loopback address so the Action can be tried against a local server.\nfunction safeHookUrl(value) {\n  let parsed;\n  try {\n    parsed = new URL(value);\n  } catch (err) {\n    return false;\n  }\n  if (parsed.protocol === \"https:\") return true;\n  return parsed.protocol === \"http:\" && [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(parsed.hostname);\n}\n\nfunction timeoutFrom(value) {\n  const n = Number(value);\n  if (!Number.isFinite(n) || n <= 0) return DEFAULT_TIMEOUT_MS;\n  return Math.min(Math.floor(n), MAX_TIMEOUT_MS);\n}\n\nfunction buildBody(event, hookPoint) {\n  const user = event.user || {};\n  const request = event.request || {};\n  const geoip = request.geoip || {};\n  const connection = event.connection || {};\n  const body = {\n    hook_point: hookPoint,\n    event_id: eventId(event),\n    user: pick({\n      user_id: user.user_id,\n      email: user.email,\n      email_verified: user.email_verified,\n      username: user.username,\n      phone_number: user.phone_number,\n      phone_verified: user.phone_verified,\n      name: user.name,\n      created_at: user.created_at,\n    }),\n    request: pick({\n      ip: request.ip,\n      user_agent: request.user_agent,\n      geo: pick({ country_code: geoip.countryCode, time_zone: geoip.timeZone }),\n    }),\n    connection: pick({ name: connection.name, strategy: connection.strategy }),\n  };\n  const methods = event.authentication && Array.isArray(event.authentication.methods)\n    ? event.authentication.methods.map((m) => pick({ name: m && m.name, timestamp: m && m.timestamp }))\n    : undefined;\n  const loginsCount = event.stats ? event.stats.logins_count : undefined;\n  if (methods || loginsCount !== undefined) {\n    body.session = pick({\n      authentication: methods ? { methods } : undefined,\n      stats: loginsCount !== undefined ? { logins_count: loginsCount } : undefined,\n    });\n  }\n  return body;\n}\n\nfunction eventId(event) {\n  const transaction = event.transaction || {};\n  if (typeof transaction.id === \"string\" && transaction.id) return transaction.id;\n  if (globalThis.crypto && typeof globalThis.crypto.randomUUID === \"function\") {\n    return globalThis.crypto.randomUUID();\n  }\n  return require(\"node:crypto\").randomUUID();\n}\n\n// Drops absent fields, so the request carries only what Auth0 supplied.\nfunction pick(fields) {\n  const out = {};\n  for (const [key, value] of Object.entries(fields)) {\n    if (value === undefined || value === null) continue;\n    if (typeof value === \"object\" && !Array.isArray(value) && Object.keys(value).length === 0) continue;\n    out[key] = value;\n  }\n  return out;\n}\n",
    "post-login.js": "/**\n * NoResponse sign-in risk check for the Auth0 Login (post-login) trigger.\n *\n * Sends the sign-in to your NoResponse integration endpoint and blocks it only\n * when NoResponse answers \"deny\". Any failure, slow answer or unexpected reply\n * lets the sign-in through, so NoResponse can never lock your users out on its\n * own.\n *\n * Secrets (Action editor, Secrets tab):\n *   NORESPONSE_HOOK_URL     the integration URL, https://api.noresponse.org/hooks/<integration id>\n *   NORESPONSE_HOOK_SECRET  the shared secret shown when you created the integration (nri_...)\n *   NORESPONSE_TIMEOUT_MS   optional, how long to wait in milliseconds (default 5000, at most 8000)\n *\n * Runtime: Node 22. No npm dependencies.\n */\n\nconst HOOK_POINT = \"post_login\";\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst MAX_TIMEOUT_MS = 8000; // Auth0 can end a run at about 10 seconds.\nconst DENY_REASON = \"Sign-in blocked. Please contact support.\";\n\nexports.onExecutePostLogin = async (event, api) => {\n  const verdict = await askNoResponse(event, HOOK_POINT);\n  if (verdict.deny) {\n    api.access.deny(DENY_REASON);\n  }\n};\n\n// ---- Shared with pre-user-registration.js: keep the two copies identical. ----\n\nasync function askNoResponse(event, hookPoint) {\n  const secrets = event.secrets || {};\n  const url = secrets.NORESPONSE_HOOK_URL;\n  const secret = secrets.NORESPONSE_HOOK_SECRET;\n  if (!url || !secret) {\n    return allow(hookPoint, \"not configured\");\n  }\n  if (!safeHookUrl(url)) {\n    return allow(hookPoint, \"hook URL must be https\");\n  }\n  const timeoutMs = timeoutFrom(secrets.NORESPONSE_TIMEOUT_MS);\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n  let response;\n  let text;\n  try {\n    response = await fetch(url, {\n      method: \"POST\",\n      headers: {\n        authorization: `Bearer ${secret}`,\n        \"content-type\": \"application/json\",\n        accept: \"application/json\",\n      },\n      body: JSON.stringify(buildBody(event, hookPoint)),\n      signal: controller.signal,\n      redirect: \"error\", // never resend the body to another host\n    });\n    text = await response.text();\n  } catch (err) {\n    return allow(hookPoint, controller.signal.aborted ? `timeout after ${timeoutMs} ms` : \"network error\");\n  } finally {\n    clearTimeout(timer);\n  }\n  if (response.status !== 200) {\n    return allow(hookPoint, `status ${response.status}`);\n  }\n  let answer;\n  try {\n    answer = JSON.parse(text);\n  } catch (err) {\n    return allow(hookPoint, \"unreadable response\");\n  }\n  if (!answer || typeof answer !== \"object\" || (answer.decision !== \"allow\" && answer.decision !== \"deny\")) {\n    return allow(hookPoint, \"unexpected response\");\n  }\n  const assessment = typeof answer.assessment_id === \"string\" ? answer.assessment_id.slice(0, 64) : \"none\";\n  if (answer.decision === \"deny\") {\n    console.log(`noresponse ${hookPoint}: deny, assessment ${assessment}`);\n    return { deny: true, assessmentId: answer.assessment_id };\n  }\n  return { deny: false, assessmentId: answer.assessment_id };\n}\n\nfunction allow(hookPoint, reason) {\n  console.log(`noresponse ${hookPoint}: allowed without a decision (${reason})`);\n  return { deny: false };\n}\n\n// The secret only ever travels over HTTPS; plain HTTP is accepted for a\n// loopback address so the Action can be tried against a local server.\nfunction safeHookUrl(value) {\n  let parsed;\n  try {\n    parsed = new URL(value);\n  } catch (err) {\n    return false;\n  }\n  if (parsed.protocol === \"https:\") return true;\n  return parsed.protocol === \"http:\" && [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(parsed.hostname);\n}\n\nfunction timeoutFrom(value) {\n  const n = Number(value);\n  if (!Number.isFinite(n) || n <= 0) return DEFAULT_TIMEOUT_MS;\n  return Math.min(Math.floor(n), MAX_TIMEOUT_MS);\n}\n\nfunction buildBody(event, hookPoint) {\n  const user = event.user || {};\n  const request = event.request || {};\n  const geoip = request.geoip || {};\n  const connection = event.connection || {};\n  const body = {\n    hook_point: hookPoint,\n    event_id: eventId(event),\n    user: pick({\n      user_id: user.user_id,\n      email: user.email,\n      email_verified: user.email_verified,\n      username: user.username,\n      phone_number: user.phone_number,\n      phone_verified: user.phone_verified,\n      name: user.name,\n      created_at: user.created_at,\n    }),\n    request: pick({\n      ip: request.ip,\n      user_agent: request.user_agent,\n      geo: pick({ country_code: geoip.countryCode, time_zone: geoip.timeZone }),\n    }),\n    connection: pick({ name: connection.name, strategy: connection.strategy }),\n  };\n  const methods = event.authentication && Array.isArray(event.authentication.methods)\n    ? event.authentication.methods.map((m) => pick({ name: m && m.name, timestamp: m && m.timestamp }))\n    : undefined;\n  const loginsCount = event.stats ? event.stats.logins_count : undefined;\n  if (methods || loginsCount !== undefined) {\n    body.session = pick({\n      authentication: methods ? { methods } : undefined,\n      stats: loginsCount !== undefined ? { logins_count: loginsCount } : undefined,\n    });\n  }\n  return body;\n}\n\nfunction eventId(event) {\n  const transaction = event.transaction || {};\n  if (typeof transaction.id === \"string\" && transaction.id) return transaction.id;\n  if (globalThis.crypto && typeof globalThis.crypto.randomUUID === \"function\") {\n    return globalThis.crypto.randomUUID();\n  }\n  return require(\"node:crypto\").randomUUID();\n}\n\n// Drops absent fields, so the request carries only what Auth0 supplied.\nfunction pick(fields) {\n  const out = {};\n  for (const [key, value] of Object.entries(fields)) {\n    if (value === undefined || value === null) continue;\n    if (typeof value === \"object\" && !Array.isArray(value) && Object.keys(value).length === 0) continue;\n    out[key] = value;\n  }\n  return out;\n}\n",
    "scripts/deploy-to-tenant.mjs": "#!/usr/bin/env node\n// Creates or updates the two NoResponse Actions in an Auth0 tenant, deploys\n// them, binds them to the Pre User Registration and Login flows, and runs\n// Auth0's own Action test on each with a sample event.\n//\n//   AUTH0_DOMAIN=dev-xyz.eu.auth0.com \\\n//   AUTH0_CLIENT_ID=... AUTH0_CLIENT_SECRET=... \\\n//   NORESPONSE_HOOK_URL=https://api.noresponse.org/hooks/<integration id> \\\n//   NORESPONSE_HOOK_SECRET=nri_... \\\n//   node scripts/deploy-to-tenant.mjs [--skip-bind] [--skip-test[=<trigger>,...]] [--auth0-cli]\n//\n// --auth0-cli sends the Management API calls through a logged-in auth0 CLI\n// (auth0 api ...) instead, and then needs only the two NORESPONSE_ values.\n// AUTH0_ACTION_PREFIX puts a prefix on both Action names.\n//\n// The client is a Machine to Machine application authorised for the Auth0\n// Management API with the read:actions, create:actions and update:actions\n// scopes. Nothing secret is printed. Exit code 0 means every step succeeded,\n// 2 means the environment is incomplete and nothing was called.\n\nimport { readFile } from \"node:fs/promises\";\nimport { spawnSync } from \"node:child_process\";\nimport { fileURLToPath } from \"node:url\";\nimport path from \"node:path\";\n\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\nconst ROOT = path.join(HERE, \"..\");\n\n// Auth0's test harness itself reads tenant.id, connection.id and\n// request.method, so the sample events carry them.\nconst SAMPLE_CONTEXT = {\n  tenant: { id: \"action-test\" },\n  client: { client_id: \"action-test\", name: \"Action test\", metadata: {} },\n  connection: { id: \"con_action_test\", name: \"Username-Password-Authentication\", strategy: \"auth0\", metadata: {} },\n  request: {\n    ip: \"203.0.113.45\",\n    hostname: \"action-test.auth0.com\",\n    method: \"POST\",\n    language: \"en\",\n    user_agent: \"Mozilla/5.0\",\n    geoip: { countryCode: \"GB\", timeZone: \"Europe/London\" },\n    body: {},\n    query: {},\n  },\n};\n\nexport const ACTIONS = [\n  {\n    file: \"pre-user-registration.js\",\n    name: \"NoResponse signup risk check\",\n    trigger: \"pre-user-registration\",\n    sampleEvent: {\n      ...SAMPLE_CONTEXT,\n      user: { email: \"auth0-action-test@example.com\" },\n    },\n  },\n  {\n    file: \"post-login.js\",\n    name: \"NoResponse sign-in risk check\",\n    trigger: \"post-login\",\n    sampleEvent: {\n      ...SAMPLE_CONTEXT,\n      transaction: { id: \"auth0-action-test\" },\n      authentication: { methods: [{ name: \"pwd\", timestamp: new Date(0).toISOString() }] },\n      stats: { logins_count: 1 },\n      user: { user_id: \"auth0|action-test\", email: \"auth0-action-test@example.com\", email_verified: true },\n    },\n  },\n];\n\nconst REQUIRED = [\"AUTH0_DOMAIN\", \"AUTH0_CLIENT_ID\", \"AUTH0_CLIENT_SECRET\", \"NORESPONSE_HOOK_URL\", \"NORESPONSE_HOOK_SECRET\"];\nconst REQUIRED_WITH_CLI = [\"NORESPONSE_HOOK_URL\", \"NORESPONSE_HOOK_SECRET\"];\n\nexport async function main(env, args, log = console.log) {\n  const viaCli = args.includes(\"--auth0-cli\");\n  const missing = (viaCli ? REQUIRED_WITH_CLI : REQUIRED).filter((k) => !env[k]);\n  if (missing.length) {\n    log(`missing environment: ${missing.join(\", \")}`);\n    return 2;\n  }\n  // AUTH0_BASE_URL exists so the tests can point the script at a fake tenant.\n  let call;\n  if (viaCli) {\n    call = cliClient();\n  } else {\n    const base = (env.AUTH0_BASE_URL || `https://${env.AUTH0_DOMAIN}`).replace(/\\/$/, \"\");\n    call = managementClient(base, await getToken(base, env));\n  }\n\n  const triggers = (await call(\"GET\", \"/api/v2/actions/triggers\")).triggers || [];\n  let failures = 0;\n  for (const template of ACTIONS) {\n    const spec = { ...template, name: `${env.AUTH0_ACTION_PREFIX || \"\"}${template.name}` };\n    const version = currentVersion(triggers, spec.trigger);\n    const code = await readFile(path.join(ROOT, spec.file), \"utf8\");\n    const action = await upsertAction(call, spec, version, code, env);\n    log(`${spec.trigger}: action ${action.id} saved (trigger version ${version})`);\n    await waitUntilBuilt(call, action.id, env);\n    // Test the built draft first, so a failing one is never deployed.\n    if (!skipsTest(args, spec.trigger)) {\n      const result = await call(\"POST\", `/api/v2/actions/actions/${action.id}/test`, { payload: { event: spec.sampleEvent } });\n      const summary = summariseTest(result);\n      log(`${spec.trigger}: Auth0 test ${summary.ok ? \"passed\" : \"FAILED, not deployed\"}: ${summary.text}`);\n      if (!summary.ok) {\n        failures += 1;\n        continue;\n      }\n    }\n    await call(\"POST\", `/api/v2/actions/actions/${action.id}/deploy`);\n    log(`${spec.trigger}: deployed`);\n    if (!args.includes(\"--skip-bind\")) {\n      const bound = await bind(call, spec, action.id);\n      log(`${spec.trigger}: ${bound ? \"bound to the flow\" : \"already bound to the flow\"}`);\n    }\n  }\n  return failures ? 1 : 0;\n}\n\n// --skip-test skips every test run; --skip-test=post-login skips one trigger.\nfunction skipsTest(args, trigger) {\n  return args.some((a) => a === \"--skip-test\" || (a.startsWith(\"--skip-test=\") && a.slice(12).split(\",\").includes(trigger)));\n}\n\nexport const RATE_LIMIT_ATTEMPTS = 8;\nconst RATE_LIMIT_MAX_WAIT_MS = 15000;\n\n// How long to wait after Auth0 answered 429: Retry-After (seconds) when sent,\n// else until X-RateLimit-Reset (Unix seconds), else exponential from 1 s.\n// Always between 250 ms and 15 s.\nexport function retryDelayMs(headers, attempt, now = Date.now()) {\n  const clamp = (ms) => Math.min(RATE_LIMIT_MAX_WAIT_MS, Math.max(250, Math.ceil(ms)));\n  const after = headers.get(\"retry-after\");\n  if (after !== null && after !== \"\" && Number.isFinite(Number(after))) return clamp(Number(after) * 1000);\n  // Retry-After may also be an HTTP-date.\n  const until = after ? Date.parse(after) : NaN;\n  if (Number.isFinite(until) && until > now) return clamp(until - now);\n  const reset = Number(headers.get(\"x-ratelimit-reset\"));\n  if (Number.isFinite(reset) && reset > 0) return clamp(reset * 1000 - now + 250);\n  return clamp(1000 * 2 ** (attempt - 1));\n}\n\n// fetch that waits out Auth0's 429 (the Management API's global limit hit a\n// staging deploy mid-poll) up to RATE_LIMIT_ATTEMPTS. A 429 means Auth0 did not\n// act, so a retried POST cannot run twice. Returns the last response.\nexport async function fetchRetrying(url, init, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {\n  for (let attempt = 1; ; attempt++) {\n    const res = await fetch(url, init);\n    if (res.status !== 429 || attempt >= RATE_LIMIT_ATTEMPTS) return res;\n    await res.body?.cancel();\n    await sleep(retryDelayMs(res.headers, attempt));\n  }\n}\n\nasync function getToken(base, env) {\n  const res = await fetchRetrying(`${base}/oauth/token`, {\n    method: \"POST\",\n    redirect: \"error\",\n    headers: { \"content-type\": \"application/json\" },\n    body: JSON.stringify({\n      grant_type: \"client_credentials\",\n      client_id: env.AUTH0_CLIENT_ID,\n      client_secret: env.AUTH0_CLIENT_SECRET,\n      audience: `https://${env.AUTH0_DOMAIN}/api/v2/`,\n    }),\n  });\n  if (res.status !== 200) throw new Error(`token request failed with status ${res.status}`);\n  const body = await res.json();\n  if (!body.access_token) throw new Error(\"token response has no access_token\");\n  return body.access_token;\n}\n\nfunction managementClient(base, token) {\n  return async (method, route, body) => {\n    const res = await fetchRetrying(`${base}${route}`, {\n      method,\n      redirect: \"error\",\n      headers: { authorization: `Bearer ${token}`, \"content-type\": \"application/json\" },\n      body: body === undefined ? undefined : JSON.stringify(body),\n    });\n    const text = await res.text();\n    if (res.status < 200 || res.status >= 300) {\n      throw new Error(`${method} ${route.split(\"?\")[0]} failed with status ${res.status}: ${text.slice(0, 300)}`);\n    }\n    return text ? JSON.parse(text) : {};\n  };\n}\n\n// The same calls through `auth0 api`, which holds its own login. The body goes\n// on stdin so secrets never appear in the process list.\nfunction cliClient(bin = process.env.AUTH0_CLI_BIN || \"auth0\") {\n  return async (method, route, body) => cliCall(bin, method, route, body);\n}\n\nfunction cliCall(bin, method, route, body) {\n  const url = new URL(route, \"https://tenant.invalid\");\n  const argv = [\"api\", method.toLowerCase(), url.pathname.replace(/^\\/api\\/v2\\//, \"\")];\n  for (const [k, v] of url.searchParams) argv.push(\"-q\", `${k}=${v}`);\n  if (body !== undefined) argv.push(\"--data\", \"@-\");\n  if (method === \"DELETE\") argv.push(\"--force\");\n  const out = spawnSync(bin, argv, { input: body === undefined ? \"\" : JSON.stringify(body), encoding: \"utf8\" });\n  if (out.status !== 0) {\n    throw new Error(`${method} ${url.pathname} failed through the auth0 CLI: ${(out.stderr || out.stdout || \"\").slice(0, 300)}`);\n  }\n  const text = (out.stdout || \"\").trim();\n  return text ? JSON.parse(text) : {};\n}\n\nexport function currentVersion(triggers, id) {\n  const matches = triggers.filter((t) => t.id === id);\n  const current = matches.find((t) => String(t.status).toUpperCase() === \"CURRENT\") || matches[0];\n  if (!current) throw new Error(`the tenant lists no ${id} trigger`);\n  return current.version;\n}\n\nasync function upsertAction(call, spec, version, code, env) {\n  const secrets = [\n    { name: \"NORESPONSE_HOOK_URL\", value: env.NORESPONSE_HOOK_URL },\n    { name: \"NORESPONSE_HOOK_SECRET\", value: env.NORESPONSE_HOOK_SECRET },\n  ];\n  if (env.NORESPONSE_TIMEOUT_MS) secrets.push({ name: \"NORESPONSE_TIMEOUT_MS\", value: env.NORESPONSE_TIMEOUT_MS });\n  const query = new URLSearchParams({ actionName: spec.name, triggerId: spec.trigger });\n  const found = ((await call(\"GET\", `/api/v2/actions/actions?${query}`)).actions || []).find((a) => a.name === spec.name);\n  const body = { code, runtime: \"node22\", secrets, dependencies: [], supported_triggers: [{ id: spec.trigger, version }] };\n  if (found) return call(\"PATCH\", `/api/v2/actions/actions/${found.id}`, body);\n  return call(\"POST\", \"/api/v2/actions/actions\", { name: spec.name, ...body });\n}\n\nasync function waitUntilBuilt(call, id, env) {\n  const deadline = Date.now() + 120000;\n  const pause = Number(env.AUTH0_POLL_MS || 1500);\n  for (;;) {\n    const action = await call(\"GET\", `/api/v2/actions/actions/${id}`);\n    if (action.status === \"built\") return;\n    if (action.status === \"failed\") throw new Error(`action ${id} failed to build`);\n    if (Date.now() > deadline) throw new Error(`action ${id} still ${action.status} after 120 s`);\n    await new Promise((r) => setTimeout(r, pause));\n  }\n}\n\n// PATCH bindings replaces the whole list, so keep every existing binding.\nasync function bind(call, spec, actionId) {\n  const route = `/api/v2/actions/triggers/${spec.trigger}/bindings`;\n  const existing = (await call(\"GET\", route)).bindings || [];\n  if (existing.some((b) => b.action && b.action.id === actionId)) return false;\n  const bindings = existing.map((b) => ({ ref: { type: \"action_id\", value: b.action.id }, display_name: b.display_name }));\n  bindings.push({ ref: { type: \"action_id\", value: actionId }, display_name: spec.name });\n  await call(\"PATCH\", route, { bindings });\n  return true;\n}\n\n// Auth0 wants the test event under payload.event and answers with\n// payload.output.commands (such as AccessDenied), payload.logs and, when the\n// code threw, payload.error.\nexport function summariseTest(result) {\n  const payload = (result && result.payload) || {};\n  if (payload.error) return { ok: false, text: `${payload.error.name || \"Error\"}: ${payload.error.message || \"\"}` };\n  const commands = ((payload.output && payload.output.commands) || []).map((c) => c.type);\n  const logs = String(payload.logs || \"\").trim().split(\"\\n\").filter(Boolean);\n  return { ok: true, text: `commands [${commands.join(\", \")}], log ${logs.length ? JSON.stringify(logs) : \"empty\"}` };\n}\n\nif (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {\n  main(process.env, process.argv.slice(2)).then(\n    (code) => process.exit(code),\n    (err) => {\n      console.log(`failed: ${err.message}`);\n      process.exit(1);\n    },\n  );\n}\n"
  }
}
