# Orange Anchor Widget  
**Widget Concept Specification & Reference Skeleton — v0.1**  
**May 2026**

> **Core invariant.** Per-commitment production cost is positive and scales linearly with volume up to operational constants, under a stated, adversary-favourable threat model. The architectural contribution is cost preservation and attribution: production cost paid by a holder persists as attributable scarcity across every system that recognises the commitment. See *Orange Anchor White Paper v2.3* for the full argument.

> **Implementation Status — read first.**
> The JavaScript skeleton in §3 is a **wireframe** illustrating the integration shape and event flow for an Orange Anchor check-in / cosign widget. It contains clearly-marked **simulation stubs** for Nostr DM transport, signature verification, and Bitcoin attestation construction. A production implementation must replace these stubs with real Nostr NIP-17 transport, real BIP340 Schnorr signature verification, and real Bitcoin chain queries (typically performed by the integrator's backend, not the widget itself). **This document is NOT a production-ready library.** It describes the integration *shape*; end-to-end production integration requires backend Bitcoin verification logic — see *Orange Anchor Integration Brief v2.2* §4.3.3 for full scope.

**Purpose**  
A dependency-free JavaScript widget concept that lets any website accept Orange Anchor collateral commitments for check-in (login) or cosign (identity linkage). This document specifies the JSON wire formats, event flow, and the reference skeleton from which a conformant production widget can be built.

The widget is **self-contained at the specification level** — this single document contains the JSON contracts and the integration shape. Production implementations of the skeleton, real Nostr transport, and backend verification are out of scope and live in the Orange Anchor reference implementation.

---

## 1. Overview & Core Principles

- **Two modes**: `checkin` (sign-in) and `cosign` (link/commitment to existing identity)
- **Primary transport**: Nostr DM (NIP-17) for maximum simplicity and native feel
- **Fallbacks**: QR code, deep links, manual copy-paste (air-gapped friendly)
- **Verification model**: **Inclusion verification only.** This widget verifies that the user controls the commitment key and that the anchor transactions are confirmed on Bitcoin (the latter performed by the integrator's backend). It does **not** verify construction quality — that is, whether the commitment's bracketed work was performed faithfully. Integrators requiring construction-quality assurance must additionally query a BARU score from a trusted audit operator (see *BAVAI Operator Specification v1.0* §6.3) or perform construction verification using the Orange Anchor reference implementation.
- **Size target**: < 25 KB gzipped for a production implementation (vanilla JS, no heavy dependencies)
- **Privacy**: Ephemeral challenges only — no user data stored by widget
- **Wire format**: All JSON field names in protocol messages are **snake_case**, matching the canonical specification in *Orange Anchor Interaction Patterns v1.0* §2.3 and *Orange Anchor Lexicon v2.7* §5b. Signed messages use canonical JSON per RFC 8785; field names are part of the signed payload.

---

## 2. API Contracts (Exact JSON Schemas)

### 2.1 Challenge Object (sent to user)

*Canonical field names are snake_case and match Interaction Patterns v1.0 §2.3.*

**For checkin mode**
```json
{
  "action": "checkin",
  "nonce": "a1b2c3d4e5f6... (32-byte hex)",
  "verifier_id": "your-unique-site-identifier",
  "timestamp": "2026-05-11T22:00:00Z",
  "expires": "2026-05-11T22:05:00Z",
  "policy": {
    "require_attribution_check": false,
    "minimum_baru_score": 0
  }
}
```

**For cosign mode**
```json
{
  "action": "cosign",
  "nonce": "a1b2c3d4e5f6...",
  "verifier_id": "your-unique-site-identifier",
  "target_pubkey": "npub1... or did:plc:... (optional)",
  "purpose": "Link to my forum account @alice",
  "timestamp": "2026-05-11T22:00:00Z",
  "expires": "2026-05-11T22:05:00Z"
}
```

### 2.2 Signed Response (returned by user / app)

```json
{
  "challenge": { "...": "original challenge object verbatim" },
  "commitment_pubkey": "<33-byte compressed public key, hex>",
  "signature": "<64-byte BIP340 Schnorr signature over canonical-JSON challenge, hex>",
  "external_signature": "<hex, only for cosign mode>",
  "external_pubkey": "<npub1... or did:..., only for cosign mode>"
}
```

### 2.3 Attestation Object (final output to integrator)

```json
{
  "type": "orange-anchor-attestation-v1",
  "version": "1.0",
  "commitment_pubkey": "02...",
  "commitment_signature": "sig...",
  "challenge": { "...": "original challenge object" },
  "verified_at": "2026-05-11T22:01:23Z",
  "bitcoin_anchors": {
    "start_block": 840000,
    "end_block": 840144,
    "start_txid": "...",
    "end_txid": "..."
  },
  "attribution": {
    "flags": 0,
    "last_checked": "2026-05-11T22:01:23Z",
    "operator": "public-index.orangeanchor.io"
  },
  "cosign_linkage": {          // only present in cosign mode
    "external_pubkey": "npub1...",
    "external_signature": "sig...",
    "linkage_proof": "merkle-proof-or-hash"
  }
}
```

---

## 3. Widget JavaScript Skeleton (Reference Wireframe)

> **DEMO STUB.** The skeleton below illustrates the integration shape, the configuration surface, and the event flow. Methods marked `DEMO STUB` contain simulation logic only and MUST be replaced before any production use. The skeleton's JavaScript object properties (e.g. `this.config.siteId`) are private to the widget and may remain camelCase as a local-language choice; the **JSON wire format** (§2) is normative and uses snake_case.

```html
<!-- Orange Anchor Widget v0.1 - Reference Wireframe (NOT PRODUCTION) -->
<script>
/**
 * Orange Anchor Widget — Reference Wireframe.
 *
 * This is a CONCEPT SKELETON. Methods marked "DEMO STUB" do not perform real
 * cryptographic operations and MUST be replaced before production use. A
 * conformant production implementation must:
 *   1. Send the canonical-JSON challenge via real Nostr NIP-17 encrypted DM
 *      (or one of the documented fallback transports in §4).
 *   2. Receive the signed response from the holder's Orange Anchor app.
 *   3. Verify the BIP340 Schnorr signature against the canonical-JSON challenge.
 *   4. Hand the attestation to the integrator's backend, which performs
 *      inclusion verification against the Bitcoin chain.
 */

// Placeholder constants used by DEMO STUB methods only. These values exist
// purely to illustrate the attestation shape and MUST NOT appear in production.
const DEMO_BLOCK_HEIGHT_START = 840000;
const DEMO_BLOCK_HEIGHT_END   = 840144;
const DEMO_TXID_PLACEHOLDER   = "DEMO-STUB-TXID-NOT-A-REAL-BITCOIN-TXID";
const DEMO_SIG_PLACEHOLDER    = "DEMO-STUB-SIG-NOT-A-REAL-SIGNATURE";
const DEMO_PUBKEY_PLACEHOLDER = "DEMO-STUB-PUBKEY-NOT-A-REAL-KEY";

class OrangeAnchorWidget {
  constructor(config) {
    this.config = {
      mode: config.mode || 'checkin',           // 'checkin' or 'cosign'
      siteId: config.siteId,                    // becomes "verifier_id" in wire JSON
      onSuccess: config.onSuccess || (() => {}),
      onError: config.onError || console.error,
      requireAttributionCheck: config.requireAttributionCheck || false,
      nostrRelays: config.nostrRelays || ['wss://relay.damus.io', 'wss://nos.lol'],
      ...config
    };
    this.challenge = null;
    this._pendingResolve = null;
  }

  // Generate a fresh canonical challenge (snake_case JSON per §2.1)
  async generateChallenge() {
    const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)))
      .map(b => b.toString(16).padStart(2, '0')).join('');

    this.challenge = {
      action: this.config.mode,
      nonce: nonce,
      verifier_id: this.config.siteId,
      timestamp: new Date().toISOString(),
      expires: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 min
      policy: {
        require_attribution_check: this.config.requireAttributionCheck,
        minimum_baru_score: this.config.minimumBaruScore || 0
      }
    };

    if (this.config.mode === 'cosign' && this.config.targetPubkey) {
      this.challenge.target_pubkey = this.config.targetPubkey;
      this.challenge.purpose = this.config.purpose || 'Link identity';
    }

    return this.challenge;
  }

  // Open the widget UI (QR + buttons)
  async open() {
    await this.generateChallenge();

    const modal = document.createElement('div');
    modal.innerHTML = `
      <div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:9999">
        <div style="background:white;padding:30px;border-radius:12px;max-width:420px;text-align:center">
          <h3>Sign with Orange Anchor</h3>
          <p>Challenge ready. Choose method:</p>

          <button onclick="window.OAWidget.sendViaNostr()">Send via Nostr DM (recommended)</button><br><br>

          <button onclick="window.OAWidget.copyChallenge()">Copy Challenge JSON</button><br><br>

          <div id="qr-placeholder" style="margin:15px 0">QR code will appear here (add qrcode.js for production)</div>

          <button onclick="window.OAWidget.closeModal()">Cancel</button>
        </div>
      </div>
    `;
    document.body.appendChild(modal);
    window.OAWidget = this; // expose for button clicks (note: this pattern supports only one widget instance per page)
    this.currentModal = modal;
  }

  // Open and return a Promise that resolves with the attestation. Convenience wrapper.
  openAndWait() {
    return new Promise((resolve, reject) => {
      const originalOnSuccess = this.config.onSuccess;
      const originalOnError = this.config.onError;
      this.config.onSuccess = (att) => { try { originalOnSuccess(att); } finally { resolve(att); } };
      this.config.onError = (err) => { try { originalOnError(err); } finally { reject(err); } };
      this.open();
    });
  }

  closeModal() {
    if (this.currentModal) this.currentModal.remove();
  }

  // DEMO STUB. Replace with real Nostr NIP-17 transport.
  // TODO(production): publish encrypted NIP-17 DM containing canonicalJson(this.challenge);
  // subscribe for reply event; on reply, verify BIP340 signature; deliver to handleSignedResponse().
  async sendViaNostr() {
    console.warn('[DEMO STUB] sendViaNostr() called. Production implementations must replace this with real NIP-17 transport.');
    throw new Error('sendViaNostr() not implemented in reference wireframe. See Integration Guide §4.');
  }

  copyChallenge() {
    const challengeStr = JSON.stringify(this.challenge, null, 2);
    navigator.clipboard.writeText(challengeStr);
    console.warn('[DEMO STUB] Challenge copied. Production implementations must additionally show a paste input field for the signed response.');
    // TODO(production): show paste input field; on paste, parse signed response and call handleSignedResponse().
  }

  // Handle a signed response received from the holder's app.
  // DEMO STUB: in production, verify the BIP340 signature against canonicalJson(this.challenge)
  // using commitment_pubkey BEFORE constructing the attestation.
  handleSignedResponse(signedResponse) {
    // TODO(production): verify BIP340 Schnorr signature over canonicalJson(this.challenge)
    //                   against signedResponse.commitment_pubkey; reject if invalid.
    this.closeModal();
    this.config.onSuccess(this.buildAttestation(signedResponse));
  }

  // DEMO STUB attestation builder. The bitcoin_anchors block here contains
  // PLACEHOLDER values; in production the integrator's backend obtains real
  // anchor data by parsing the holder's portable proof envelope and performing
  // inclusion verification against the Bitcoin chain.
  buildAttestation(signedResponse) {
    return {
      type: "orange-anchor-attestation-v1",
      version: "1.0",
      commitment_pubkey: signedResponse.commitment_pubkey || DEMO_PUBKEY_PLACEHOLDER,
      commitment_signature: signedResponse.signature || DEMO_SIG_PLACEHOLDER,
      challenge: signedResponse.challenge || this.challenge,
      verified_at: new Date().toISOString(),
      bitcoin_anchors: {
        // DEMO STUB values. Replace with real anchors parsed from the portable proof envelope.
        start_block: DEMO_BLOCK_HEIGHT_START,
        end_block: DEMO_BLOCK_HEIGHT_END,
        start_txid: DEMO_TXID_PLACEHOLDER,
        end_txid: DEMO_TXID_PLACEHOLDER
      },
      attribution: {
        flags: 0,
        last_checked: new Date().toISOString(),
        operator: "public-index.orangeanchor.io"
      },
      cosign_linkage: signedResponse.external_pubkey ? {
        external_pubkey: signedResponse.external_pubkey,
        external_signature: signedResponse.external_signature,
        linkage_proof: "PLACEHOLDER-merkle-proof"
      } : undefined
    };
  }
}

// Usage example (copy-paste this block)
</script>
```

**How to use the skeleton above**  
1. Paste the `<script>` block into your HTML.  
2. Add a button:  
   ```html
   <button onclick="new OrangeAnchorWidget({mode:'checkin', siteId:'my-forum-123', onSuccess: handleSuccess}).open()">
     Sign in with Orange Anchor
   </button>
   ```  
3. Implement `handleSuccess(attestation)` — send `attestation` to your backend.  
4. **Implement the DEMO STUB methods** (`sendViaNostr`, `handleSignedResponse`, real signature verification, real attestation construction). Until these are implemented the widget is a wireframe, not a working integration.

---

## 4. Integration Guide

### 4.1 Basic Forum / Airdrop Integration (minimal)

```html
<button id="oa-btn">Sign in with Orange Anchor</button>

<script src="orange-anchor-widget.js"></script>
<script>
  const widget = new OrangeAnchorWidget({
    mode: 'checkin',
    siteId: 'my-forum-2026',
    onSuccess: async (att) => {
      const res = await fetch('/api/verify-orange-anchor', { method: 'POST', body: JSON.stringify(att) });
      if (res.ok) window.location = '/dashboard';
    }
  });
  document.getElementById('oa-btn').onclick = () => widget.open();
</script>
```

### 4.2 Nostr-Native Integration (Recommended)

```html
<button>Link Nostr Identity + Orange Anchor</button>

<script>
  new OrangeAnchorWidget({
    mode: 'cosign',
    siteId: 'nostr-app-xyz',
    targetPubkey: 'npub1yourcurrentnpub...',   // pre-fill if known
    purpose: 'Link to my Nostr profile for reputation',
    onSuccess: (att) => {
      // att.cosignLinkage.externalPubkey is the npub
      // Store linkage in your database
      console.log('Nostr + Orange Anchor linked!', att);
    }
  }).open();
</script>
```

**Nostr-specific notes**  
- Widget publishes encrypted NIP-17 DM containing the challenge.  
- User signs in any Nostr client (Amethyst, Damus, Primal, etc.).  
- Reply comes back via Nostr relay or deep link.  
- No custom wallet connection required.

### 4.3 Airdrop / Token Claim Example

```js
// After user claims airdrop
const attestation = await widget.openAndWait(); // returns promise; resolves with attestation
if (attestation.attribution.flags === 0) {
  // Allow claim
  claimAirdrop(attestation.commitment_pubkey);
}
```

---

## 5. Security & Best Practices

- Always verify the attestation on your **backend** against Bitcoin (start/end anchors) and the public attribution index. The widget's `bitcoin_anchors` block carries the values the user's app produced; your backend must independently confirm them against the chain.
- Use short expiry (5 minutes recommended).
- `nonce` + `verifier_id` + `timestamp` prevents replay across sessions and across verifiers.
- For high-value actions, require `require_attribution_check: true` and apply a non-zero `minimum_baru_score`.
- Never trust the widget output alone — treat it as an input to your policy.
- **Inclusion verification is not construction verification.** Anchor presence on Bitcoin proves that two transactions were submitted; it does not prove that the bracketed work was performed faithfully. Construction-quality assurance requires a BARU score from a trusted audit operator or use of the Orange Anchor reference implementation.

---

## 6. Configuration Options (Full List)

*JavaScript constructor option names are camelCase by JS convention. The JSON wire format these options map to (§2) is snake_case.*

| Option                    | Type     | Default     | Wire-format JSON field            | Description |
|---------------------------|----------|-------------|-----------------------------------|-------------|
| `mode`                    | string   | 'checkin'   | `action`                          | 'checkin' or 'cosign' |
| `siteId`                  | string   | required    | `verifier_id`                     | Unique identifier for your site |
| `onSuccess`               | function | —           | (callback)                        | Called with attestation object |
| `onError`                 | function | console.error | (callback)                      | Error handler |
| `requireAttributionCheck` | boolean  | false       | `policy.require_attribution_check`| Auto-query public index for flags |
| `minimumBaruScore`        | number   | 0           | `policy.minimum_baru_score`       | Minimum acceptable BARU score |
| `nostrRelays`             | array    | public relays | (transport config)              | List of relays for NIP-17 |
| `theme`                   | string   | 'light'     | (UI only)                         | 'light' / 'dark' / custom |
| `qrSize`                  | number   | 200         | (UI only)                         | QR code pixel size |

---

## 7. Error Codes

- `EXPIRED_CHALLENGE`
- `INVALID_SIGNATURE`
- `ATTRIBUTION_FLAGGED`
- `BITCOIN_ANCHOR_MISSING`
- `USER_CANCELLED`

---

**This document specifies the integration shape and the JSON wire format.**  
The JavaScript skeleton is a reference wireframe. A conformant production widget replaces every method marked `DEMO STUB`, performs real BIP340 signature verification on the client, and is paired with a backend that performs inclusion verification against the Bitcoin chain. End-to-end integration timing depends on the integrator's existing Bitcoin tooling and trust-and-safety workflow; see *Orange Anchor Integration Brief v2.2* §4.3.3.

---

*Orange Anchor Widget Concept Specification & Reference Skeleton v0.1 — May 2026.*