Back to Envelope opener
SOURCE / PINNED RELEASE

Made of little things.

Envelope opener

Release
eabb9c3e84df…
Author-recorded commit
Not provided
License
Check the original source

The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Envelope opener</title>
    <style>
      :root { color-scheme: dark; font: 16px/1.5 system-ui, sans-serif; background: #111; color: #eee; }
      body { min-height: 100vh; margin: 0; display: grid; place-items: center; padding: 1.5rem; }
      main { width: min(32rem, 100%); display: grid; gap: .9rem; }
      h1 { margin: 0; font-size: 1rem; letter-spacing: .08em; text-transform: uppercase; }
      p { margin: 0; color: #aaa; }
      form { display: grid; gap: .65rem; }
      input, button { padding: .65rem .75rem; border-radius: .3rem; font: inherit; }
      input { background: #1c1c1c; color: #eee; border: 1px solid #555; }
      button { border: 0; background: #b9e6b9; color: #111; cursor: pointer; }
      [hidden] { display: none; }
      [data-state="error"] { color: #ffaaa5; }
    </style>
  </head>
  <body>
    <main>
      <h1>Envelope opener</h1>
      <p id="status" role="status" aria-live="polite">Waiting for launch fragment…</p>
      <form id="unlock" hidden>
        <label for="password">Password</label>
        <input id="password" name="password" type="password" autocomplete="current-password" minlength="8" required>
        <button type="submit">Open encrypted link</button>
      </form>
    </main>
    <script>
      (() => {
        const codec = (function createFragmentDecoder(runtime = globalThis) {
  const publicPrefix = 'nw1.';
  const encryptedPrefix = 'nwe1.';
  const maxFragmentLength = 8_192;
  const maxStateBytes = 4_096;
  const maxPointerLength = 512;
  const iterations = 120_000;
  const pointerPattern = /^(?:naddr|nevent)1[023456789acdefghjklmnpqrstuvwxyz]+$/;

  function decodeFragmentValue(hash = '') {
    if (typeof hash !== 'string') throw new Error('fragment must be a string');
    const raw = hash.startsWith('#') ? hash.slice(1) : hash;
    if (raw === '') return null;
    if (raw.startsWith(encryptedPrefix)) throw new Error('encrypted fragment requires a password');
    if (raw.length > maxFragmentLength) throw new Error(`invalid fragment: maximum is ${maxFragmentLength} characters`);
    if (!raw.startsWith(publicPrefix)) throw new Error('unsupported fragment version');
    try {
      const decoded = JSON.parse(decodeBase64Url(raw.slice(publicPrefix.length)));
      validatePayload(decoded);
      return {
        version: 1,
        pointer: decoded.pointer,
        ...(Object.hasOwn(decoded, 'state') ? { state: decoded.state } : { state: undefined }),
        ...(Object.hasOwn(decoded, 'intent') ? { intent: normalizeIntentValue(decoded.intent) } : {}),
      };
    } catch (error) {
      if (error instanceof Error && /pointer|state|version|payload|intent/.test(error.message)) throw error;
      throw new Error('invalid fragment payload');
    }
  }

  function isEncryptedFragmentValue(hash = '') {
    return typeof hash === 'string'
      && (hash.startsWith(`#${encryptedPrefix}`) || hash.startsWith(encryptedPrefix));
  }

  async function decodeEncryptedFragmentValue(hash, password) {
    if (typeof hash !== 'string') throw new Error('fragment must be a string');
    const raw = hash.startsWith('#') ? hash.slice(1) : hash;
    if (!raw.startsWith(encryptedPrefix)) throw new Error('not an encrypted fragment');
    if (raw.length > maxFragmentLength) throw new Error('invalid encrypted fragment');
    validatePassword(password);
    const parts = raw.slice(encryptedPrefix.length).split('.');
    if (parts.length !== 3) throw new Error('invalid encrypted fragment or password');
    try {
      const salt = decodeBase64UrlBytes(parts[0]);
      const iv = decodeBase64UrlBytes(parts[1]);
      const ciphertext = decodeBase64UrlBytes(parts[2]);
      if (salt.byteLength !== 16 || iv.byteLength !== 12 || ciphertext.byteLength < 17) {
        throw new Error('invalid encrypted fragment');
      }
      const key = await derivePasswordKey(password, salt);
      const plaintext = await getCrypto().subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
      const decoded = JSON.parse(new runtime.TextDecoder().decode(plaintext));
      validatePayload(decoded);
      return {
        version: 1,
        pointer: decoded.pointer,
        ...(Object.hasOwn(decoded, 'state') ? { state: decoded.state } : { state: undefined }),
        ...(Object.hasOwn(decoded, 'intent') ? { intent: normalizeIntentValue(decoded.intent) } : {}),
      };
    } catch {
      throw new Error('invalid encrypted fragment or password');
    }
  }

  function validatePayload(value) {
    if (!isPlainObjectValue(value) || value.v !== 1) throw new Error('invalid fragment payload version');
    validatePointer(value.pointer);
    if (Object.hasOwn(value, 'state')) {
      const stateJson = stringifyJsonValue(value.state);
      if (new runtime.TextEncoder().encode(stateJson).byteLength > maxStateBytes) {
        throw new Error(`launch state is too large; maximum is ${maxStateBytes} bytes`);
      }
    }
    if (Object.hasOwn(value, 'intent')) normalizeIntentValue(value.intent);
  }

  function normalizeIntentValue(value) {
    if (!isPlainObjectValue(value)) throw new Error('launch intent must be an object');
    const archetype = validateIntentSlug(value.archetype, 'archetype');
    const convention = value.convention;
    if (
      typeof convention !== 'string'
      || convention.length > 256
      || !/^napplet:[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/.test(convention)
    ) throw new Error('launch intent convention must be a bounded napplet convention');
    const intent = { archetype, convention };
    if (Object.hasOwn(value, 'action')) intent.action = validateIntentSlug(value.action, 'action');
    if (Object.hasOwn(value, 'payload')) intent.payload = cloneJsonValue(value.payload, 'intent payload');
    if (Object.hasOwn(value, 'behavior')) intent.behavior = normalizeIntentBehavior(value.behavior);
    const serialized = stringifyJsonValue(intent);
    if (new runtime.TextEncoder().encode(serialized).byteLength > maxStateBytes) {
      throw new Error(`launch intent is too large; maximum is ${maxStateBytes} bytes`);
    }
    return intent;
  }

  function validateIntentSlug(value, field) {
    if (typeof value !== 'string' || value.length > 64 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
      throw new Error(`launch intent ${field} must be a lowercase slug`);
    }
    return value;
  }

  function normalizeIntentBehavior(value) {
    if (!isPlainObjectValue(value)) throw new Error('launch intent behavior must be an object');
    const behavior = {};
    for (const field of ['focus', 'newWindow', 'reuse']) {
      if (!Object.hasOwn(value, field)) continue;
      if (typeof value[field] !== 'boolean') throw new Error(`launch intent behavior ${field} must be boolean`);
      behavior[field] = value[field];
    }
    if (Object.keys(value).some((field) => !['focus', 'newWindow', 'reuse'].includes(field))) {
      throw new Error('launch intent behavior contains an unsupported field');
    }
    return behavior;
  }

  function cloneJsonValue(value, label) {
    const serialized = stringifyJsonValue(value);
    if (new runtime.TextEncoder().encode(serialized).byteLength > maxStateBytes) {
      throw new Error(`${label} is too large; maximum is ${maxStateBytes} bytes`);
    }
    return JSON.parse(serialized);
  }

  function validatePointer(pointer) {
    if (typeof pointer !== 'string' || pointer.length === 0 || pointer.length > maxPointerLength) {
      throw new Error('pointer must be a bounded naddr or nevent string');
    }
    if (!pointerPattern.test(pointer)) throw new Error('pointer must be an naddr or nevent NIP-19 string');
  }

  function validatePassword(password) {
    if (typeof password !== 'string' || password.length < 8) throw new Error('password must be at least 8 characters');
  }

  function getCrypto() {
    if (!runtime.crypto?.subtle) throw new Error('Web Crypto API is unavailable');
    return runtime.crypto;
  }

  async function derivePasswordKey(password, salt) {
    const webCrypto = getCrypto();
    const material = await webCrypto.subtle.importKey(
      'raw',
      new runtime.TextEncoder().encode(password),
      'PBKDF2',
      false,
      ['deriveKey'],
    );
    return webCrypto.subtle.deriveKey(
      { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
      material,
      { name: 'AES-GCM', length: 256 },
      false,
      ['decrypt'],
    );
  }

  function decodeBase64Url(value) {
    const bytes = decodeBase64UrlBytes(value);
    return new runtime.TextDecoder().decode(bytes);
  }

  function decodeBase64UrlBytes(value) {
    if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) throw new Error('invalid base64url');
    const padded = value.replaceAll('-', '+').replaceAll('_', '/') + '='.repeat((4 - (value.length % 4)) % 4);
    const binary = runtime.atob(padded);
    return Uint8Array.from(binary, (character) => character.charCodeAt(0));
  }

  function isPlainObjectValue(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
  }

  function stringifyJsonValue(value) {
    let result;
    try {
      result = JSON.stringify(value);
    } catch {
      throw new Error('launch state must be JSON-compatible');
    }
    if (result === undefined) throw new Error('launch state must be JSON-compatible');
    return result;
  }

  return Object.freeze({
    decodeFragment: decodeFragmentValue,
    isEncryptedFragment: isEncryptedFragmentValue,
    decodeEncryptedFragment: decodeEncryptedFragmentValue,
  });
})(globalThis);
        const controller = (function createOpenerNappletController(runtime, codec) {
  const parentWindow = runtime.parent;
  const status = runtime.document.querySelector('#status');
  const unlock = runtime.document.querySelector('#unlock');
  const password = runtime.document.querySelector('#password');
  let shellReady = false;
  let fragment;
  let decoding = false;
  let started = false;
  let requestSequence = 0;
  let pendingLaunch = null;

  const setStatus = (message, state) => {
    status.textContent = message;
    if (state) status.dataset.state = state;
    else delete status.dataset.state;
  };

  const reportError = (error, encrypted = false) => {
    const message = encrypted
      ? 'This encrypted link could not open. Check the password and try again.'
      : 'This link could not open safely.';
    setStatus(message, 'error');
    parentWindow.postMessage({
      type: 'opener.error',
      version: 1,
      error: error instanceof Error ? error.message : String(error),
    }, '*');
  };

  const launch = (request) => {
    pendingLaunch = null;
    if (request === null) {
      setStatus('Choose a napplet to open.', 'empty');
      return;
    }

    const requestId = `launch-${++requestSequence}`;
    pendingLaunch = { requestId, request };
    parentWindow.postMessage({
      type: 'opener.open',
      version: 1,
      requestId,
      pointer: request.pointer,
    }, '*');
    setStatus('Verifying napplet…', 'loading');
  };

  const decodePublic = () => {
    try {
      launch(codec.decodeFragment(fragment));
    } catch (error) {
      reportError(error);
    }
  };

  const consume = () => {
    if (!shellReady || fragment === undefined || decoding) return;
    pendingLaunch = null;
    unlock.hidden = true;
    if (codec.isEncryptedFragment(fragment)) {
      setStatus('Enter the password to decrypt this link.', 'locked');
      unlock.hidden = false;
      password.focus();
      return;
    }
    decodePublic();
  };

  const normalizeContracts = (contracts) => {
    if (!Array.isArray(contracts)) return [];
    return contracts.filter((contract) => (
      contract !== null
      && typeof contract === 'object'
      && typeof contract.archetype === 'string'
      && contract.archetype.length > 0
      && typeof contract.convention === 'string'
      && contract.convention.length > 0
    ));
  };

  const createIntentRequest = (request, contracts) => {
    const advertised = normalizeContracts(contracts);
    const requested = request.intent;
    const contract = requested
      ? advertised.find((candidate) => (
          candidate.archetype === requested.archetype
          && candidate.convention === requested.convention
        ))
      : advertised[0];

    if (!contract) throw new Error('the target does not advertise the requested intent contract');

    const intent = {
      archetype: contract.archetype,
      action: requested?.action ?? 'open',
      convention: contract.convention,
    };
    if (requested && Object.hasOwn(requested, 'payload')) intent.payload = requested.payload;
    else if (request.state !== undefined) intent.payload = request.state;
    if (requested?.behavior !== undefined) intent.behavior = requested.behavior;
    return intent;
  };

  const invokeTarget = async (message) => {
    const pending = pendingLaunch;
    if (
      !pending
      || message.requestId !== pending.requestId
      || message.pointer !== pending.request.pointer
    ) return;

    pendingLaunch = null;
    try {
      const intentApi = runtime.napplet?.intent;
      if (typeof intentApi?.invoke !== 'function') throw new Error('the Paja runtime did not grant NAP-INTENT');
      const result = await intentApi.invoke(createIntentRequest(pending.request, message.contracts));
      if (!result || result.ok !== true || result.handled !== true) {
        throw new Error(result?.error ?? 'the target did not handle the launch intent');
      }
      setStatus('Napplet opened.', 'ready');
    } catch (error) {
      reportError(error);
    }
  };

  const onSubmit = async (event) => {
    event.preventDefault();
    if (decoding || !codec.isEncryptedFragment(fragment)) return;
    decoding = true;
    setStatus('Decrypting link…', 'loading');
    try {
      launch(await codec.decodeEncryptedFragment(fragment, password.value));
      unlock.hidden = true;
    } catch (error) {
      reportError(error, true);
      unlock.hidden = false;
      password.select();
    } finally {
      decoding = false;
    }
  };

  const onMessage = (event) => {
    if (event.source !== parentWindow) return;
    const data = event.data;
    if (!data || typeof data.type !== 'string') return;
    if (data.type === 'shell.init') {
      shellReady = true;
      consume();
      return;
    }
    if (data.type === 'opener.fragment' && typeof data.fragment === 'string') {
      fragment = data.fragment;
      consume();
      return;
    }
    if (data.type === 'opener.target.ready' && data.version === 1) {
      void invokeTarget(data);
      return;
    }
    if (
      data.type === 'opener.target.error'
      && pendingLaunch
      && data.requestId === pendingLaunch.requestId
    ) {
      pendingLaunch = null;
      reportError(new Error(typeof data.message === 'string' ? data.message : 'target failed to open'));
    }
  };

  function start() {
    if (started) return;
    started = true;
    unlock.addEventListener('submit', onSubmit);
    runtime.addEventListener('message', onMessage);
  }

  return Object.freeze({ start });
})(globalThis, codec);
        controller.start();
      })();
    </script>
  </body>
</html>