Reference

RMA decision ladder — functional and data design spec (hypothetical)

Status: hypothetical. This is a design for discussion, using the HyperX support knowledge base and its chatbot as the worked example. It does not describe a live system. Statements about the current HyperX estate are marked Observed 2026-10-04 and were checked on that date; everything else is proposed.

The companion diagram shows the same model visually: the four type structures, the mapping onto Xano and the Cloudflare Worker, the functions, and the ladder.

One knowledge base, four type structures, one backend: a HyperX support article as a Webflow Collection item, a Shopify metaobject, a Rust struct and a Jekyll page, mapped onto Xano and the Cloudflare Worker, with the Worker functions and the RMA decision ladder

Open the diagram full size.

Try the chatbot: live preview on the hx-stage demo store. This is the same <crm-chat> the knowledge-base search page embeds; the ladder in this spec is the hypothetical design behind it, not a description of what the preview does today.

The Mermaid diagrams in sections 5, 8 and 9 render on GitHub; the book shows their source. The principles come from the CRM Sync book: The AI ladder for the escalation tiers, QA and Release Gating for the gate, and Secure frontend, AI-safe backend for publishing.

1. Scope

In scope: a customer asks the support chatbot about a product, may start a return (RMA), and is handed to a person when a rule says so. Also in scope: what the chatbot may link to (firmware), what code may run around it (browser code), and how changes to it reach production (deploy).

Out of scope: the refund payment itself, warehouse receiving, and the Shopify order lifecycle. The ladder stops at the decision; money moves in the system that owns it.

2. Principals

PrincipalIdentified byMayMay never
VisitorNothingRead public articles; ask the chatbotSee any order or case
CustomerSigned-in sessionRead their own orders and cases; open an RMASee another customer's rows
Chat agentA mandate from the customer's session: scoped, expiringAct at rungs 0–2 for that customerApprove money; hold a refund key
Support personStaff identity with rma:refund:approveDecide rung 3Approve their own case
ComplianceStaff identityHandle tier 3; edit internal notesChange a case outcome
DeployerCI token, one per environmentDeploy a build that passed the gateDeploy unreviewed code
ReviewerA person who did not author the changeApprove a code changeApprove their own change

3. Data design

Xano is the record; the Cloudflare Worker is the only way out. Every field has one writer.

kb_article:
  id: int
  question: text            # required · writer: support editor
  slug: text                # required, unique
  answer: html              # sanitized on the way out
  category_id: ref category # table reference — the Worker checks it resolves
  tag_ids: [ref tag]        # family + product, e.g. cloud-family, cloud-ii-core-wireless
  pdf_key: r2_key?          # manual or quick-start guide
  escalation: enum [none, tier1, tier2, tier3]   # routing carried as content
  is_private: bool          # excluded from every public query

firmware_release:
  id: int
  product_tag_id: ref tag
  version: text
  image_key: r2_key
  sha256: hex               # of the image bytes
  signature: base64         # by the firmware signing key, over sha256
  signer_key_id: text       # which public key verifies it
  sbom_key: r2_key          # CycloneDX or SPDX
  status: enum [draft, attested, revoked]
  attested_at: timestamp?

rma_case:
  id: int
  ref: text                 # e.g. RMA-2026-0187, given to the customer
  customer_id: ref customer # the owner; every read is id AND customer_id
  order_id: ref order       # read-only mirror of the Shopify order
  serial: text
  state: enum [requested, awaiting_review, approved, denied, safety_hold, closed]
  conditions_hit: [condition_id]
  opened_by: enum [agent, person]

escalation:
  id: int
  case_id: ref rma_case?
  tier: enum [tier1, tier2, tier3]
  question: text
  kb_hits: [ref kb_article] # kept even when wrong: that's how gaps are found
  answer: text?
  pair_status: enum [en_only, ko_only, paired]   # derived by the server, never sent
  # FORBIDDEN fields: name, email, phone, address — the record links to the customer by ID only

ledger_entry:
  id: int
  subject: text             # who acted: customer, agent mandate, person, deployer
  cap: text                 # the capability used
  action: text
  target: text
  decided_by: enum [rule, person]
  evidence_sha256: hex?     # build, firmware image or script hash, when there is one
  at: timestamp

