0.4.0-alpha.61 • Published 4 months ago

thin-hook v0.4.0-alpha.61

Weekly downloads
3
License
BSD-2-Clause
Repository
github
Last release
4 months ago

npm version Bower version

thin-hook

Thin Hook Preprocessor (experimental)

Notes

old namenew namefeature
methodoldMethodscript.js,Class,method
cachedMethodmethodscript.js,Class,method including computed property names
  • Hook Callback Compatibility Since 0.0.149 with #123, the hook callback function has to support new operators for hooking in strict mode. See below for the updated hook callback function hook.__hook__. hook.hookCallbackCompatibilityTest() can detect if the target hook callback function is compatible or not.
  • Opaque URL Authorization Since 0.0.178 with #178, all opaque content URLs must be authorized via hook.parameters.opaque = [ 'opaque_url', ..., (url) => url.match(/opaque_url_pattern/), ... ] configuration.

Native API Access Graph generated via hook callback function (view2 of thin-hook/demo/)

Demo on GitHub Pages

Input

  class C {
    add(a = 1, b = 2) {
      let plus = (x, y) => x + y;
      return plus(a, b);
    }
  } 

Hooked Output

  const __context_mapper__ = $hook$.$(__hook__, [
    'examples/example2.js,C',
    '_p_C;examples/example2.js,C',
    'examples/example2.js,C,add',
    'examples/example2.js,C,add,plus'
  ]);
  $hook$.global(__hook__, __context_mapper__[0], 'C', 'class')[__context_mapper__[1]] = class C {
    add(a, b) {
      return __hook__((a = 1, b = 2) => {
        let plus = (...args) => __hook__((x, y) => x + y, null, args, __context_mapper__[3]);
        return __hook__(plus, null, [
          a,
          b
        ], __context_mapper__[2], 0);
      }, null, arguments, __context_mapper__[2]);
    }
  };

Preprocess

  const hook = require('thin-hook/hook.js');
  let code = fs.readFileSync('src/target.js', 'UTF-8');
  let initialContext = [['src/target.js', {}]];
  let gen = hook(code, '__hook__', initialContext, 'hash');
  fs.writeFileSync('hooked/target.js', gen);
  fs.writeFileSync('hooked/target.js.contexts.json', JSON.stringify(contexts, null, 2));

