# Feature Request — Public accessor for the device's identity public key

**To**: Shade SDK team
**From**: Dispatch (browser-based Shade consumer)
**Target**: Shade SDK 4.4.x (or whichever release vehicle fits)
**Priority**: medium — unblocks real per-device fingerprint binding at
enrollment time; consumers ship with placeholder keys until then

Thanks for shipping `@shade/storage-indexeddb` so quickly — that unblocked
Dispatch's Slice 2.5 (persistent browser identity). During integration we
hit one more gap that's worth raising as a separate FR.

---

## Summary

Expose the local device's Ed25519 identity public key as a public accessor
on `Shade`, so applications can hand it to their own backend at enrollment
time for per-device verification, audit, or peer-fingerprint computation.

Today the SDK exposes `myAddress` and `fingerprint`, but the underlying
identity public key — the cryptographic root that everything else binds
to — is reachable only via the private `this.storage.getIdentityKeyPair()`
call inside `Shade`. Consumers building enrollment flows have no way to
hand the real key over.

## Problem

A common pattern in Shade-using apps is:

1. Browser device generates Shade identity (via `createShade`)
2. User enters an enrollment token
3. Browser POSTs to its backend: `{ token, address, identityPublicKey, ... }`
4. Backend records the device, computes/stores a safety number from the
   identity key, opens a Shade session

Step 3 today has nowhere to get a real `identityPublicKey` from. Dispatch
currently sends `crypto.getRandomValues(new Uint8Array(32))` formatted as
hex, with this comment in the source:

```ts
/**
 * The browser submits a hex public-key field at enroll time so the schema
 * stays stable. Wiring this to the SDK-generated identity key requires a
 * Shade SDK addition (no public accessor for the raw identity key today).
 */
export function generatePlaceholderIdentityPublicKey(): string { ... }
```

The backend stores this placeholder but cannot verify anything against it,
because it's not actually the device's key. Real cryptographic binding is
deferred until the SDK exposes the underlying key.

## Why `fingerprint` isn't sufficient

`Shade.fingerprint` returns a 12-groups-of-5-digits safety-number string
designed for human side-channel comparison. That's the right output for a
"compare these digits with your friend" UX, but it's a derived format, not
the raw key. Backends that want to:

- Store the key for later signature verification
- Compute their own safety-number representation (Dispatch uses
  `deterministicSafetyNumber(localAddr, peerAddr)` based on the raw bytes)
- Re-derive the fingerprint after an identity rotation

…all need access to the raw 32-byte Ed25519 public key.

## Proposed API

Add a single async accessor on `Shade`:

```ts
class Shade {
  /**
   * The local device's Ed25519 identity public key (32 bytes).
   *
   * Stable for the lifetime of the identity. After `rotateIdentity()`
   * this returns the new key; the old key is preserved in retired-
   * identities storage for the configured grace period.
   */
  get identityPublicKey(): Promise<Uint8Array>;
}
```

Internally:

```ts
get identityPublicKey(): Promise<Uint8Array> {
  if (!this.initialized) throw new Error('Not initialized');
  return this.storage.getIdentityKeyPair().then((kp) => {
    if (!kp) throw new Error('Identity not yet generated');
    return kp.publicKey;
  });
}
```

If returning a getter that produces a Promise feels off, the equivalent
method form is fine:

```ts
async getIdentityPublicKey(): Promise<Uint8Array> { ... }
```

Either shape works for consumers — pick whichever matches existing SDK
conventions.

## Alternative considered: object accessor

Returning an object would leave room to expose other identity-related
fields later without a breaking change:

```ts
get identity(): Promise<{
  address: string;
  publicKey: Uint8Array;
  fingerprint: string;
  // future: registrationId, createdAt, etc.
}>;
```

This would mildly duplicate `myAddress` + `fingerprint`, but consolidates
identity-related accessors. Not a blocker for shipping the simpler
single-purpose accessor — just flagging the option in case the SDK is
considering broader API ergonomics.

## What this unblocks

For Dispatch specifically:

- Real device identity binding at enrollment (replaces the placeholder
  `crypto.randomUUID()`-derived hex bytes the backend currently stores)
- Server-side `computePeerSafetyNumber()` can use the real key instead of
  the deterministic-from-address stand-in (`shade-identity-provider.ts:151`)
- Future signature-based device authentication (sign a challenge with the
  device's identity key during enrollment) becomes possible without
  another SDK round

For other Shade consumers:

- Any app that hands a device key to its own backend for enrollment —
  multi-device pairing flows, contact verification UIs, push-notification
  targeting — gets an actual key to work with

## Acceptance criteria

1. New accessor exposed on `Shade` (getter or async method, SDK's
   preference)
2. Returns the 32-byte Ed25519 identity public key
3. Returns the **current** key after `rotateIdentity()`
4. Throws (or resolves to a clear error) when called before
   `initialize()` completes
5. Documented alongside `myAddress` and `fingerprint` in the SDK reference
6. One unit test in `shade-sdk` confirming the returned bytes match what
   `storage.getIdentityKeyPair()` holds
7. Mentioned in the changelog under the next release

## Out of scope

- **Private key access** — consumers should never need it; signature
  operations go through `shade.send()` etc. Don't expose the secret half.
- **Cross-peer key lookup** — getting *another* peer's identity key is a
  separate concern (related to peer-verification storage), not what this
  FR is about. This is strictly the local device's own key.
- **Format conversions** — base64/hex/PEM helpers don't belong in the SDK.
  Consumers can encode the raw bytes however their wire format requires.

## Why this can't be done in consumer-land

The identity keypair is generated by `MemoryStorage` / `SQLiteStorage` /
`IndexedDBStorage` and consumed by `ShadeSessionManager`. Consumers can't
reach into either layer without breaching the SDK's encapsulation. A
public accessor is the only path that doesn't require monkey-patching
private fields.
