Skip to content

Latest commit

 

History

History
898 lines (626 loc) · 44.6 KB

File metadata and controls

898 lines (626 loc) · 44.6 KB

3pm — Complete Vision, Architecture & Thought Process

This document is the single source of truth for why this project exists, what it is trying to do, every architectural decision made, and the reasoning behind each one. It is written to be read by a developer picking up this codebase cold — you should finish reading this and have a complete mental model of the system before touching any code. Nothing is assumed. Everything is explained.


Table of Contents

  1. The Problem
  2. The Pivot: Why We Don't Replace npm
  3. The Core Insight: Trust Layer, Not Registry
  4. How Package Ownership Actually Works
  5. How Multisig Works: Complete Technical Walkthrough
  6. Wallet Connection: Why Keys Never Touch the CLI
  7. Team Management: The Invite Flow
  8. The Full Product: Six Layers of Trust
  9. TEE: Trusted Execution Environments
  10. Critical Analysis of Current Implementation
  11. The Better Combined Architecture
  12. Hackathon Demo Sequence
  13. Full Command Surface
  14. Ideas Not Yet Explored
  15. Why This Is the Right Architecture

1. The Problem

npm supply chain attacks are not theoretical. They are one of the most damaging and consistent attack vectors in modern software development. Here are real incidents:

  • event-stream (2018) — a malicious actor convinced the original author to transfer maintainership, then injected a cryptominer targeting a specific Bitcoin wallet. 3.5 million downloads per week. Nobody noticed for months.
  • ua-parser-js (2021) — the maintainer's npm account was compromised. A malicious version was live for 6 hours. Millions of machines affected before the package was pulled.
  • node-ipc (2022) — the maintainer themselves deliberately added code that wiped files on Russian and Belarusian IP addresses as a political protest. No compromise needed. One person, one decision.
  • xz-utils (2024) — a two-year social engineering campaign to gain maintainer trust, then insert a backdoor into a core Linux utility. Nearly shipped in major Linux distributions before a Microsoft engineer noticed by accident.

What all of these share: a single point of trust that failed. One compromised npm token, one rogue maintainer, one stolen account — and every downstream project installs malicious code.

npm's current response is "provenance attestations" — linking a release to a specific GitHub Actions CI run. This sounds good but doesn't solve the problem. It just moves the single point of failure from "npm publish token" to "GitHub Actions token." Compromise the CI secret and you're back to square one. It also does nothing for the node-ipc scenario — the maintainer had legitimate access.

The real fix requires M-of-N authorization. If publishing requires multiple independent humans to sign off, an attacker must simultaneously compromise M separate people — people on different machines, different companies, different countries, with different security postures. That is not just harder. It is a qualitatively different class of attack.


2. The Pivot: Why We Don't Replace npm

The original idea for this project was to replace npm entirely — a fully decentralized registry where package names are NFTs, tarballs live on IPFS, and publishing requires multisig from day one. That is technically ambitious and parts of it were built.

We stepped back and asked: does the world need another package registry?

The answer is no. npm works. It has the packages, the tooling, the CDN, the ecosystem, the IDE integrations, the corporate allowlists. Developers are not going to switch registries because the switching cost is enormous and the benefit to any individual developer is invisible until something goes wrong — at which point it is too late.

The right approach is to add safety to the ecosystem people already use, not ask them to leave it.

So the pivot: 3pm does not replace npm. It wraps it. npm handles hosting and distribution. 3pm handles accountability and verification. These are separate jobs and we only do ours.

This reframing changes everything about the product:

  • Zero adoption friction for consumers3pm install calls npm install internally if verification passes. Same result, one extra trust check.
  • Opt-in for publishers — you register your package on 3pm, set your multisig rules, and from that point the chain is the source of truth. Packages not on 3pm still install — they are just unverified.
  • No ecosystem lock-in — if 3pm disappeared tomorrow, all packages are still on npm, perfectly installable. The chain records are proofs, not infrastructure.
  • Composable — every piece (on-chain records, CLI, Chrome extension, AI scorer) is independently useful.

3. The Core Insight: Trust Layer, Not Registry

When a developer runs npm install some-package, they are making a trust decision. They are saying: "I trust that these bytes are safe to run on my machine." Currently nothing enforces or verifies that trust. You get bytes. You hope they are what the maintainer intended.

3pm install intercepts that moment. Before any code runs on your machine:

  1. It checks what hash the maintainers cryptographically signed off on
  2. It verifies the bytes you just received from npm match that hash exactly
  3. (When the AI layer is live) It checks what a local model thinks of the diff from the previous version

If everything checks out, it calls npm install transparently. If not, it warns you and asks what you want to do.

This is the same mental model as HTTPS. Your browser does not stop you from visiting a site with an invalid certificate. It warns you, explains the risk, and lets you decide. But you cannot claim you were not informed. 3pm brings that pattern to package installation.