Context Generator Function (customizable)

  // Built-in Context Generator Function
  hook.contextGenerators.method = function generateMethodContext(astPath) {
    return astPath.map(([ path, node ], index) => node && node.type
      ? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
        ? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
      : index === 0 ? path : '').filter(p => p).join(',');
  }
  // Example Custom Context Generator Function with Hashing
  const hashSalt = '__hash_salt__';
  let contexts = {};

  hook.contextGenerators.hash = function generateHashContext(astPath) {
    const hash = hook.utils.createHash('sha256');
    let hashedInitialContext = astPath[0][0];
    astPath[0][0] = contexts[hashedInitialContext] || astPath[0][0];
    let methodContext = hook.contextGenerators.method(astPath);
    astPath[0][0] = hashedInitialContext;
    hash.update(hashSalt + methodContext);
    let hashContext = hash.digest('hex');
    contexts[hashContext] = methodContext;
    return hashContext;
  }
{
  // Authorization Tickets for no-hook scripts
  // Ticket for this script itself is specified in URL of script tag as
  // hook.min.js?no-hook-authorization={ticket}
  // Note: no-hook-authorization must not exist in learning mode
  let noHookAuthorization = {
    // '*' is for learning mode to detect authorization tickets in 
    //   hook.parameters.noHookAuthorizationPassed,
    //   hook.parameters.noHookAuthorizationFailed
    // JSONs are output to console in the learning mode
    //'*': true,
    "35ae97a3305b863af7eb0ac75c8679233a2a7550e4c3046507fc9ea182c03615": true,
    "16afd3d5aa90cbd026eabcc4f09b1e4207a7042bc1e9be3b36d94415513683ed": true,
    "ae11a06c0ddec9f5b75de82a40745d6d1f92aea1459e8680171c405a5497d1c8": true,
    "5b7ebf7b0b2977d44f47ffa4b19907abbc443feb31c343a6cbbbb033c8deb01a": true,
    "c714633723320be54f106de0c50933c0aeda8ac3fba7c41c97a815ed0e71594c": true,
    "2f43d927664bdfcbcb2cc4e3743652c7eb070057efe7eaf43910426c6eae7e45": true,
    "b397e7c81cca74075d2934070cbbe58f345d3c00ff0bc04dc30b5c67715a572f": true,
    "02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a": true,
    "aebb23ce36eb6f7d597d37727b4e6ee5a57aafc564af2d65309a9597bfd86625": true
  };
  let hidden;
  const passcode = 'XX02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a';
  if (typeof self === 'object' && self.constructor.name === 'ServiceWorkerGlobalScope') {
    // Service Worker
    let reconfigure = false;
    if (hook.parameters.noHookAuthorization) {
      if (Object.getOwnPropertyDescriptor(hook.parameters, 'noHookAuthorization').configurable) {
        reconfigure = true;
      }
    }
    else {
      reconfigure = true;
    }
    if (reconfigure) {
      Object.defineProperty(hook.parameters, 'noHookAuthorization', {
        configurable: false,
        enumerable: true,
        get() {
          return hidden;
        },
        set(value) {
          if (value && value.passcode === passcode) {
            delete value.passcode;
            Object.freeze(value);
            hidden = value;
          }
        }
      });
    }
    noHookAuthorization.passcode = passcode;
    hook.parameters.noHookAuthorization = noHookAuthorization;
  }
  else {
    // Browser Document
    Object.defineProperty(hook.parameters, 'noHookAuthorization', {
      configurable: false,
      enumerable: true,
      writable: false,
      value: Object.freeze(noHookAuthorization)
    });
  }
  if (!noHookAuthorization['*']) {
    Object.seal(hook.parameters.noHookAuthorizationPassed);
  }
}
{
  // source map target filters
  hook.parameters.sourceMap = [
    url => location.origin === url.origin && url.pathname.match(/^\/components\/thin-hook\/demo\//)
  ];
  // hook worker script URL
  hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
}
// Hook worker script (demo/hook-worker.js)
//
// Configuration:
//   hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
importScripts('../hook.min.js?no-hook=true', 'context-generator.js?no-hook=true', 'bootstrap.js?no-hook=true');
onmessage = hook.hookWorkerHandler;
  <!-- Example Custom Context Generator for Service Worker and Browser Document -->
  <script src="bower_components/thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&context-generator-name=method2&fallback-page=index-fb.html&service-worker-ready=true"></script>
  <script context-generator src="custom-context-generator.js?no-hook=true"></script>
  <script context-generator no-hook>
  {
    hook.contextGenerators.method2 = function generateMethodContext2(astPath) {
      return astPath.map(([ path, node ], index) => node && node.type
        ? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
          ? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
        : index === 0 ? path : '').filter(p => p).join(',') +
          (astPath[astPath.length - 1][1].range ? ':' + astPath[astPath.length - 1][1].range[0] + '-' + astPath[astPath.length - 1][1].range[1] : '');
    }
    Object.freeze(hook.contextGenerators);
    // CORS script list
    hook.parameters.cors = [
      'https://raw.githubusercontent.com/t2ym/thin-hook/master/examples/example1.js',
      (url) => { let _url = new URL(url); return _url.hostname !== location.hostname && ['www.gstatic.com'].indexOf(_url.hostname) < 0; }
    ];
    // Authorized opaque URL list
    hook.parameters.opaque = [
      'https://www.gstatic.com/charts/loader.js',
      (url) => {
        let _url = new URL(url);
        return _url.hostname !== location.hostname &&
          _url.href.match(/^(https:\/\/www.gstatic.com|https:\/\/apis.google.com\/js\/api.js|https:\/\/apis.google.com\/_\/)/);
      }
    ];
  }
  </script>

Hook Callback Function (customizable)

  // Built-in Minimal Hook Callback Function without hooking properties (hook-property=false)
  hook.__hook_except_properties__ = function __hook_except_properties__(f, thisArg, args, context, newTarget) {
    return newTarget
      ? Reflect.construct(f, args)
      : thisArg
        ? f.apply(thisArg, args)
        : f(...args);
  }
  // the global object
  const _global = (new Function('return this'))();

  // helper for strict mode
  class StrictModeWrapper {
    static ['#.'](o, p) { return o[p]; }
    static ['#[]'](o, p) { return o[p]; }
    static ['#*'](o) { return o; }
    static ['#in'](o, p) { return p in o; }
    static ['#()'](o, p, a) { return o[p](...a); }
    static ['#p++'](o, p) { return o[p]++; }
    static ['#++p'](o, p) { return ++o[p]; }
    static ['#p--'](o, p) { return o[p]--; }
    static ['#--p'](o, p) { return --o[p]; }
    static ['#delete'](o, p) { return delete o[p]; }
    static ['#='](o, p, v) { return o[p] = v; }
    static ['#+='](o, p, v) { return o[p] += v; }
    static ['#-='](o, p, v) { return o[p] -= v; }
    static ['#*='](o, p, v) { return o[p] *= v; }
    static ['#/='](o, p, v) { return o[p] /= v; }
    static ['#%='](o, p, v) { return o[p] %= v; }
    static ['#**='](o, p, v) { return o[p] **= v; }
    static ['#<<='](o, p, v) { return o[p] <<= v; }
    static ['#>>='](o, p, v) { return o[p] >>= v; }
    static ['#>>>='](o, p, v) { return o[p] >>>= v; }
    static ['#&='](o, p, v) { return o[p] &= v; }
    static ['#^='](o, p, v) { return o[p] ^= v; }
    static ['#|='](o, p, v) { return o[p] |= v; }
    static ['#.='](o, p) { return { set ['='](v) { o[p] = v; }, get ['=']() { return o[p]; } }; }
  }

  // Built-in Minimal Hook Callback Function with hooking properties (hook-property=true) - default
  function __hook__(f, thisArg, args, context, newTarget) {
    let normalizedThisArg = thisArg;
    if (newTarget === false) { // resolve the scope in 'with' statement body
      let varName = args[0];
      let __with__ = thisArg;
      let scope = _global;
      let _scope;
      let i;
      for (i = 0; i < __with__.length; i++) {
        _scope = __with__[i];
        if (Reflect.has(_scope, varName)) {
          if (_scope[Symbol.unscopables] && _scope[Symbol.unscopables][varName]) {
            continue;
          }
          else {
            scope = _scope;
            break;
          }
        }
      }
      thisArg = normalizedThisArg = scope;
    }
    let result;
    let args1 = args[1]; // for '()'
    function * gen() {}
    let GeneratorFunction = gen.constructor;
    switch (f) {
    case Function:
      args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args);
      break;
    case GeneratorFunction:
      args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, true);
      break;
    case '()':
    case '#()':
      switch (thisArg) {
      case Reflect:
        switch (args[0]) {
        case 'construct':
          if (args[1]) {
            switch (args[1][0]) {
            case Function:
              args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
              if (args[1][2]) {
                args1.push(args[1][2]);
              }
              break;
            default:
              if (args[1][0].prototype instanceof Function) {
                args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], args[1][0].prototype instanceof GeneratorFunction)];
                if (args[1][2]) {
                  args1.push(args[1][2]);
                }
              }
              break;
            }
          }
          break;
        case 'apply':
          if (args[1]) {
            switch (args[1][0]) {
            case Function:
              args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2])];
              break;
            case GeneratorFunction:
              args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], true)];
              break;
            default:
              if (args[1][0].prototype instanceof Function) {
                args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], args[1][0].prototype instanceof GeneratorFunction)];
              }
              break;
            }
          }
          break;
        default:
          break;
        }
        break;
      case Function:
        switch (args[0]) {
        case 'apply':
          args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
          break;
        case 'call':
          args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1))];
          break;
        default:
          break;
        }
        break;
      case GeneratorFunction:
        switch (args[0]) {
        case 'apply':
          args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], true)];
          break;
        case 'call':
          args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1), true)];
          break;
        default:
          break;
        }
        break;
      default:
        if (thisArg instanceof GeneratorFunction && args[0] === 'constructor') {
          args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1], true);
        }
        else if (thisArg instanceof Function && args[0] === 'constructor') {
          args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1]);
        }
        break;
      }
      break;
    default:
      if (typeof f === 'function') {
        if (f.prototype instanceof Function && newTarget) {
          args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, f.prototype instanceof GeneratorFunction);
        }
        else if (newTarget === '') {
          if (args[0] && Object.getPrototypeOf(args[0]) === Function) {
            args = [ args[0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args.slice(1)) ];
          }
        }
      }
      break;
    }
    if (typeof f !== 'string') {
      result = newTarget
        ? Reflect.construct(f, args)
        : thisArg
          ? f.apply(thisArg, args)
          : f(...args);
    }
    else {
      // property access
      switch (f) {
      // getter
      case '.':
      case '[]':
        result = thisArg[args[0]];
        break;
      // enumeration
      case '*':
        result = thisArg;
        break;
      // property existence
      case 'in':
        result = args[0] in thisArg;
        break;
      // funcation call
      case '()':
        result = thisArg[args[0]](...args1);
        break;
      // unary operators
      case 'p++':
        result = thisArg[args[0]]++;
        break;
      case '++p':
        result = ++thisArg[args[0]];
        break;
      case 'p--':
        result = thisArg[args[0]]--;
        break;
      case '--p':
        result = --thisArg[args[0]];
        break;
      case 'delete':
        result = delete thisArg[args[0]];
        break;
      // assignment operators
      case '=':
        result = thisArg[args[0]] = args[1];
        break;
      case '+=':
        result = thisArg[args[0]] += args[1];
        break;
      case '-=':
        result = thisArg[args[0]] -= args[1];
        break;
      case '*=':
        result = thisArg[args[0]] *= args[1];
        break;
      case '/=':
        result = thisArg[args[0]] /= args[1];
        break;
      case '%=':
        result = thisArg[args[0]] %= args[1];
        break;
      case '**=':
        result = thisArg[args[0]] **= args[1];
        break;
      case '<<=':
        result = thisArg[args[0]] <<= args[1];
        break;
      case '>>=':
        result = thisArg[args[0]] >>= args[1];
        break;
      case '>>>=':
        result = thisArg[args[0]] >>>= args[1];
        break;
      case '&=':
        result = thisArg[args[0]] &= args[1];
        break;
      case '^=':
        result = thisArg[args[0]] ^= args[1];
        break;
      case '|=':
        result = thisArg[args[0]] |= args[1];
        break;
      // LHS property access
      case '.=':
        result = { set ['='](v) { thisArg[args[0]] = v; }, get ['=']() { return thisArg[args[0]]; } };
        break;
      // strict mode operators prefixed with '#'
      // getter
      case '#.':
      case '#[]':
        result = StrictModeWrapper['#.'](thisArg, args[0]);
        break;
      // enumeration
      case '#*':
        result = StrictModeWrapper['#*'](thisArg);
        break;
      // property existence
      case '#in':
        result = StrictModeWrapper['#in'](thisArg, args[0]);
        break;
      // funcation call
      case '#()':
        result = StrictModeWrapper['#()'](thisArg, args[0], args1);
        break;
      // unary operators
      case '#p++':
        result = StrictModeWrapper['#p++'](thisArg, args[0]);
        break;
      case '#++p':
        result = StrictModeWrapper['#++p'](thisArg, args[0]);
        break;
      case '#p--':
        result = StrictModeWrapper['#p--'](thisArg, args[0]);
        break;
      case '#--p':
        result = StrictModeWrapper['#--p'](thisArg, args[0]);
        break;
      case '#delete':
        result = StrictModeWrapper['#delete'](thisArg, args[0]);
        break;
      // assignment operators
      case '#=':
        result = StrictModeWrapper['#='](thisArg, args[0], args[1]);
        break;
      case '#+=':
        result = StrictModeWrapper['#+='](thisArg, args[0], args[1]);
        break;
      case '#-=':
        result = StrictModeWrapper['#-='](thisArg, args[0], args[1]);
        break;
      case '#*=':
        result = StrictModeWrapper['#*='](thisArg, args[0], args[1]);
        break;
      case '#/=':
        result = StrictModeWrapper['#/='](thisArg, args[0], args[1]);
        break;
      case '#%=':
        result = StrictModeWrapper['#%='](thisArg, args[0], args[1]);
        break;
      case '#**=':
        result = StrictModeWrapper['#**='](thisArg, args[0], args[1]);
        break;
      case '#<<=':
        result = StrictModeWrapper['#<<='](thisArg, args[0], args[1]);
        break;
      case '#>>=':
        result = StrictModeWrapper['#>>='](thisArg, args[0], args[1]);
        break;
      case '#>>>=':
        result = StrictModeWrapper['#>>>='](thisArg, args[0], args[1]);
        break;
      case '#&=':
        result = StrictModeWrapper['#&='](thisArg, args[0], args[1]);
        break;
      case '#^=':
        result = StrictModeWrapper['#^='](thisArg, args[0], args[1]);
        break;
      case '#|=':
        result = StrictModeWrapper['#|='](thisArg, args[0], args[1]);
        break;
      // LHS property access
      case '#.=':
        result = StrictModeWrapper['#.='](thisArg, args[0]);
        break;
      // getter for super
      case 's.':
      case 's[]':
        result = args[1](args[0]);
        break;
      // super method call
      case 's()':
        result = args[2](args[0]).apply(thisArg, args[1]);
        break;
      // unary operators for super
      case 's++':
      case '++s':
      case 's--':
      case '--s':
        result = args[1].apply(thisArg, args);
        break;
      // assignment operators for super
      case 's=':
      case 's+=':
      case 's-=':
      case 's*=':
      case 's/=':
      case 's%=':
      case 's**=':
      case 's<<=':
      case 's>>=':
      case 's>>>=':
      case 's&=':
      case 's^=':
      case 's|=':
        result = args[2].apply(thisArg, args);
        break;
      // getter in 'with' statement body
      case 'w.':
      case 'w[]':
        result = args[1]();
        break;
      // function call in 'with' statement body
      case 'w()':
        result = args[2](...args[1]);
        break;
      // constructor call in 'with' statement body
      case 'wnew':
        result = args[2](...args[1]);
        break;
      // unary operators in 'with' statement body
      case 'w++':
      case '++w':
      case 'w--':
      case '--w':
        result = args[1]();
        break;
      // unary operators in 'with' statement body
      case 'wtypeof':
      case 'wdelete':
        result = args[1]();
        break;
      // LHS value in 'with' statement body (__hook__('w.=', __with__, ['p', { set ['='](v) { p = v } } ], 'context', false)['='])
      case 'w.=':
        result = args[1];
        break;
      // assignment operators in 'with' statement body
      case 'w=':
      case 'w+=':
      case 'w-=':
      case 'w*=':
      case 'w/=':
      case 'w%=':
      case 'w**=':
      case 'w<<=':
      case 'w>>=':
      case 'w>>>=':
      case 'w&=':
      case 'w^=':
      case 'w|=':
        result = args[2](args[1]);
        break;
      // default (invalid operator)
      default:
        f(); // throw TypeError: f is not a function
        result = null;
        break;
      }
    }
    return result;
  }
  // Example Hook Callback Function with Primitive Access Control
  hashContext = { 'hash': 'context', ... }; // Generated from hook.preprocess initialContext[0][1]
  trustedContext = { 'context': /trustedModules/, ... }; // Access Policies

  window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
    console.log('hook:', context, args);
    if (!hashContext[context] ||
        !trustedContext[hashContext[context]] ||
        !(new Error('').stack.match(trustedContext[hashContext[context]]))) {
      // plus check thisArg, args, etc.
      throw new Error('Permission Denied');
    }
    return newTarget
      ? Reflect.construct(f, args)
      : thisArg
        ? f.apply(thisArg, args)
        : f(...args);
  }