4. Rules as data

The ladder and its conditions are data the Worker reads, not code paths. Changing a threshold is a reviewed data change, not a redeploy.

rungs:
  - id: 0
    name: Knowledge base
    who: visitor
    cap: none
    may: [answer_from_public_articles, link_attested_firmware, link_pdf]
    may_not: [read_private_article, read_order]
    tier: none
    up_when: [no_article_answers, customer_asks_to_return]
  - id: 1
    name: Look up
    who: customer
    cap: none            # ownership is the check: id AND customer_id
    may: [read_own_order, read_warranty, check_serial_against_order]
    tier: tier1
    up_when: [all_policy_checks_pass]
  - id: 2
    name: Open an RMA
    who: chat_agent
    cap: rma:request:create
    may: [create_rma_request]
    may_not: [issue_refund, issue_replacement]
    tier: tier1
    up_when: [money_would_move, any_condition_true]
  - id: 3
    name: Person decides
    who: support_person
    cap: rma:refund:approve
    may: [approve_refund, approve_replacement, deny]
    may_not: [approve_own_case]
    tier: tier2

conditions:          # any true → tier 2, a person decides
  E1: { name: outside_warranty_window,   test: "today > order.date + product.warranty_days" }
  E2: { name: serial_mismatch,           test: "serial not in order.serials" }
  E3: { name: repeat_return,             test: "count(rma_case where serial = this.serial) >= 1" }
  E4: { name: over_refund_limit,         test: "order_line.amount > policy.auto_refund_limit" }
  E5: { name: disputed_twice,            test: "customer rejected the answer twice" }
  E6: { name: money_moves,               test: "outcome in [refund, replacement]" }
safety:              # any true → tier 3, immediately, from any rung
  S1: { name: safety_report, test: "overheating | smoke | battery swelling | injury" }

Thresholds (warranty_days, auto_refund_limit) are policy values held in Xano, written by the support lead, and never in the prompt.

5. RMA case states

stateDiagram-v2
    [*] --> requested: rung 2 · rma:request:create
    requested --> awaiting_review: any E1–E6 true
    requested --> safety_hold: S1
    awaiting_review --> approved: rung 3 · rma:refund:approve
    awaiting_review --> denied: rung 3 · person
    awaiting_review --> safety_hold: S1
    safety_hold --> awaiting_review: compliance clears
    approved --> closed
    denied --> closed
    closed --> [*]
    note right of awaiting_review: A person decides.\nThe agent cannot leave this state.
    note right of safety_hold: Tier 3. US CPSC §15(b)\n24-hour clock starts.

E6 is true for every refund or replacement, so no case reaches approved without a person. That is deliberate: rung 2 can open a case on its own, but only rung 3 can close one with money attached.

6. Functions

FunctionPrincipalCheckLedger
GET /kb/search?q&category&tagvisitoris_private = false—
GET /kb/articles/:slugvisitoris_private = false; firmware links only if status = attested—
POST /chatvisitor or customerpublic articles only; GPC honored; escalation record has no PIIescalation
GET /me/orders/:idcustomerid AND customer_id = caller, else 404—
POST /rmachat agentrma:request:create; order owned by the mandate's customeryes
POST /rma/:id/decisionsupport personrma:refund:approve; decider ≠ case openeryes
POST /firmware/:id/attestrelease engineersignature verifies with signer_key_id; SBOM presentyes, with sha256

Paths and cap names are illustrative; caps follow plane:resource:verb.

7. Verification and attestation

Attestation answers one question for every artifact a customer receives: can anyone check, after the fact, that this is exactly what we reviewed and released?

7.1 Firmware

The chatbot's most consequential answer is "here is your firmware update". So it may only link firmware the record says is attested.

RequirementHow it's checked
The image is signedsignature verifies against the public key signer_key_id, over sha256 of the image bytes
The SBOM existssbom_key resolves in R2 and lists the image's components
The hash is publicThe KB article shows sha256, so a customer or support person can compare
Only attested images are linkedGET /kb/articles/:slug filters status = attested; draft and revoked are never linked
Revocation propagatesSetting revoked removes the link on the next request; the ledger records who revoked it and why
The device verifies before flashingRequired of the updater (NGENUITY or the device bootloader); a valid link is not a substitute