4. How Package Ownership Actually Works

This is the most important design question in the system and worth understanding deeply. The question is: if you mint an NFT for my-pkg on Base Sepolia, what does that actually mean? npm does not know about your blockchain.

The honest answer is that NFT ownership and npm ownership are two separate systems that must be deliberately connected. Understanding how they connect — and what each one controls — is essential.

Two Separate Ownership Layers

npm ownership is controlled by npm access tokens. npm has its own auth system. It does not know or care about your wallet address or your NFT. This is immutable — we cannot change how npm works.

3pm ownership is controlled by the ERC-721 NFT on-chain. The NFT owner controls: who the co-signers are, what the signing threshold is, and who can authorize releases through the 3pm system.

These two must be connected at registration time and stay connected through a deliberate handoff.

The Registration and Handoff Flow

Here is how a developer gets their npm package into 3pm:

Step 1: Prove you own the npm package ✅ IMPLEMENTED

Anyone could try to claim react or lodash on 3pm. You need proof of legitimate ownership before an NFT is minted. The solution is publish-based verification — the same pattern as domain verification (Google Search Console, Let's Encrypt, DNS TXT records):

3pm register react

→ CLI derives token: "3pm-verify-<your-wallet-address-lowercase>"
→ CLI prints:
  "Add this to your package.json and publish any version:
   { \"_3pm\": \"3pm-verify-0xabc...def\" }"
→ You publish any version to npm with that field
→ 3pm fetches https://registry.npmjs.org/react/latest and checks _3pm === token
→ Proof confirmed: only the real npm owner could have published that
→ NFT minted to your wallet address
→ You are now the 3pm owner of this package

This is provable, permissionless, and requires no trust in a third party. If you can publish to npm, you are the owner. The token in the published package proves it.

Step 2: Hand the npm token to 3pm ✅ IMPLEMENTED

This is the critical step. After registration:

3pm register-token react

You generate an npm automation token scoped to this package and hand it to the backend. The command:

  1. Signs register-npm-token:react as EIP-191 with your wallet — proves you are the NFT owner
  2. Calls https://registry.npmjs.org/-/whoami to validate the token is real before storing it
  3. Encrypts the token with AES-256-GCM (key from NPM_TOKEN_ENCRYPTION_KEY env var) and stores it in the Package.npmToken database field

From this point:

  • dualPublish looks up the per-package token from the DB and decrypts it at publish time
  • Falls back to the global NPM_PUBLISH_TOKEN env var if no per-package token is registered
  • Publishing to npm requires going through 3pm multisig — the backend never exposes the token
  • If someone tries npm publish directly with a personal token — they can if they still have one, which is why you rotate and hand over exclusively

Step 3: The chain of custody

npm publish rights        → held by 3pm backend (npm automation token)
Who authorizes 3pm        → NFT owner (your wallet via MetaMask)
Who counts as co-signer   → NFT owner adds them via addMaintainer() on-chain
How releases get approved → M-of-N co-signers sign EIP-712 payload

The NFT is not trying to replace npm's auth. It is the governance layer for the 3pm system, which in turn holds the npm token. Security comes from that chain of delegation.

What Happens if Someone Bypasses 3pm?

Say an attacker somehow gets a separate npm token and publishes a malicious version directly to npm, bypassing multisig entirely.

  • That version has no on-chain record in PackageRegistry
  • 3pm install react@18.4.0 → downloads from npm → checks chain → no record → warns: "This version has no on-chain verification record. No maintainers signed it."
  • The user can proceed, but cannot claim ignorance

This is the second layer of defense. The absence of an on-chain record is itself a signal.

What Happens if the Owner Loses Their Wallet?

If the NFT owner loses their wallet (lost phone, deleted MetaMask, no seed phrase backup):

  • The NFT is stuck at that address and cannot be administered
  • No one can add/remove maintainers or change threshold
  • The package enters a kind of governance freeze

Mitigations to build:

  1. Recovery address — at registration time, optionally designate a second wallet that can claim ownership after a time delay (e.g., 30 days of owner inactivity)
  2. Maintainer rescue vote — if M-of-N existing maintainers sign an ownership transfer request, it executes even without the owner
  3. The practical answer — document your 12-word MetaMask seed phrase offline. If you have those words, you can restore your wallet on any device.

5. How Multisig Works: Complete Technical Walkthrough

The Signing Object

Every release is represented by an EIP-712 typed data payload. EIP-712 is an Ethereum standard for structured, human-readable typed data signing — MetaMask shows the fields clearly rather than asking you to sign an opaque hex blob.

Domain: {
  name:              "3pm",
  version:           "1",
  chainId:           84532,        // Base Sepolia
  verifyingContract: "0x..."       // PackageRegistry address
}

Type: Release {
  packageName: string,             // "react"
  version:     string,             // "18.3.0"
  integrity:   string,             // "sha512-ABC123==" — npm SRI format
  contentHash: bytes32,            // sha256(tarball) — efficient on-chain comparison
  nonce:       uint256             // per-package, fetched from chain
}

Two hash fields:

  • integritysha512-base64 in npm's Subresource Integrity format. This enables npm ecosystem interop and is what npm's own lockfiles use.
  • contentHashsha256(tarball) as bytes32. Efficient for on-chain storage and comparison.

The nonce is a per-package counter stored in the PackageRegistry contract. It increments after every successful publish. Including it in the signed payload means an old signature from v1.0.0 cannot be replayed against v1.0.1. Even if an attacker captures all signatures from a past release, they are useless for future releases.

The verifyingContract in the domain means signatures are bound to the specific contract instance. A signature for the 3pm PackageRegistry cannot be replayed against a different contract.


Phase 1: The Proposer Initiates (3pm publish)

The proposer is the developer initiating the release. They do not need to be the NFT owner — any registered maintainer can propose.

Developer runs: 3pm publish

1. Reads package.json → validates name, version, semver
2. Preflight checks:
   - Package is registered on 3pm (NFT exists)
   - Caller is a registered maintainer
   - This version hasn't been published already
3. npm pack → produces tarball bytes
4. Computes:
   - integrity    = "sha512-" + base64(sha512(tarball))
   - contentHash  = sha256(tarball) as bytes32
5. PackageRegistry.getNonce(packageName) → nonce = 5
6. Constructs EIP-712 payload: { packageName, version, integrity, contentHash, nonce }
7. WalletConnect sends signing request → MetaMask on phone
8. Proposer reviews payload in MetaMask, approves → signature_1
9. Uploads tarball + proposal to backend:
   POST /api/propose-release {
     name, version, integrity, contentHash, nonce,
     gitSha, ciRunUrl,
     signature: sig_1, signerAddress: proposer_address
   }
10. Backend creates PendingRelease, stores tarball
11. CLI prints:
    ✓ Release proposed
    Package:    react@18.3.0
    Hash:       sha512-ABC123==
    Nonce:      5
    Signatures: 1 of 3

    Co-signers: run `3pm approve <releaseId>`

Critical design note: The proposer signs as co-signer #1. This is different from a naive design where the proposer just proposes and does not sign. Requiring the proposer to sign means you cannot create a malicious proposal without your wallet address being permanently attached to it. There is a cryptographic cost to proposing.


Phase 2: Co-Signers Approve (3pm approve <releaseId>)

Each co-signer runs the approve command on their own machine. The releaseId is a UUID that uniquely identifies this pending release — more precise than pkg@version since a package could theoretically have multiple pending releases.

Co-signer runs: 3pm approve abc-123-def

1. CLI fetches pending release: GET /api/pending-by-id/abc-123-def
2. Validates:
   - Status is PENDING (not expired, not already published)
   - This signer hasn't already signed
3. CLI displays:

   Package:      react@18.3.0
   Integrity:    sha512-ABC123==
   ContentHash:  0xdeadbeef...
   Proposed by:  0x1234... (alice.eth)
   Nonce:        5
   Signatures:   1 of 3 collected
   Expires:      in 71 hours

   Proceed to sign? (y/n)

4. Co-signer confirms
5. CLI reconstructs the EXACT same EIP-712 payload
   (must match byte-for-byte with what proposer signed)
6. WalletConnect → MetaMask → co-signer approves → signature_2
7. POST /api/submit-signature { releaseId, signature, signerAddress }
8. Backend:
   - Confirms signer is a registered maintainer on-chain
   - Validates signature (recovers address from ECDSA, must match signerAddress)
   - Deduplicates (cannot sign twice)
   - Appends to signatures[]
   - Checks: signatures.length >= threshold?
     → YES: threshold met, trigger publish
     → NO:  "Collected 2 of 3 signatures"

Why every signer signs the same payload: The smart contract rebuilds the EIP-712 hash from scratch using the release data and verifies each signature against that hash. If any co-signer signed different data (different integrity, different nonce, different version), their signature will fail ECDSA recovery on-chain and the publish transaction will revert.


Phase 3: On-Chain Publish (Last Signer or Permissionless Relay)

Once the threshold is met, the publish transaction must be submitted to the PackageRegistry contract. There are two approaches:

Approach A — Last signer submits (preferred)

The CLI detects that the co-signer's approval pushes the count to threshold and prompts:

✓ Threshold reached (3 of 3)
You are the final signer. Submit on-chain now?
Cost: ~$0.01 in gas on Base Sepolia
(y/n)

If yes, their already-connected MetaMask sends the publish() transaction. No separate relayer key needed.

Approach B — Permissionless relay

The publish() function on-chain only cares about the validity of the signatures. It does not care who calls it. So once enough signatures are collected and stored in the backend, anyone can submit the transaction — the proposer, any co-signer, a public relayer service, or even a dedicated backend relayer account. The signatures are the authorization. The caller is just paying gas.

This means the system can degrade gracefully: if the backend relayer is down, any maintainer can grab the signatures and submit manually.

The on-chain verification (PackageRegistry.publish):

PackageRegistry.publish(
  name,         version,
  integrity,    contentHash,
  signatures[], signers[],
  gitSha,       ciRunUrl
)

Contract does:
1. Fetches PackageMeta from NameRegistry (maintainers[], threshold)
2. Checks version not already published
3. Verifies arrays are same length
4. Rebuilds EIP-712 digest using current nonce
5. For each (signature, signer) pair:
   - signer must be in maintainers[]
   - no duplicate signers
   - if signer address has code (smart contract wallet): EIP-1271 isValidSignature()
   - if EOA: ECDSA.recover(digest, sig) must equal signer address
6. Requires validCount >= threshold
7. Stores Release: { integrity, contentHash, signers[], timestamp, gitSha, ciRunUrl }
8. Increments nonce (prevents replay)
9. Emits PackagePublished event

After on-chain confirmation:

Backend:
1. Receives confirmation event / transaction receipt
2. Marks PendingRelease as PUBLISHED in database
3. Updates Package.latestVersion if newer semver
4. Runs npm publish using stored npm automation token
   (the exact tarball uploaded at propose time)
5. Issues EAS attestation (optional, fire-and-forget)

Phase 4: Install Verification (3pm install react)

This is where the full trust loop closes. This command is what makes all the on-chain record-keeping meaningful.

User runs: 3pm install react@18.3.0

1. Download tarball from npm registry
2. Compute locally:
   - integrity   = "sha512-" + base64(sha512(tarball))
   - contentHash = sha256(tarball)
3. Fetch on-chain: PackageRegistry.getRelease("react", "18.3.0")
   → { integrity, contentHash, signers[], timestamp, ... }
4. Compare:
   - received integrity   === on-chain integrity?   → ✓ or ✗
   - received contentHash === on-chain contentHash? → ✓ or ✗
5. If no on-chain record exists:
   → "⚠ react@18.3.0 has no verified on-chain record.
      It was not published through 3pm multisig.
      Proceed anyway? (y/N)"
6. If hash mismatch:
   → "✗ HASH MISMATCH — react@18.3.0
      The bytes you received from npm do not match what
      the maintainers signed.
      Expected: sha512-ABC123==
      Got:      sha512-XYZ999==
      This may indicate a supply chain attack.
      Do NOT proceed unless you know why this differs.
      Proceed anyway? (y/N)"
7. If all checks pass:
   → "✓ Verified — react@18.3.0
      Signed by 3 maintainers on Base Sepolia
      Signers: alice.eth, bob.eth, carol.eth
      Timestamp: 2024-03-15 14:32 UTC"
   → npm install react@18.3.0 proceeds normally
8. [When AI layer is live]:
   → Diff this version vs previous in local Gemma
   → "Safety score: 87/100 — 1 new outbound network call"
   → If below threshold: additional warning

6. Wallet Connection: Why Keys Never Touch the CLI

This is a critical security property worth understanding clearly.

The CLI connects to the developer's wallet via WalletConnect v2. The protocol creates an encrypted relay channel between the CLI (a WalletConnect "dApp") and the developer's mobile wallet (MetaMask, Coinbase Wallet, Rainbow, etc.).

First-time setup:

3pm wallet connect

→ QR code appears in terminal
→ Developer scans with MetaMask on phone
→ WalletConnect session established (encrypted channel)
→ CLI receives wallet address (public — safe to store)
→ Session data persisted to ~/.3pm/sessions/
→ No QR needed on subsequent commands

When signing is needed (publish, approve):

CLI constructs EIP-712 payload
       ↓
Sends sign request via WalletConnect relay
       ↓
MetaMask receives request, shows structured data:
  Package:    react@18.3.0
  Integrity:  sha512-ABC123==
  Nonce:      5
       ↓
Developer reviews on phone and approves (or rejects)
       ↓
Signature returned to CLI via relay
       ↓
CLI uses signature — never saw the private key

What lives where:

~/.3pm/config.json    ← connected wallet address, registry URL
~/.3pm/sessions/      ← WalletConnect encrypted session
MetaMask (phone)      ← private key, never leaves here

If someone steals your laptop they get your address (public) and a WalletConnect session that requires phone approval to do anything. They cannot sign a single release without your physical phone.

WalletConnect v2 sessions persist and auto-reconnect. You scan once. Future commands connect silently unless the session expires (typically 7 days) or you explicitly disconnect.


7. Team Management: The Invite Flow

The naive approach to adding co-signers is 3pm team add 0x1234567890abcdef1234567890abcdef12345678. Typing a 42-character hex string is not a product experience. ENS names are better (alice.eth) but you cannot require your entire team to have purchased one.

The right solution is an invite flow — the same UX pattern used by every successful collaboration tool:

Owner:      3pm team invite --package react
            → Invite code: 3pm-abc-xyz-123 (expires in 24h)
            → Share this with your co-signer via Slack, email, whatever.

Co-signer:  3pm team join 3pm-abc-xyz-123
            → [QR code appears in their terminal]
            → They scan with MetaMask
            → Their wallet address submitted to backend
            → Backend calls NameRegistry.addMaintainer() via relayer
            → Owner sees: ✓ 0x5678...ef joined react as maintainer

The owner never types an address. The co-signer never needs ENS. The coordination happens via a sharable code over whatever channel the team already uses.

For teams with ENS or Basenames, also support name-based adding:

3pm team add alice.eth            # ENS: resolves via Ethereum mainnet
3pm team add alice.base.eth       # Basename: resolves directly on Base (cheaper, native)
3pm team add 0x1234...            # Raw address: always works as fallback

Threshold management:

3pm team threshold 2              # 2-of-N must sign every release
3pm team status                   # show current team

Package:   react
Owner:     you.eth (0x1234...)
Threshold: 2-of-4

Maintainers:
  ✓ you.eth     (0x1234...)
  ✓ alice.eth   (0x5678...)
  ✓ bob.eth     (0x9abc...)
  ✓ carol.eth   (0xdef0...)

Important: adding a co-signer does not require them to have ETH or pay gas. Co-signers only sign messages off-chain. Gas is paid by the owner when they call addMaintainer(), and by whoever submits the publish() transaction. Being a co-signer is free.


8. The Full Product: Six Layers of Trust

Layer 1 — Multisig publish

M-of-N maintainers must sign the exact content hash before a release reaches npm. Stops compromised tokens, rogue maintainers, and account takeovers.

Layer 2 — Install-time hash verification

3pm install computes the hash of what npm served you and compares it against the on-chain signed record. Catches tampered packages even if the attacker published successfully to npm after the fact.

Layer 3 — Local AI safety scoring ✅ IMPLEMENTED

Two-stage analysis runs on every 3pm install and 3pm approve:

Stage A — Heuristic scan (always, no setup needed): regex patterns detect eval(), child_process, outbound network calls, sensitive env reads, obfuscated payloads. Only flags patterns that are new in the diff vs the previous version — no false positives from long-standing code.

Stage B — Gemma AI analysis (when ollama serve is running): the code diff is sent to gemma3:4b locally. Gemma returns a score (0–100), per-finding flags with severity, and a plain-English summary explaining what it found and why it's dangerous. Combined score = AI × 0.7 + heuristic × 0.3. Code never leaves your machine.

Setup: ollama pull gemma3:4b — then 3pm auto-detects it.

Layer 4 — Chrome extension on npmjs.com ✅ IMPLEMENTED

When browsing npmjs.com/package/react, an overlay shows:

✓ 3pm verified | Signed by 4 maintainers | Safety score: 94/100
Last release: 2h ago | View on-chain →

Passive visibility. Developers researching packages see trust signals before they install anything. Creates social pressure: unverified packages look untrustworthy.

Layer 5 — 3pm audit ✅ IMPLEMENTED (enhanced)

3pm audit
→ Scanning 247 packages (42 direct + 205 transitive, from package-lock.json)
→ ✗ 2 CVEs found  (lodash@4.17.20 GHSA-p6mc-m468-83gw [HIGH], ...)
→ ⚠ 1 suspected typosquat  (expres → "express", distance 1)
→ ✓ 12 packages: on-chain record, hashes match
→ ○ 233 packages: not enrolled in 3pm

Now reads package-lock.json for the full transitive dependency graph (not just direct deps). Runs three checks in parallel: OSV CVE batch lookup, Levenshtein typosquat detection against top 500+ npm packages, and on-chain hash verification. Exits with code 1 if CVEs are found — suitable for CI gating.

Layer 6 — GitHub Action integration

- uses: 3pm/propose-release@v1
  with:
    package: ./
    api-key: ${{ secrets.PM3_API_KEY }}

CI builds the tarball and proposes the release on-chain. Maintainers get notified and run 3pm approve from their terminals. CI has no signing power — it only proposes. Humans authorize.


9. TEE: Trusted Execution Environments

The Trust Gap That Remains

After all the design above, there is still a centralization risk. The backend:

  • Collects signatures from co-signers
  • Validates them
  • Decides when threshold is met
  • Holds the npm automation token
  • Submits the on-chain transaction
  • Triggers npm publish

This is a lot of trust in one server. If the backend is compromised, an attacker could forge the threshold check, skip signature validation, or use the npm token directly. You moved the trust for who signed onto the chain, but the coordinator is still a black box.

TEE closes this gap.

A Trusted Execution Environment (Intel SGX, AMD SEV, AWS Nitro Enclaves) is a hardware-isolated compute environment with two critical properties:

  1. Isolation — code runs in a sealed enclave. The host OS, the cloud provider, and the server operator cannot inspect its memory or tamper with its execution. Not even root access can read the enclave's memory.

  2. Remote attestation — the TEE produces a cryptographic proof: "I am real SGX hardware, running code with hash 0xABC123, initialized with these parameters. Nothing has been modified." Anyone can verify this attestation against Intel/AMD's root certificate.

TEE for the Backend Coordinator

The signature collection logic and relayer run inside a TEE. Before trusting the backend, any user or auditor can verify its attestation:

"The server at this address is running code hash 0xABC,
on real Intel SGX hardware, with the npm token sealed
inside the enclave. The server operator cannot extract it
or modify the threshold logic."

This shifts the trust model from "trust the company running this server" to "verify the cryptographic proof of what is running." That is a fundamental difference. One is a promise. The other is a proof.

The npm automation token stored inside the enclave cannot be extracted even by the server operator. The threshold logic cannot be tampered with. Users do not have to trust 3pm the company — they trust the attestation.

TEE for AI Safety Scoring — Verifiable AI

This is the most novel part of the architecture and worth understanding carefully.

In the base design, the AI model runs locally on the developer's machine. That is good for privacy — your code never leaves your machine. But the safety score is self-reported. There is no proof you actually ran the model, or ran the right model version, or did not manipulate the output.

When the AI scorer runs inside a TEE, something remarkable becomes possible:

The release diff enters the TEE enclave
         ↓
Gemma 4 26B runs inside the enclave
(model weights are sealed, their hash is known)
         ↓
TEE produces a signed attestation:
{
  model:       "gemma-4-26b-it-thinking",
  modelHash:   "0xABC...",       ← proves exact model version
  inputHash:   "0xDEF...",       ← proves which diff was analyzed
  score:        73,
  flags:        ["new_network_call", "reads_process_env"],
  reasoning:   "Version adds fetch() call to external domain...",
  hardware:    "Intel SGX enclave on AWS",
  attestation: "0x..."           ← hardware-signed proof of above
}
         ↓
Attestation stored on-chain alongside the release

Now the safety score is cryptographically verifiable. Anyone can confirm:

  • The exact model version used (a backdoored model would have a different hash)
  • That the diff analyzed corresponds to the release's content hash (cannot run AI on a safe diff and claim it analyzed the malicious one)
  • That the score was not modified after the fact

This is verifiable AI safety scoring — something that does not exist anywhere in the current software supply chain ecosystem. It is a research-level contribution built on top of an engineering system.

The Three Layers of Verification

Layer 1 — On-chain:    "Who signed this release?"
           (immutable, transparent, verifiable by anyone with an RPC)

Layer 2 — TEE:         "Was the coordination logic tampered with?"
           (remote attestation proves the backend ran the expected code)

Layer 3 — TEE + AI:    "Is this release safe?"
           (verifiable inference proves the score is honest and unmanipulated)

Each layer answers a different attack class:

Attack Caught by
Compromised npm publish token Layer 1 — multisig required
Tampered bytes after publish Layer 1 — hash mismatch at install
Compromised backend coordinator Layer 2 — TEE attestation
Subtle malicious diff that maintainers approved Layer 3 — AI scoring
Forged AI safety score Layer 3 — TEE attestation on AI

Practical TEE Options

Platform Notes
Phala Network Web3-native TEE, attestations verifiable on-chain, "Phat Contract" system designed for this use case. Fastest path for a Web3 project.
AWS Nitro Enclaves Mature, well-documented, centralized AWS dependency. Good for getting something working quickly.
Marlin Protocol TEE compute with on-chain attestation verification, more infrastructure-level.
Gramine + Intel SGX Maximum control, runs on bare metal, hardest to set up.

10. Critical Analysis of the Current Implementation

The collaborator's implementation is solid and thinking is aligned with this vision. Here is an honest assessment of what is good and what needs improvement.

What Is Well-Done

Integrity field in EIP-712 — Including integrity (sha512-base64) in the signed payload is smart. Signers cryptographically approve the exact bytes. npm uses SRI format natively so this integrates well with the ecosystem.

Two-phase separation — Propose (untrusted, anyone with API key) and approve (trusted, only registered maintainers) is a clean separation of concerns. CI can trigger proposals; humans control publication.

Nonce-based replay protection — Per-package nonce included in the EIP-712 payload. Simple, robust, prevents old signatures being reused.

EIP-1271 support — Smart contract wallets (Coinbase Smart Wallet, Safe, Argent) work alongside EOAs. This matters for teams using passkey-based wallets.

Backend as coordination middleware — Backend is not the authority. It validates and coordinates, but the smart contract is the source of truth.

What Needs Improvement

The relayer private key is a single point of failure

The backend holds RELAYER_PRIVATE_KEY to submit on-chain transactions. Their own security docs note this but call it "mitigated by rate limits." Rate limits are not a mitigation for key theft. The fix: make the last signer submit the transaction (they already have WalletConnect active) and support permissionless relay for the fallback case.

IPFS is unnecessary complexity

The original design kept Storacha/IPFS. But npm is the distribution layer — the package ends up on npm regardless. IPFS adds another service to depend on, another credential to manage, and another failure mode. Drop it. The integrity hash is the proof. npm stores the bytes.

3pm install — ✅ IMPLEMENTED

Full install-time verification is live. 3pm install <pkg> downloads the tarball, computes the sha512 integrity hash, compares against the on-chain record, and blocks or warns on mismatch. Includes typosquat detection (pre-download), multi-layer heuristic scanning, and optional AI analysis via local Gemma.

The proposer does not sign — ✅ FIXED

The proposer now signs as co-signer #1 automatically in 3pm publish. The proposer's wallet address is permanently attached to every proposal on-chain.

sha256 contentHash is missing

Only sha512-base64 integrity is stored. Adding bytes32 contentHash (sha256) alongside it enables efficient on-chain comparison and aligns with Ethereum's native hash format.


11. The Better Combined Architecture

Taking the best of the current implementation and improving on its gaps:

Registration:
  3pm register my-pkg
    → Publish-based verification proves npm ownership
    → NFT minted to your wallet
  3pm register-token my-pkg
    → Signs ownership proof via WalletConnect
    → Validates npm token against registry.npmjs.org/-/whoami
    → Encrypts and stores token (AES-256-GCM) in DB

  3pm team invite → co-signer runs 3pm team join <code>
  3pm team threshold 2

Publish (proposer is always signer #1):
  3pm publish
    → npm pack → compute sha512 integrity + sha256 contentHash
    → fetch nonce from PackageRegistry.getNonce(name)
    → WalletConnect → MetaMask signs EIP-712:
      { packageName, version, integrity, contentHash, nonce }
    → POST to backend: tarball + proposal + signature_1
    → "1/3 signatures — run `3pm approve <releaseId>`"

Approve (each co-signer):
  3pm approve <releaseId>
    → fetch pending release
    → show: package, version, hashes, proposer, sig count
    → WalletConnect → MetaMask signs same EIP-712 payload
    → POST signature to backend
    → On final signature:
        "Threshold reached. Submit on-chain?"
        → MetaMask sends PackageRegistry.publish() transaction
        → Contract verifies M-of-N, stores release, increments nonce
        → Backend does npm publish from stored tarball

Install:
  3pm install my-pkg@1.2.3
    → download tarball from npm
    → compute integrity + contentHash locally
    → fetch PackageRegistry.getRelease(name, version)
    → compare both hashes
    → [TEE AI] fetch safety attestation from chain
    → all pass → npm install proceeds
    → any fail → warn with cryptographic proof + prompt

12. Hackathon Demo Sequence

The demo is structured as attack-then-defense. Judges need to feel the problem before seeing the solution.

Act 1: The attack (30 seconds)

"This is what happened to event-stream in 2018." Demonstrate: attacker with a stolen npm token runs npm publish with a malicious tarball. It goes through. No friction. No warning. Millions of downstream projects are now vulnerable.

Act 2: Protected publish (45 seconds)

"Same package, protected by 3pm." Demonstrate: 3pm publish → proposer signs in MetaMask → second terminal 3pm approve → co-signer signs → threshold met → MetaMask sends on-chain tx → confirmed → npm publish. The hash is now on-chain, signed by multiple humans, permanent.

Act 3: The attack fails (30 seconds)

"Attacker still has the npm token. They publish a tampered version." Show: attacker does npm publish with modified tarball → succeeds on npm's side. Show: 3pm install my-pkg@1.2.3 → hash computed → MISMATCHREJECTED with message: "The bytes you received do not match what the maintainers signed. Here is cryptographic proof of tampering."

Act 4: AI layer (15 seconds) Show the diff analysis: "New fetch() call to external domain. Reads process.env.AWS_SECRET_KEY. Safety score: 8/100." Even if hashes matched, a local model flagged it.

Act 5: Chrome extension (10 seconds) Open npmjs.com, show the trust badge overlaid on a verified package. Show an unverified package with the warning state. Visual, immediate, no technical knowledge required to understand.


13. Full Command Surface

# Identity
3pm wallet connect              # WalletConnect QR → scan with MetaMask
3pm wallet disconnect           # end session
3pm wallet status               # show connected address and ENS if available

# Package registration
3pm register <pkg-name>         # prove npm ownership → mint NFT → hand over npm token

# Team management
3pm team invite                 # generate invite code for co-signer
3pm team join <code>            # co-signer: connect wallet and join package
3pm team add <ens|addr> ...     # add by ENS / Basename / raw address
3pm team remove <ens|addr>      # remove maintainer
3pm team threshold <n>          # set M-of-N signing requirement
3pm team status                 # show current team, threshold, pending invites

# Publishing
3pm publish                     # pack → hash → sign → propose release
3pm pending                     # list pending releases across all your packages
3pm approve <releaseId>         # review diff + sign pending release
3pm status <releaseId>          # check current signature count and expiry

# Installing and verifying
3pm install <pkg>[@version]     # install with full hash + AI verification
3pm verify <pkg@version>        # verify without installing
3pm audit                       # check all deps in package.json

# Revocation
3pm revoke <pkg@version>        # propose revoking a compromised release (requires M-of-N)

14. Ideas Not Yet Fully Explored

Portable signing files3pm propose > release.sig.json generates a file with the complete pending release and signed payload. Any co-signer can run 3pm sign release.sig.json to sign it. The file can be passed via any channel — Slack, email, committed to a branch. This enables fully offline, async signing with no backend coordination required. The completed file with all signatures can then be submitted on-chain by anyone.

Time-lock with veto window — After M-of-N threshold is met, introduce a 1-hour delay before the on-chain transaction is submitted and before npm publish fires. During this window, any registered maintainer can veto the release. This catches scenarios where enough keys were stolen to reach threshold without the real maintainers noticing, or where social engineering convinced co-signers to approve something malicious.

Signer identity mapping via EAS — Ethereum Attestation Service can map wallet addresses to GitHub usernames. 3pm approve could show "Signed by @sindresorhus, @nicolo-ribaudo, @ljharb" instead of 0x1234..., 0x5678.... Human-readable accountability.

.npmrc hook — make it invisible ✅ IMPLEMENTED — During 3pm init, 3pm prompts to enable automatic verification and appends alias npm='3pm npm-intercept' to ~/.zshrc and/or ~/.bashrc. After reloading the shell, all npm install <pkg> calls are intercepted transparently:

  • For each named package: runs 3pm install <pkg> (typosquat + on-chain hash + Gemma AI), then re-runs npm with the original flags
  • Bare npm install (lockfile restore) and all other npm subcommands: passed through unchanged

The wrapper is implemented as npmInterceptCommand in packages/cli/src/commands/npm-intercept.ts. Users don't need to remember to use 3pm install — it just works after running 3pm init.

Public trust dashboard — A web interface showing: recently verified packages, pending releases, safety scores, revocation history, top verified publishers. Creates social proof. Makes the absence of verification visible.

Farcaster integration — When a release is published and verified on-chain, broadcast to the Farcaster social network. Creates a public, immutable audit trail of what was published and when. The developer community can see and comment on releases.

Version diff in approve ✅ IMPLEMENTED — 3pm approve now fetches both the pending tarball (from backend) and the previous version (from npm), diffs them, runs heuristic + Gemma AI analysis, and shows the full safety report before asking for the signature. Signing is now meaningful: you see exactly what changed and what the AI thinks of it before committing your wallet.


15. Why This Is the Right Architecture

To close, a summary of the key reasoning behind every major decision:

npm is not the problem, trust is. Replacing npm adds enormous adoption friction without addressing the core issue. A trust layer adds safety without requiring anyone to change where they host or install from. Meet developers where they are.

Multisig is the only real defense against account compromise. Single-sig systems (npm tokens, code signing certificates, GitHub Actions provenance) just move the single point of failure. M-of-N means an attacker must simultaneously compromise multiple independent humans. That is a qualitatively different threat model.

Content hashing ties trust to actual bytes, not labels. Version strings are labels that can be reused or spoofed. Signing a hash is signing a commitment to specific bytes. Tamper the bytes and the signature no longer matches — regardless of what the version number says, regardless of what npm's metadata claims.

EIP-712 structured signing makes approvals meaningful. MetaMask shows co-signers exactly what they are approving in human-readable form: package name, version, hash. They are not blindly signing an opaque hex blob.

The proposer must sign. Requiring the initiator to be co-signer #1 creates a cryptographic cost to proposing. Malicious proposals have the proposer's address permanently attached on-chain.

Permissionless relay eliminates the key custody problem. By making publish() callable by anyone with valid signatures, no single entity needs to hold a privileged key. The signatures are the authorization. Gas is the only cost.

Local AI is right for privacy. Cloud-based code scanning sends your proprietary source code to a third party. Local Gemma means the analysis happens on your machine. You get the safety signal without the privacy tradeoff.

TEE makes the backend trustless. Without TEE, you are asking developers to trust that a company is running the code it claims. With TEE and remote attestation, trust becomes verifiable. The coordinator proves what it is doing.

WalletConnect means keys stay in the wallet. A CLI that manages private keys is a target. WalletConnect makes the CLI a stateless coordinator that requests signatures — it never touches the key material.

The combination of these properties — multisig authorization, content hash verification, permissionless relay, publish-based ownership proof, local AI scoring, and TEE attestation — creates a system where trust is not asserted but proven at every step. That is the complete vision.