Entry HTML with Service Worker

If hooking is performed run-time in Service Worker, the entry HTML page must be loaded via Service Worker so that no hook-targeted scripts are evaluated without hooking.

To achieve this, the static entry HTML has to be Encoded at build time by hook.serviceWorkerTransformers.encodeHTML(html).

Hook CLI to encode the entry HTML

  # encode src/index.html to dist/index.html
  hook --out dist/index.html src/index.html

Decoded/Original HTML (source code)

<html>
  <head>
    <script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=true"></script>
    <!-- Hook Callback Function witout hooking properties -->
    <script no-hook>
      window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
        ...
        return newTarget
          ? Reflect.construct(f, args)
          : thisArg
            ? f.apply(thisArg, args)
            : f(...args);
      }
    </script><!-- end of mandatory no-hook scripts -->
    <!-- comment --->
    <script src="..."></script>
    ...
</html>

Encoded HTML (Service Worker converts it to Decoded HTML)

<html>
  <head>
    <script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=false"></script></head></html>
    <!-- Hook Callback Function without hooking properties -->
    <script no-hook>
      window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
        ...
        return newTarget
          ? Reflect.construct(f, args)
          : thisArg
            ? f.apply(thisArg, args)
            : f(...args);
      }
    </script><!--<C!-- end of mandatory no-hook scripts --C>
    <C!-- comment --C>
    <script src="..."></script>
    ...