Background: Your Firmware Is a URL and Firmware Asset Publishing.

7.2 Browser code

Every script on a page that hosts the chatbot runs with that page's full authority. Each one must be either built and deployed through the gate in section 8, or pinned to a known hash.

RequirementHow it's checked
Third-party scripts carry SRIEvery <script src> from another origin has integrity="sha384-…" and crossorigin
First-party scripts are versionedcrm-chat.<hash>.js, not crm-chat.js, so SRI can pin it without breaking on each deploy
A CSP limits script originsContent-Security-Policy: script-src lists exactly the origins used
The deploy records what shippedThe build writes an asset manifest (SHA-256 of every file) and the ledger stores its hash
The consent gate is attestedAs in Consent gate attestation: measured on the released version, reproducible by anyone

Observed 2026-10-04 on omenphase1-1.webflow.io/knowledge-base-search:

ScriptSRI
Webflow runtime and jQuery (5 files)yes, added by Webflow
crm-chat.js from hxphase11ty.pages.dev (the chatbot)no
kbsearchloader, kbdeeplinksearch, crmlegalloader (custom code)no
Finsweet list attributesno
GSAP and ScrollTrigger (3 files, two origins)no

13 external scripts, 5 with SRI; no script-src policy (the only CSP directive is frame-ancestors). The chatbot script is unversioned, so SRI can't be added until its URL carries a hash. See What SRI is, and where it bites.

8. Deploy controls: workflow locks and human review

The gate blocks; it does not report. And the system that generates code must not be the system that judges it: AI-written changes reach production only after a person who didn't write them approves.

ControlRequiredObserved 2026-10-04 in hxphase11ty/.github/workflows/deploy.yml
L1 · Workflow lockOne deploy per environment at a time; a running deploy is never cancelled mid-wayAbsent. No concurrency group; a push and a content dispatch can deploy at once
L2 · Code vs contentA content-only deploy (Xano content-update) may not ship code that hasn't been reviewedAbsent. repository_dispatch builds whatever is on main
L3 · Human reviewCode deploys only from a commit merged through a PR approved by someone other than its authorAbsent. Deploys on any push to main. Branch protection and environment reviewers aren't available on this private repo's plan, so the workflow itself must enforce it
L4 · Blocking auditnpm audit --audit-level=high fails the buildInformational only (`true`)
L5 · AttestationThe build's asset manifest hash is written to the ledger with the deployAbsent

A minimal change that implements L1, L3 and L4 in the workflow itself:

concurrency:
  group: deploy-production       # L1: one at a time
  cancel-in-progress: false      # never kill a deploy halfway

jobs:
  deploy:
    permissions:
      contents: read
      deployments: write
      pull-requests: read        # to read the review on the merged PR
    steps:
      - uses: actions/checkout@v5
      - name: Require a reviewed commit (L3)
        if: github.event_name == 'push'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          pr=$(gh api repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls --jq '.[0].number // empty')
          [ -n "$pr" ] || { echo "Not merged through a PR: refusing to deploy"; exit 1; }
          author=$(gh api repos/$GITHUB_REPOSITORY/pulls/$pr --jq .user.login)
          ok=$(gh api repos/$GITHUB_REPOSITORY/pulls/$pr/reviews \
               --jq "[.[] | select(.state==\"APPROVED\" and .user.login!=\"$author\")] | length")
          [ "$ok" -gt 0 ] || { echo "PR #$pr has no approval from someone other than $author"; exit 1; }
      - name: Blocking audit (L4)
        run: npm audit --audit-level=high

L2 (a content dispatch may only rebuild the last reviewed code) needs the last approved SHA recorded with each deploy, and L5 needs the ledger write. Both are left as open items in section 10.