</html>-->
  • </head></html> is inserted between the first hook.min.js script and the second no-hook script, which looks strange but is required for correct execution of no-hook scripts.
    • If </head></html> is inserted at the end of mandatory no-hook scripts according to the normal HTML format, the page encounters the unexpected "hook is not defined" error, whose root cause is under investigation.

Supported Syntax

  • Functions
  • Object Shorthand Methods ({ m() {} })
  • ES6 Classes (constructor, super, this, new)
  • ES6 Modules (import, export);
  • Expressions in Template Literals(`${(v => v * v)(x)}`)
  • Generator Functions (function *g() { yield X })
  • Arrow Functions (a => a, a => { return a; }, a => ({ p: a }))
  • Async Functions (async function f() {}, async method() {}, async () => {})
  • Default Parameters for Functions/Methods/Arrow Functions
  • Default Parameters with Destructuring (function f([ a = 1 ], { b = 2, x: c = 3 }) {})
  • Property Accessors (o.p, o['p'], o.p())

Install

Browsers

  bower install --save thin-hook

NodeJS

  npm install --save thin-hook

Import

Browsers

  <!-- browserified along with espree and escodegen; minified -->
  <script src="path/to/bower_components/thin-hook/hook.min.js"></script>

NodeJS

  const hook = require('thin-hook/hook.js');