flowchart LR
    A[AI or person writes change] --> B[Pull request]
    B --> C{Approved by someone\nwho didn't write it?}
    C -- no --> X1[Blocked]
    C -- yes --> D[Merge to main]
    D --> E{Lock free?\nconcurrency: deploy-production}
    E -- no --> W[Wait in queue]
    W --> E
    E -- yes --> F[Tests · blocking audit]
    F -- fail --> X2[Blocked]
    F -- pass --> G[Build · asset manifest SHA-256]
    G --> H[Deploy]
    H --> I[Ledger: deploy + manifest hash]
    J[Xano content-update] --> K{Code unchanged since\nlast reviewed deploy?}
    K -- no --> X3[Blocked]
    K -- yes --> E

9. Test design

The tests below are written from this spec by a person. They are the fixed target: an AI may generate additional cases against them, but it doesn't write or relax the gate's tests, and a generated test that passes on first run is treated with suspicion.

9.1 Test matrix

IDGivenWhenThenCovers
T01A private article matching the queryVisitor searchesIt isn't returnedrung 0, is_private
T02A private articleThe chatbot answersIt isn't used or quotedrung 0
T03Customer A signed inRequests customer B's order ID404, not 403rung 1, ownership
T04In warranty, serial matches, first return, under limitChat agent opens an RMACase requested; no refund issuedrung 2
T05Order outside the warranty windowChat agent opens an RMACase awaiting_review, conditions_hit: [E1]E1
T06Serial not on the orderChat agent opens an RMAawaiting_review, [E2]E2
T07A previous case for the same serialChat agent opens an RMAawaiting_review, [E3]E3
T08Line amount over the refund limitChat agent opens an RMAawaiting_review, [E4]E4
T09Any caseChat agent calls the decision endpointRefused: lacks rma:refund:approverung 3, E6
T10A person opened the caseThe same person decides itRefused: decider ≠ openerrung 3
T11Message mentions a swelling batteryAt any rungTier 3; case safety_hold; clock startsS1
T12Any escalationRecord is writtenContains no name, email, phone or addressescalation
T13Firmware release draftArticle links firmwareNot linked7.1
T14Firmware signature fails verificationAttest is calledRefused; status stays draft7.1
T15Release revokedNext article requestLink is gone; ledger has the revocation7.1
T16A third-party script without integrityPage check runsGate fails, naming the script7.2
T17Commit pushed straight to mainDeploy runsRefused: not merged through a PRL3
T18PR approved only by its author's accountDeploy runsRefusedL3
T19Two deploys triggered togetherBoth startSecond waits; neither is cancelledL1
T20npm audit reports a high vulnerabilityDeploy runsBuild failsL4

9.2 Testing diagram

Each decision point in the ladder and the release path, with the tests that hold it.

flowchart TD
    Q[Customer message] --> S1{Safety words?}
    S1 -- yes --> T3[Tier 3 · safety_hold]:::warn
    S1 -- no --> R0{Public article answers it?}
    R0 -- yes --> A0[Answer · tier none]
    R0 -- no / wants a return --> AUTH{Signed in?}
    AUTH -- no --> SIGN[Ask to sign in]
    AUTH -- yes --> OWN{Order belongs\nto caller?}
    OWN -- no --> N404[404]
    OWN -- yes --> COND{Any of E1–E5?}
    COND -- yes --> T2[Tier 2 · awaiting_review]:::warn
    COND -- no --> OPEN[Open RMA · requested]
    OPEN --> MONEY{Refund or\nreplacement?}
    MONEY -- yes --> T2
    T2 --> PERSON{Person with\nrma:refund:approve,\nnot the opener}
    PERSON --> DONE[approved or denied · ledger]

    T3 -.- t11([T11])
    R0 -.- t01([T01 T02 T13 T15])
    OWN -.- t03([T03])
    COND -.- t05([T05 T06 T07 T08])
    OPEN -.- t04([T04 T12])
    MONEY -.- t09([T09])
    PERSON -.- t10([T10])

    classDef warn fill:#ffffff,stroke:#000000,stroke-width:3px,color:#000000

9.3 The gate

10. Open questions

  1. The values of warranty_days per product and auto_refund_limit, and who signs off changes to them.
  2. Which people hold rma:refund:approve, and whether high-value cases need two of them.
  3. Where the last reviewed SHA is recorded for L2: a GitHub deployment record or the ledger.
  4. Whether the Webflow custom-code scripts (kbsearchloader, crmlegalloader) move into the reviewed repo, so they can be versioned and pinned with SRI.
  5. Which updater (NGENUITY or the bootloader) verifies firmware signatures on each product line, and how that is evidenced for the CRA.