API (Tentative)

  • hook(code: string, hookName: string = '__hook__', initialContext: Array = [], contextGeneratorName: string = 'method', metaHooking: boolean = true, hookProperty: boolean = true, sourceMap: object = null, asynchronous: boolean = false, compact: boolean = false, hookGlobal: boolean = true, hookPrefix: string = '_p_', initialScope: object = null)
    • code: input JavaScript as string
    • hookName: name of hook callback function
    • initialContext: typically [ ['script.js', {}] ]
    • contextGeneratorName: function property name in hook.contextGenerators
      • argument astPath = [ ['script.js', {}], ['root', rootAst], ['body', bodyAst], ..., [0, FunctionExpressionAst] ]
    • metaHooking: Enable meta hooking (run-time hooking of metaprogramming) if true
    • hookProperty: Enable hooking of object property accessors and new operators if true
    • sourceMap: Source map parameter in an object. { pathname: 'path/to/script_source.js'} Default: null
    • asynchronous: Return a Promise if true. Default: false
    • compact: Generate compact code if true. Default: false
      • Note: sourceMap is disabled when compact is true
    • hookGlobal: Hook global variable access. Must be enabled with hookProperty. Default: true
    • hookPrefix: Prefix for hook.global()._p_GlobalVariable proxy accessors. Default: _p_
      • Note: hook.global() return the global object with get/set accessors for the prefixed name
    • initialScope: Initial scope object ({ vname: true, ... }) for hooked eval scripts. Default: null
  • $hook$: $hook$ === hook. Alias of hook in hooked scripts
  • hook.hookHtml(html: string, hookName, url, cors, contextGenerator, contextGeneratorScripts, isDecoded, metaHooking = true, scriptOffset = 0, _hookProperty = true, asynchronous = false)
  • hook.__hook__(f: function or string, thisArg: object, args: Array, context: string, newTarget: new.target meta property)
    • minimal hook callback function with property hooking
    • f:
      • function: target function to hook
      • string: property operation to hook
        • .: get property (o.prop)
        • *: iterate over (for (p in o), for (p of o))
        • in: property existence ('p' in o)
        • (): function call (o.func())
        • =, +=, ...: assignment operation (o.prop = value)
        • p++, ++p, p--, --p: postfixed/prefixed increment/decrement operation (o.prop++)
        • delete: delete operation (delete o.prop)
        • s.: get property of super (super.prop)
        • s(): call super method (super.method())
        • s=, s+=, ...: assignment operation for super (super.prop = value)
        • s++, ++s, s--, --s: postfixed/prefixed increment/decrement operation for super (super.prop++)
        • w., w=, w(), w++, ...: operations on variables in within with statements
    • thisArg: this object for the function or the operation
    • args:
      • arguments for the function
      • [ property ] for property access operations
      • [ property, value ] for property assignment operations
      • [ property, [...args] ] for function call operations
    • context: context in the script
    • newTarget: new.target meta property for constructor calls;
      • true for new calls
      • Falsy values for non-new operations for faster detection of the operations
        • false for with statement calls
        • 0 for function calls
        • undefined for other calls
  • hook.__hook_except_properties__(f, thisArg, args, context, newTarget)
    • minimal hook callback function without property hooking
  • hook.hookCallbackCompatibilityTest(__hook__ = window[hookName], throwError = true, checkTypeError = true)
    • run-time test suite for hook callback function
    • Usage: window.__hook__ = function __hook__ (...) {}; hook.hookCallbackCompatibilityTest();
    • An error is thrown on compatibility test failure.
    • false is returned on a test failure if throwError = false
    • tests on non-callable object's function call are skipped if checkTypeError = false
  • hook.contextGenerators: object. Context Generator Functions
    • null(): context as ''
    • astPath(astPath: Array): context as 'script.js,[root]Program,body,astType,...'
    • method(astPath: Array): context as 'script.js,Class,Method' with caching, including computed method variable name
    • cachedMethod(astPath: Array): alias for method
    • cachedMethodDebug(astPath: Array): context as 'script.js,Class,Method', comparing contexts with those by "oldMethod" in console.warn() messages
    • oldMethod(astPath: Array): context as 'script.js,Class,Method' for compatibility
    • custom context generator function has to be added to this object with its unique contextGeneratorName
  • hook.$(symbolToContext = __hook__, contexts): context symbol generator function used in hooked scripts to generate symbols corresponding to given contexts
    • Example call inserted at the beginning of a hooked script: const __context_mapper__ = $hook$.$(__hook__, [ 'examples/example2.js,C', ... ]);
    • __context_mapper__: Array of symbol contexts
      • In a hooked script, __context_mapper__ is actually __ + hex(sha256(topContextOfScript + code)) + __
        • Note: Due to this specification, the same script in the same URL cannot be loaded to a single document multiple times
      • __context_mapper__[N]: the symbol context corresponding to the string context contexts[N]
      • __hook__[__context_mapper__[N]] is set as contexts[N] so that __hook__ can convert symbol contexts to their corresponding string contexts
  • Hooked Native APIs: Automatically applied in hook() preprocessing
    • hook.global(hookCallback: function = hookName, context: string, name: string, type: string)._p_name: hooked global variable accessor when hookGlobal is true
      • type: one of 'var', 'function', 'let', 'const', 'class', 'get', 'set', 'delete', 'typeof'
    • hook.Function(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName): hooked Function constructor for use in hook callback function __hook__
      • Usage: (new (hook.Function('__hook__', [['window,Function', {}]], 'method'))('return function f() {}'))()
      • Notes:
        • Avoid replacing the native API window.Function for better transparency (now commented out in the demo/hook-native-api.js)
        • NOT automatically applied in the hooking
        • Applied in the hook callback function (__hook__) instead
    • hook.FunctionArguments(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName = 'method', args, isGenerator = false): generate hooked Function arguments to hand to Function constructor for use in hook callback function __hook__
      • Usage: hook.FunctionArguments('__hook__', [['window,Function', {}]], 'method', ['return function f() {}'])
      • Returns hooked args in a cloned Array
    • hook.eval(hookName, initialContext: Array = [['eval', {}]], contextGeneratorName): hooked eval function
      • Usage: hook.eval('__hook__', [['eval', {}]], 'method'))('1 + 2', (script, eval) => eval(script))
      • Note: In no-hook scripts with the hooked global eval function via hook.hook(hook.eval(...)), the evaluation is bound to the global scope unless the wrapper arrow function (script, eval) => eval(script) is defined in the local scope and specifed as the second argument of each eval() call
    • hook.setTimeout(hookName, initialContext: Array = [['setTimeout', {}]], contextGeneratorName): hooked setTimeout function
      • Note: Not automatically applied if the first argument is an (arrow) function expression
    • hook.setInterval(hookName, initialContext: Array = [['setInterval', {}]], contextGeneratorName): hooked setInterval function
      • Note: Not automatically applied if the first argument is an (arrow) function expression
    • hook.Node(hookName, initialContext: Array = [['Node', {}]], contextGeneratorName): hook textContent property
      • set textContent: hooked with context 'ClassName,set textContent'
    • hook.Element(hookName, initialContext: Array = [['Element', {}]], contextGeneratorName): hook setAttribute function
      • setAttribute('onXX', '{script in attribute}'): Script in onXX handler attribute is hooked
      • setAttribute('href', 'javascript:{script in URL}'): Script in URL "javascript:{script in URL}" is hooked
    • hook.HTMLScriptElement(hookName, initialContext: Array = [['HTMLScriptElement', {}]], contextGeneratorName): HTMLScriptElement with hooked properties
      • Note: Applied only at run time. Not applied in preprocessing. HTMLScriptElement class is the same object as the native one. hook.Node and hook.Element are called internally.
      • set textContent: Script in textContent is hooked if type is a JavaScript MIME type. Node.textContent is hooked as well.
        • Note: Scripts set by innerHTML/outerHTML/text properties are NOT executed, while text should be executed according to the standards.
      • set type: Script in this.textContent is hooked if type is a JavaScript MIME type.
      • setAttribute('type', mimeType): Script in this.textContent is hooked if mimeType is a JavaScript MIME type. Element.setAttribute is hooked as well.
    • hook.HTMLAnchorElement(hookName, initialContext: Array = [['HTMLAnchorElement', {}]]), contextGeneratorName): HTMLAnchorElement with hooked href property
      • set href: Script in URL "javascript:{script in URL}" is hooked
    • hook.HTMLAreaElement(hookName, initialContext: Array = [['HTMLAreaElement', {}]]), contextGeneratorName): HTMLAreaElement with hooked href property
      • set href: Script in URL "javascript:{script in URL}" is hooked
    • hook.Document(hookName, initialContext: Array = [['Document', {}]], contextGeneratorName): hook write function
      • write('<sc' + 'ript>{script in string}</sc' + 'ript>'): Script in HTML fragment is hooked
    • hook.with(scope: Object, ...scopes: Array of Object): Hook with statement scope object
      • with (hook.with(obj, { v1: true, v2: true, ...})) {}
    • hook.importScripts(): return hooked importScripts function for Workers, invalidating extensions other than .js and .mjs
      • Note: No arguments to pass
  • hook.hook(target: Class, ...): hook platform global object with target
    • Usage: ['Function','setTimeout','setInterval',...].forEach(name => hook.hook(hook.Function('__hook__', [[name, {}]], 'method'))
  • hook.serviceWorkerHandlers: Service Worker event handlers
    • install: 'install' event handler. Set version from the version parameter
    • activate: 'activate' event handler. Clear caches of old versions.
    • message: 'message' event handler.
      • INTERNAL 'channel' message: Transfer MessageChannel port objects for hook workers from the main document to the Service Worker at initialization
      • INTERNAL 'unload' message: Trigger unloading of hook workers
      • INTERNAL 'coverage' message: Transfer __coverage__ instanbul coverage object for the Service Worker to the main document to collect code coverage in test/hook.min.js
      • ['plugin', 'pluginId', ...params ] message: Transfer a message to the target plugin identified by 'pluginId'. The target plugin must add its own event listener to handle the message.
        • ['plugin', 'pluginId:enqueue', ...params ]: When the pluginId ends with :enqueue, events with posted messages are enqueued to hook.parameters.messageQueues['pluginId:enqueue'] = [] even before plugins are loaded into the Service Worker
          • Each enqueued message is immediately responded via event.ports[0].postMessage() with a dummy response message generated by cloning the posted message and appending ':enqueued' such as ['plugin', 'pluginId:enqueue', ...params, ':enqueued' ]
          • The target plugin must dequeue the enqueued events and append ':dequeued' to the queue to stop further enqueueing. For example, the queue [] changes as follows:
            • An event is enqueued: [ event1 ]
            • The plugin append ':dequeued': [ event1, ':dequeued' ]
            • The plugin dequeues and processes the event(s): [ ':dequeued' ]
          • Enqueued messages are likely to be one-way messages as the main document is about to reload itself
          • hook.parameters.messageQueues['pluginId:enqueue'] may NOT exist when the plugin is loaded. So the plugin must create its own queue if it has not been created.
    • fetch: 'fetch' event handler. Cache hooked JavaScripts and HTMLs except for the main page loading hook.min.js
      • <script src="thin-hook/hook.min.js?version=1&sw-root=/&no-hook=true&hook-name=__hook__&discard-hook-errors=true&fallback-page=index-no-sw.html&hook-property=true&service-worker-ready=true"></script>: arguments from the page
        • version: default 1. Service Worker cache version. Old caches are flushed when the version is changed in the main page and reloaded. Service Worker is updated when the controlled page is detached after the reloading.
        • sw-root: optional. Set Service Worker scope
        • hook-name: default __hook__. hook callback function name
        • context-generator-name: default method. context generator callback function name
        • discard-hook-errors: true if errors in hooking are ignored and the original contents are provided. Default: true
        • fallback-page: fallback page to land if Service Worker is not available in the browser
        • no-hook-authorization: Optional. CSV of no-hook authorization tickets for no-hook scripts. Typically for ticket of no-hook authorization script itself.
          • The values are stored in hook.parameters.noHookAuthorizationPreValidated object in Service Worker
          • Add the value log-no-hook-authorization to log authorization in console
          • Note: no-hook-authorization must not exist in learning mode with hook.parameters.noHookAuthorization['*'] === true
            • Steps to update authorized no-hook scripts:
                1. Let no-hook be "learning mode" by truthy hook.parameters.noHookAuthorization['*']
                1. Remove (or temporarily rename) no-hook-authorization parameter from hook.min.js
                1. Update no-hook script(s)
                1. Clear Service Worker(s)
                1. Update version parameter for hook.min.js
                1. Check "Preserve Logs" option in debugger console
                1. Reload the page(s) with no-hook script(s)
                1. Copy and Paste values of hook.parameters.noHookAuthorizationPassed from both browser document and Service Worker to no-hook authorization script
                1. Disable "learning mode"
                1. Enable (or revive) no-hook-authorization parameter for hook.min.js with a dummy value
                1. Clear Service Worker(s)
                1. Update version parameter for hook.min.js
                1. Reload the page(s) with no-hook scripts(s)
                1. Copy and Paste the ticket for the no-hook authorization script into the no-hook-authorization parameter
                1. Update version parameter for hook.min.js
                1. Clear Service Worker(s)
                1. Reload the page(s) with no-hook script(s)
                1. Check if there are no unauthorized no-hook scripts
0.4.0-alpha.61

4 months ago

0.4.0-alpha.60

4 months ago

0.4.0-alpha.58

10 months ago

0.4.0-alpha.59

9 months ago

0.4.0-alpha.54

10 months ago

0.4.0-alpha.55

10 months ago

0.4.0-alpha.56

10 months ago

0.4.0-alpha.57

10 months ago

0.4.0-alpha.52

10 months ago

0.4.0-alpha.53

10 months ago

0.4.0-alpha.51

2 years ago

0.4.0-alpha.50

2 years ago

0.4.0-alpha.49

3 years ago

0.4.0-alpha.48

4 years ago

0.4.0-alpha.47

4 years ago

0.4.0-alpha.46

4 years ago

0.4.0-alpha.45

4 years ago

0.4.0-alpha.44

4 years ago

0.4.0-alpha.43

4 years ago

0.4.0-alpha.42

4 years ago

0.4.0-alpha.41

4 years ago

0.4.0-alpha.40

4 years ago

0.4.0-alpha.39

4 years ago

0.4.0-alpha.38

4 years ago

0.4.0-alpha.37

4 years ago

0.4.0-alpha.36

4 years ago

0.4.0-alpha.35

4 years ago

0.4.0-alpha.34

4 years ago

0.4.0-alpha.32

4 years ago

0.4.0-alpha.33

4 years ago

0.4.0-alpha.31

4 years ago

0.4.0-alpha.30

4 years ago

0.4.0-alpha.29

4 years ago

0.4.0-alpha.28

4 years ago

0.4.0-alpha.27

4 years ago

0.4.0-alpha.26

4 years ago

0.4.0-alpha.25

4 years ago

0.4.0-alpha.24

4 years ago

0.4.0-alpha.23

4 years ago

0.4.0-alpha.21

4 years ago

0.4.0-alpha.20

4 years ago

0.4.0-alpha.19

4 years ago

0.4.0-alpha.18

4 years ago

0.4.0-alpha.17

4 years ago

0.4.0-alpha.15

4 years ago

0.4.0-alpha.16

4 years ago

0.4.0-alpha.14

4 years ago

0.4.0-alpha.13

4 years ago

0.4.0-alpha.12

4 years ago

0.4.0-alpha.11

4 years ago

0.4.0-alpha.10

4 years ago

0.4.0-alpha.9

4 years ago

0.4.0-alpha.8

4 years ago

0.4.0-alpha.7

4 years ago

0.4.0-alpha.6

4 years ago

0.4.0-alpha.5

4 years ago

0.4.0-alpha.4

4 years ago

0.4.0-alpha.3

4 years ago

0.4.0-alpha.2

4 years ago

0.4.0-alpha.1

5 years ago

0.3.7

5 years ago

0.3.6

5 years ago

0.3.5

5 years ago

0.3.4

5 years ago

0.3.3

5 years ago

0.3.2

5 years ago

0.3.1

5 years ago

0.3.0

5 years ago

0.2.6

5 years ago

0.2.5

5 years ago

0.2.4

5 years ago

0.2.3

6 years ago

0.2.2

6 years ago

0.2.1

6 years ago

0.2.0

6 years ago

0.1.13-stack.21

6 years ago

0.1.13-stack.20

6 years ago

0.1.13-stack.19

6 years ago

0.1.13-stack.18

6 years ago

0.1.13-stack.17

6 years ago

0.1.13-stack.16

6 years ago

0.1.13-stack.15

6 years ago

0.1.13-stack.14

6 years ago

0.1.13-stack.13

6 years ago

0.1.13-stack.12

6 years ago

0.1.13-stack.11

6 years ago

0.1.13-stack.10

6 years ago

0.1.13-stack.9

6 years ago

0.1.13-stack.8

6 years ago

0.1.13-stack.6

6 years ago

0.1.13-stack.5

6 years ago

0.1.13-stack.4

6 years ago

0.1.13-stack.3

6 years ago

0.1.13-stack.2

6 years ago

0.1.13-stack.1

6 years ago

0.1.13

6 years ago

0.1.12

6 years ago

0.1.11

6 years ago

0.1.10

6 years ago

0.1.9

6 years ago

0.1.8

6 years ago

0.1.7

6 years ago

0.1.6

6 years ago

0.1.5

6 years ago

0.1.4

6 years ago

0.1.3

6 years ago

0.1.2

6 years ago

0.1.1

6 years ago

0.1.0

6 years ago

0.0.257

6 years ago

0.0.256

6 years ago

0.0.255

6 years ago

0.0.254

6 years ago

0.0.253

6 years ago

0.0.252

6 years ago

0.0.251

6 years ago

0.0.250

6 years ago

0.0.249

6 years ago

0.0.248

6 years ago

0.0.247

6 years ago

0.0.246

6 years ago

0.0.245

6 years ago

0.0.244

6 years ago

0.0.243

6 years ago

0.0.242

6 years ago

0.0.241

6 years ago

0.0.240

6 years ago

0.0.239

6 years ago

0.0.238

6 years ago

0.0.237

6 years ago

0.0.236

6 years ago

0.0.235

6 years ago

0.0.234

6 years ago

0.0.233

6 years ago

0.0.232

6 years ago

0.0.231

6 years ago

0.0.230

6 years ago

0.0.229

6 years ago

0.0.228

6 years ago

0.0.227

6 years ago

0.0.226

6 years ago

0.0.225

6 years ago

0.0.224

6 years ago

0.0.223

6 years ago

0.0.222

6 years ago

0.0.221

6 years ago

0.0.220

6 years ago

0.0.219

6 years ago

0.0.218

6 years ago

0.0.217

6 years ago

0.0.216

6 years ago

0.0.215

6 years ago

0.0.214

6 years ago

0.0.213

6 years ago

0.0.212

6 years ago

0.0.211

6 years ago

0.0.210

6 years ago

0.0.209

6 years ago

0.0.208

6 years ago

0.0.207

6 years ago

0.0.206

6 years ago

0.0.205

6 years ago

0.0.204

6 years ago

0.0.203

6 years ago

0.0.202

6 years ago

0.0.201

6 years ago

0.0.200

6 years ago

0.0.199

6 years ago

0.0.198

6 years ago

0.0.197

6 years ago

0.0.196

6 years ago

0.0.195

6 years ago

0.0.194

6 years ago

0.0.193

6 years ago

0.0.192

6 years ago

0.0.191

6 years ago

0.0.190

6 years ago

0.0.189

6 years ago

0.0.188

6 years ago

0.0.187

6 years ago

0.0.186

6 years ago

0.0.185

6 years ago

0.0.184

6 years ago

0.0.183

6 years ago

0.0.182

6 years ago

0.0.181

6 years ago

0.0.180

6 years ago

0.0.179

6 years ago

0.0.178

6 years ago

0.0.177

6 years ago

0.0.176

6 years ago

0.0.175

6 years ago

0.0.174

6 years ago

0.0.173

6 years ago

0.0.172

6 years ago

0.0.171

7 years ago

0.0.170

7 years ago

0.0.169

7 years ago

0.0.168

7 years ago

0.0.167

7 years ago

0.0.166

7 years ago

0.0.165

7 years ago

0.0.164

7 years ago

0.0.163

7 years ago

0.0.162

7 years ago

0.0.161

7 years ago

0.0.160

7 years ago

0.0.159

7 years ago

0.0.158

7 years ago

0.0.157

7 years ago

0.0.156

7 years ago

0.0.155

7 years ago

0.0.154

7 years ago

0.0.153

7 years ago

0.0.152

7 years ago

0.0.151

7 years ago

0.0.150

7 years ago

0.0.149

7 years ago

0.0.148

7 years ago

0.0.147

7 years ago

0.0.146

7 years ago

0.0.145

7 years ago

0.0.144

7 years ago

0.0.143

7 years ago

0.0.142

7 years ago

0.0.141

7 years ago

0.0.140

7 years ago

0.0.139

7 years ago

0.0.138

7 years ago

0.0.137

7 years ago

0.0.136

7 years ago

0.0.135

7 years ago

0.0.134

7 years ago

0.0.133

7 years ago

0.0.132

7 years ago

0.0.131

7 years ago

0.0.130

7 years ago

0.0.129

7 years ago

0.0.128

7 years ago

0.0.127

7 years ago

0.0.126

7 years ago

0.0.125

7 years ago

0.0.124

7 years ago

0.0.123

7 years ago

0.0.122

7 years ago

0.0.121

7 years ago

0.0.120

7 years ago

0.0.119

7 years ago

0.0.118

7 years ago

0.0.117

7 years ago

0.0.116

7 years ago

0.0.115

7 years ago

0.0.114

7 years ago

0.0.113

7 years ago

0.0.112

7 years ago

0.0.111

7 years ago

0.0.110

7 years ago

0.0.109

7 years ago

0.0.108

7 years ago

0.0.107

7 years ago

0.0.106

7 years ago

0.0.105

7 years ago

0.0.104

7 years ago

0.0.103

7 years ago

0.0.102

7 years ago

0.0.101

7 years ago

0.0.100

7 years ago

0.0.99

7 years ago

0.0.98

7 years ago

0.0.97

7 years ago

0.0.96

7 years ago

0.0.95

7 years ago

0.0.94

7 years ago

0.0.93

7 years ago

0.0.92

7 years ago

0.0.91

7 years ago

0.0.90

7 years ago

0.0.89

7 years ago

0.0.88

7 years ago

0.0.87

7 years ago

0.0.86

7 years ago

0.0.85

7 years ago

0.0.84

7 years ago

0.0.83

7 years ago

0.0.82

7 years ago

0.0.81

7 years ago

0.0.80

7 years ago

0.0.79

7 years ago

0.0.78

7 years ago

0.0.77

7 years ago

0.0.76

7 years ago

0.0.75

7 years ago

0.0.74

7 years ago

0.0.73

7 years ago

0.0.72

7 years ago

0.0.71

7 years ago

0.0.70

7 years ago

0.0.69

7 years ago

0.0.68

7 years ago

0.0.67

7 years ago

0.0.66

7 years ago

0.0.65

7 years ago

0.0.64

7 years ago

0.0.63

7 years ago

0.0.62

7 years ago

0.0.61

7 years ago

0.0.60

7 years ago

0.0.59

7 years ago

0.0.58

7 years ago

0.0.57

7 years ago

0.0.56

7 years ago

0.0.55

7 years ago

0.0.54

7 years ago

0.0.53

7 years ago

0.0.52

7 years ago

0.0.51

7 years ago

0.0.50

7 years ago

0.0.49

7 years ago

0.0.48

7 years ago

0.0.47

7 years ago

0.0.46

7 years ago

0.0.45

7 years ago

0.0.44

7 years ago

0.0.43

7 years ago

0.0.42

7 years ago

0.0.41

7 years ago

0.0.40

7 years ago

0.0.39

7 years ago

0.0.38

7 years ago

0.0.37

7 years ago

0.0.36

7 years ago

0.0.35

7 years ago

0.0.34

7 years ago

0.0.33

7 years ago

0.0.32

7 years ago

0.0.31

7 years ago

0.0.30

7 years ago

0.0.29

7 years ago

0.0.28

7 years ago

0.0.27

7 years ago

0.0.26

7 years ago

0.0.25

7 years ago

0.0.24

7 years ago

0.0.23

7 years ago

0.0.22

7 years ago

0.0.21

7 years ago

0.0.20

7 years ago

0.0.19

7 years ago

0.0.18

7 years ago

0.0.17

7 years ago

0.0.16

7 years ago

0.0.15

7 years ago

0.0.14

7 years ago

0.0.13

7 years ago

0.0.12

7 years ago

0.0.11

7 years ago

0.0.10

7 years ago

0.0.9

7 years ago

0.0.8

7 years ago

0.0.7

7 years ago

0.0.5

7 years ago

0.0.4

7 years ago

0.0.3

7 years ago

0.0.2

7 years ago

0.0.1

7 years ago