Generate deterministic fake values: The same input will always generate the same fake-output.

Overview

copycat

import { copycat } from '@snaplet/copycat'

copycat.email('foo')
// => '[email protected]'

copycat.email('bar')
// => '[email protected]'

copycat.email('foo')
// => '[email protected]'

Motivation

The problem

Many of the use cases we aim on solving with snaplet involve anonymizing sensitive information. In practice, this involves replacing each bit of sensitive data with something else that resembles the original value, yet does not allow the original value to be inferred.

To do this, we initially turned to faker for replacing the sensitive data with fake data. This approach took us quite far. However, we struggled with getting the replacement data to be deterministic: we found we did not have enough control over how results are generated to be able to easily ensure that for each value of the original data we wanted to replace, we'd always get the same replacement value out.

Faker allows one to seed a psuedo-random number generator (PRNG), such that the same sequence of values will be generated every time. While this means the sequence is deterministic, the problem was we did not have enough control over where the next value in the sequence was going to be used. Changes to the contents or structure in the original data we're replacing and changes to how we are using faker both had an effect on the way we used this sequence, which in turn had an effect on the resulting replacement value for any particular value in the original data. In other words, we had determinism, but not in a way that is useful for our purposes.

The solution

What we were really needing was not the same sequence of generated values every time, but the same mapping to generated values every time.

This is exactly what we designed copycat to do. For each method provided by copycat, a given input value will always map to the same output value.

import { copycat } from '@snaplet/copycat'

copycat.email('foo')
// => '[email protected]'

copycat.email('bar')
// => '[email protected]'

copycat.email('foo')
// => '[email protected]'

Copycat work statelessly: for the same input, the same value will be returned regardless of the environment, process, call ordering, or any other external factors.

Under the hood, copycat hashes the input values (in part relying on md5), with the intention of making it computationally infeasible for the input values to be inferred from the output values.

Alternative approaches

It is still technically possible to make use of faker or similar libraries that offer deterministic PRNG - with some modification. That said, these solutions came with practical limitations that we decided made them less viable for us:

  • It is possible to simply seed the PRNG for every identifier, and then use it to generate only a single value. This seems to be a misuse of these libraries though: there is an up-front cost to seeding these PRNGs that can be expensive if done for each and every value to be generated. Here are benchmarks that point to this up-front cost.
  • You can generate a sequence of N values, hash identifiers to some integer smaller than N, then simply use that as an index to lookup a value in the sequence. This can even be done lazily. Still, you're now limiting the uniqueness of the values to N. The larger N is, the larger the cost of keeping these sequences in memory, or the more computationally expensive it is if you do not hold onto the sequences in memory. The smaller N is, the less unique your generated values are.

Note though that for either of these approaches, hashing might also still be needed to make it infeasible for the inputs to be inferred from the outputs.

API Reference

Overview

All copycat functions take in an input value as their first parameter:

import { copycat } from '@snaplet/copycat'

copycat.email('foo')
// => '[email protected]'

The given input can be any JSON-serializable value. For any two calls to the same function, the input given in each call serializes down to the same value, the same output will be returned.

Note that unlike JSON.stringify(), object property ordering is not considered.

faker

A re-export of faker from @faker-js/faker. We do not alter faker in any way, and do not seed it.

copycat.oneOf(input, values)

Takes in an input value and an array of values, and returns an item in values that corresponds to that input:

copycat.oneOf('foo', ['red', 'green', 'blue'])
// => 'red'

times(input, range, fn)

Takes in an input value and a function fn, calls that function repeatedly (each time with a unique input) for a number of times within the given range, and returns the results as an array:

copycat.times('foo', [4, 5], copycat.word)
// => [ 'Raeko', 'Vame', 'Kiyumo', 'Koviva', 'Kiyovami' ]

As shown above, range can be a tuple array of the minimum and maximum possible number of times the maker should be called. It can also be given as a number, in which case fn will be called exactly that number of times:

copycat.times('foo', 2, copycat.word)
// => [ 'Raeko', 'Vame' ]

copycat.int(input[, options])

Takes in an input value and returns an integer.

int('foo')
// => 2196697842

options

  • min=0 and max=Infinity: the minimum and maximum possible values for returned numbers

copycat.bool(input)

Takes in an input value and returns a boolean.

copycat.bool('foo')
// => false

copycat.float(input[, options])

Takes in an input value and returns a number value with both a whole and decimal segment.

copycat.float('foo')
// => 2566716916.329745

copycat.char(input)

Takes in an input value and returns a string with a single character.

copycat.char('foo')
// => 'M'

The generated character will be an alphanumeric: lower and upper case ASCII letters and digits 0 to 9.

copycat.hex(input)

Takes in an input value and returns a string with a single digit value.

copycat.digit('foo')
// => '2'

copycat.hex(input)

Takes in an input value and returns a string with a single hex value.

copycat.hex('foo')
// => '2'

options

  • min=0 and max=Infinity: the minimum and maximum possible values for returned numbers

copycat.dateString(input[, options])

Takes in an input value and returns a string representing a date in ISO 8601 format.

dateString('foo')
// => '1982-07-11T18:47:39.000Z'

options

  • minYear=1980 and maxYear=2019: the minimum and maximum possible year values for returned dates

copycat.uuid(input)

Takes in an input and returns a string value resembling a uuid.

copycat.uuid('foo')
// => '540b95dd-98a2-56fe-9c95-6e7123c148ca'

copycat.email(input)

Takes in an input and returns a string value resembling an email address.

copycat.email('foo')
// => '[email protected]'

copycat.firstName(input)

Takes in an input and returns a string value resembling a first name.

copycat.firstName('foo')
// => 'Alejandrin'

copycat.lastName(input)

Takes in an input and returns a string value resembling a last name.

copycat.lastName('foo')
// => 'Keeling'

copycat.fullName(input)

Takes in an input and returns a string value resembling a full name.

copycat.fullName('foo')
// => 'Zakary Hessel'

copycat.phoneNumber(input)

Takes in an input and returns a string value resembling a phone number.

copycat.phoneNumber('foo')
// => '+3387100418630'

note The strings resemble phone numbers, but will not always be valid. For example, the country dialing code may not exist, or for a particular country, the number of digits may be incorrect. Please let us know if you need valid phone numbers, and feel free to contribute :)

copycat.username(input)

Takes in an input and returns a string value resembling a username.

copycat.username('foo')
// => 'Zakary.Block356'

copycat.password(input)

Takes in an input value and returns a string value resembling a password.

password('foo')
// => 'uRkXX&u7^uvjX'

Note: not recommended for use as a personal password generator.

copycat.city(input)

Takes in an input and returns a string value representing a city.

copycat.city('foo')
// => 'Garland'

copycat.country(input)

Takes in an input and returns a string value representing a country.

copycat.country('foo')
// => 'Bosnia and Herzegovina'

copycat.streetName(input)

Takes in an input and returns a string value representing a fictitious street name.

copycat.streetName('foo')
// => 'Courtney Orchard'

copycat.streetAddress(input)

Takes in an input and returns a string value representing a fictitious street address.

copycat.streetAddress('foo')
// => '757 Evie Vista'

copycat.postalAddress(input)

Takes in an input and returns a string value representing a fictitious postal address.

copycat.postalAddress('foo')
// => '178 Adaline Forge, Moreno Valley 8538, Haiti'

copycat.countryCode(input)

Takes in an input and returns a string value representing a country code.

copycat.countryCode('foo')
// => 'BV'

copycat.timezone(input)

Takes in an input and returns a string value representing a time zone.

copycat.timezone('foo')
// => 'Asia/Tbilisi'

copycat.word(input)

Takes in an input value and returns a string value resembling a fictitious word.

copycat.word('foo')
// => 'Kinkami'

options

  • capitalize=true: whether or not the word should start with an upper case letter
  • minSyllables=2 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain
word('id-2', {
  minSyllables: 1,
  maxSyllables: 6,
  unicode: 0.382
})
// =>
'Rayuashira'

copycat.words(input)

Takes in an input value and returns a string value resembling fictitious words.

copycat.words('foo')
// => 'Niko vichinashi'

options

  • min=2 and max=3: the minimum and maximum possible number of words that returned strings will contain.
  • capitalize='first': whether or not the words should start with upper case letters. If true or 'all' is given, each string returned will start with an upper case letter in each word. If 'first' is given, for each string returned, only the first word will start with an upper case letter. If false is given, each string returned will always contain only lower case letters.
  • minSyllables=1 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain

copycat.sentence(input)

Takes in an input value and returns a string value resembling a sentence of fictitious words.

copycat.sentence('foo')
// => 'Kiraevavi somani kihy viyoshi nihahyke kimeraeni.'

options

  • minClauses=1 and maxClauses=2: the minimum and maximum possible number of clauses that a returned sentence will contain.
  • minWords=5 and maxWords=8: the minimum and maximum possible number of words that each clause will contain.
  • minSyllables=1 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain

copycat.paragraph(input)

Takes in an input value and returns a string value resembling a paragraph of fictitious words.

copycat.paragraph('foo')
// => 'Vakochiko ke rako kimuvachi hayuso mi vako kaichina, mishi mukaimo hakin va racea. Raechime miko kaimo keki shi navi makin yomehyha, na hya nano kin yokimo rae ra. Ke chi kakinaki kakorae machi. Raeva ka kaiko muvani ka racea kaichiyuchi muvinota, sokaiyu komechino shiso yuha raeraceaki kin chitavi. Kokaiashi chirako rae muyo vachi mukani nakoyuta kinmochikai, muhamuva hy mayushita ke shimo takinka notavi kinvayo.'

options

  • minSentences=3 and minSentences=7: the minimum and maximum possible number of sentences that a returned paragraph will contain.
  • minClauses=1 and maxClauses=2: the minimum and maximum possible number of clauses that each sentence will contain.
  • minWords=5 and maxWords=8: the minimum and maximum possible number of words that each clause will contain.
  • minSyllables=1 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain

copycat.ipv4(input)

Takes in an input value and returns a string value resembling an IPv4 address.

copycat.ipv4('foo')
// => '166.164.23.159'

copycat.mac(input)

Takes in an input value and returns a string value resembling a MAC address.

copycat.mac('foo')
// => 'e1:2c:54:74:b7:80'

copycat.userAgent(input)

Takes in an input value and returns a string value resembling a browser User Agent string.

copycat.userAgent('foo')
// => 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 5.3; Trident/3.1; .NET CLR 1.2.39149.4)'

note For simplicity, this is currently working off of a list of 500 pre-defined user agent strings. If this is too limiting for your needs and you need something more dynamic than this, please let us know, and feel free to contribute :)

Comments
  • Support scrambling

    Support scrambling

    I think of scrambling as a generic method for non-specific transformations. The idea is that a given string is randomly shifted:

    the DOG ate the cheese! becomes shx SAS weq sss snmwlx. I would love to be able to specify characters that should not be transformed:

    copycat.scramble(input, { preserve: ['@', ' ', '!'] })

    opened by peterp 5
  • feature: Helper utility to generate a collection of faked objects for bulk seeding

    feature: Helper utility to generate a collection of faked objects for bulk seeding

    Helper utility to generate a collection of faked objects for bulk seeding

    Use Case

    When using RedwoodJS and seeding data with Prisma, I often find myself wanting to quickly generate N models populated with data.

    For example, given the Prisma model:

    model Person {
      id            Int    @id @default(autoincrement())
      fullName      String @unique
      postalAddress String
    }
    

    I may want to quickly generate 100 people.

    Currently, I do something like this to construct 100 people with some fake data and then save ...

      const data: Prisma.PersonCreateArgs['data'][] = [
            ...Array(100).keys(),
          ].map((key) => {
            return {
              fullName: copycat.fullName(key),
              postalAddress: copycat.postalAddress(key),
            }
          })
    
    // ...
    
      await db.person.createMany({ data, skipDuplicates: true })
    

    But, I keep having to remember the somewhat clunky syntax

    [ ...Array(100).keys(),].map((key) => {}
    

    and it would be useful in copycat to have some method that can:

    • take an argument for how many data objets to generate
    • return a collection of those populated objects to save

    I could envision several implementations:

    • a callback to set the "generate" methods ... in the above example the returned {fullName, postalCode}
    • option to return a collection (memory heavy) to use with createMany
    • option to "stream" so-to-speak so can create individual via Prisma create (to support databases other than Postgres, but if you are using Snaplet, not sure why... but could be useful still).
    • option to configure the "shape" like that in fictional to configure the object ... e.g., define:
    const person =  {
              fullName,
              postalAddress,
            }
    
    const person =  {
              name: fullName,
              postalAddress,
            }
    
    

    so that no need to pass in the key per item.

    opened by dthyresson 3
  • Support slugs?

    Support slugs?

    I see that your username function may generate usernames with dots in them. In our use case we need to generate username with only alphanumeric characters and maybe dashes and underscores.

    opened by zomars 3
  • UUID Collision

    UUID Collision

    Version 0.5.0

    Given only 168140 rows, we're seeing 4 UUID generation collisions across the entire set even when when copycat.uuid is fed with unique-to-that-table data. Different inputs are giving the same outputs.

    This occurs for us regardless of whether we use our integer IDs or prefixed-cuids as the input.

    Replication:

    console.log(copycat.uuid(158012))
    console.log(copycat.uuid(30939))
    console.log()
    
    console.log(copycat.uuid(99754))
    console.log(copycat.uuid(132262))
    console.log()
    
    console.log(copycat.uuid(46865))
    console.log(copycat.uuid(102367))
    console.log()
    
    console.log(copycat.uuid(122917))
    console.log(copycat.uuid(91620))
    console.log()
    
    console.log(copycat.uuid("usr_cl74n76272kyp04ujquy1beqn"))
    console.log(copycat.uuid("usr_cl74n4tp512yq04ujdi321oez"))
    

    Output:

    219f19e0-b9c4-5a42-ad8a-9a80e9b246a6
    219f19e0-b9c4-5a42-ad8a-9a80e9b246a6
    
    2ce050e8-37de-5de3-9a6a-1ad1252df1f4
    2ce050e8-37de-5de3-9a6a-1ad1252df1f4
    
    4358a1c5-c43f-5cfe-963e-13b02a3621f1
    4358a1c5-c43f-5cfe-963e-13b02a3621f1
    
    bc58abc9-eb5a-5ceb-84e5-684612eb7d7b
    bc58abc9-eb5a-5ceb-84e5-684612eb7d7b
    
    1d99d2e8-3edc-5e70-9c9c-f19919872ffa
    1d99d2e8-3edc-5e70-9c9c-f19919872ffa
    

    https://replit.com/join/krqpfrimzc-darianmoody

    opened by djm 2
  • Document motivation

    Document motivation

    Adds a 'motivation' section to the readme.

    Thought it'd also be a good time to share this with everyone else on the team, for their context (thus all the reviewers I've added to this PR).

    opened by justinvdm 2
  • Implement uuid() basics

    Implement uuid() basics

    Adds basic support for generating strings representing uuids.

    Important notes

    • I've used v5 uuids here instead of v4, since v4 is meant to be random (not what we're doing) and v5 is meant to map to some input value (what we're doing). One gotcha here, is that v5 uuids also need a namespace identifier - we just define a global one (maybe we can make the namespace an option at some point).
    • The good news about v5 uuids is the security they offer:

    Version-3 and version-5 UUIDs have the property that the same namespace and name will map to the same UUID. However, neither the namespace nor name can be determined from the UUID, even if one of them is specified, except by brute-force search

    @peterp ^ that makes me wonder about PII with copycat in general - I have no idea what guarantees fictional offers here - it might be technically possible for people to infer the input values from the output values given by copycat. That depends on what string-hash's algorithm offers. I think we might be able to guarantee this for everything provided by fictional if we get a guarantee that the hash is not reversible, I guess we'd want that, right?

    opened by justinvdm 2
  • fix: support scrambling negative numbers

    fix: support scrambling negative numbers

    Currently, copycat.scramble(-123) will return NaN because the - is converted into, for example, a "g".

    I think this is unexpected and undesirable behaviour, especially when used with snaplet, as a typical use case is:

    // ...
    a_number_column: copycat.scramble(row.a_number_column),
    // ...
    

    Which will work when row.a_number_column is positive, but not when it is negative. The work around would be to manually convert all your numbers to strings, then add "-" to the preserve options, then cast it back to the appropriate number type, but obviously that's not fun.

    opened by dave-v 1
  • feat: Implements fictional's someOf primitive

    feat: Implements fictional's someOf primitive

    This PR implement's fictional's someOf which:

    Takes in an identifying input value and an array of values, repeatedly picks items from that array a number of times within the given range. Each item will be picked no more than once.

    someOf('id-23', [1, 2], ['red', 'green', 'blue'])
    // =>
    [
      'green'
    ]
    

    This is useful when seeding string arrays seen here as tags in Prisma models like:

    model Post {
      id        Int      @id @default(autoincrement())
      title     String
      content   String
      tags      String[]
    }
    

    While the following oneOf can be performed:

    copycat.oneOf([tech,life,music,art,science]),

    There was no way in copycat to make an array like ['tech', 'art'].

    The someOf function now lets you generate deterministic collections.

    opened by dthyresson 1
  • Less collisions and better { limit } handling for email and username

    Less collisions and better { limit } handling for email and username

    Context

    username and email need to be improved as far as collisions (different inputs returning the same outputs) are concerned:

    • from measurements done (see #29 and stats below), the worst case dataset size at which collisions were observed was around 9000 for username and 8700 for email. This means collisions might happen at datasets larger than 9000, which is not a very uncommon size for production databases.
    • username and email are used for values that usually need to be unique in a database
      • uuid also of course, though since that is basically proxying straight through to a uuid v5, I'm less worried about it.
      • Things like fullName are less likely to need to be unique - it may look a little weird if people see the names appearing multiple times, so still something worth fixing, but lower priority than values that typically need to be unique
      • There's probably still others to improve (dateString maybe?), for the same reasons, but this is a start

    Approach

    Add more possibilities to the return values for username and email to allow for a larger output range, then measure with the collision script again.

    While I was there, I also added limit support for username (see #30 for context), and some improvements to limit logic. I also made some improvements to the collisions script. I've added PR comments for these things for more context.

    Measurements

    before:

    {"methodName":"username","mean":"59674.56","stddev":"29524.62","moe":"0.14","runs":50,"n":50,"min":9005,"max":145998,"sum":2983728,"hasCollided":true,"duration":379735}
    {"methodName":"email","mean":"276483.20","stddev":"179478.58","moe":"0.18","runs":50,"n":50,"min":8706,"max":849407,"sum":13824160,"hasCollided":true,"duration":2543995}
    

    after:

    {"methodName":"username","mean":"800847.73","stddev":"138023.51","moe":"0.10","runs":50,"n":11,"min":485308,"max":994067,"sum":999999,"hasCollided":true,"duration":7921728}
    {"methodName":"email","mean":null,"stddev":null,"moe":null,"runs":50,"n":0,"min":null,"max":null,"sum":999999,"hasCollided":false,"duration":19710157}
    

    Note how there were only 11 runs for username that had collisions - for the rest, we generated 999999 values without finding any collision.

    email's stats look strange, but that's because no collisions were found at all - for 50 runs, where in of those runs we generated 999999 values without finding any collision.

    opened by justinvdm 1
  • feat: Initial implementation of `limit` option

    feat: Initial implementation of `limit` option

    Video with context

    https://www.loom.com/share/b94094839ca04524ab661b8837eebf6e

    Problem

    At the moment, copycat gives you the ability to generate some fake value corresponding to some input value, for example:

    copycat.email('[email protected]')
    // => '[email protected]'
    

    However, there is currently no way to restrict the generated values to be within a given character limit.

    At snaplet, we make use of copycat to replace real values in a database with fake values.

    This is where the problem comes in: the real values in the database are within some character limit. However, the fake values generated by copycat that we're replacing them with are not always within this same character limit. As a result, we aren't able to use these fake values.

    Solution

    Add a limit option to each copycat API method that generates a string, for example:

    // generated result will be <= 20 characters
    copycat.email('[email protected]', { limit: 20 })
    

    Approach

    Copycat makes use of composition to generate values. For example, copycat.email() makes use of copycat.firstName() and copycat.lastName().

    The idea is to have each component in each layer of composition be aware of the character limit. For composites like email, allocate a limit to each component. For example, if the limit is 25, then give 5 as a limit for firstName.

    Then, for each primitive/leaf (the end of the chain of composition), have it restrict its output to be within that limit. For example, firstName makes use of oneOf(), and uses it to pick from an array of first names (provided by faker). So this PR replaces this oneOf() usage with a new oneOfString() function, which will only pick from the list of values that are within that limit. If none are found, it defaults to using copycat.word().

    For more context on the approach, take a look at the video: https://www.loom.com/share/b94094839ca04524ab661b8837eebf6e

    opened by justinvdm 1
  • chore: Add basic collision probabilities script

    chore: Add basic collision probabilities script

    Context

    We currently have very little information on how likely collisions are for each of copycat's API methods. For some methods (e.g. bool), these are trivial or unimportant. For data types that typically need to be unique (e.g. uuid or email), this is important information to know: it allows us to know where in the API we need to improve to make collisions less likely, and lets our users know how likely collisions will be for their use case.

    Approach

    * For each API method in the shortlist:
       * Until the stopping conditions are met (see below):
         * Run the method with a new uuid v4 as input until the first collision is found, or until we've iterated 999,999 times
         * Record the data set size at which the first collision happened as a datapoint
       * Output the obtained stats (e.g. mean, stddev, margin of error)
    
    // The stopping conditions
    * Both of these must be true:
     * We've run the method a minimum (100); or
     * Either
       * The margin of error is under a lower threshold (`0.05`); or
       * The margin of error is under a higher threshold (`0.10`), but the sum (sum of first collision dataset sizes obtained so far) is over a higher threshold (999,999)
    

    Since the task here is number-crunching, we do this using a worker farm to leverage multiple cpu cores to get results faster.

    Why complicate the logic for the stopping conditions?

    Ideally, we could only check two things: that our runs (the set of first collision data set sizes) is over a minimum threshold, and that our margin of error is under a maximum threshold.

    Unfortunately, this isn't very computationally feasible for methods with large numbers for their first collision dataset size - it would just take too long to reach a lower margin of error. So we make a compromise, and allow for a higher margin of error (0.10 rather than 0.05) for methods with high numbers for their first collision data set size.

    For example, the first collision dataset size for int is very large - so in this case, it isn't very feasible to run this until we obtained 0.05. Let's say we ran it 100 times, and in total, all of the runs (i.e. all of the first collision data set sizes) summed up to over 999,999. We then decide that we're happy enough with a margin of error of 0.10, and stop there.

    Why not analyse for the theoretical probabilities instead?

    We're taking an empirical approach here, rather than analysing the code to work out the theoretical probabilities. The answer is that it is difficult for us to do this for each and every copycat API method in question, and know for certain these calculations are accurate - there are just too many different variables at play influencing the probabilities, and we wouldn't really know for sure if we've accounted for all of them. The only way we can know how the collision probabilities look in practice, is to actually run the code.

    Limitations

    • I must admit that I know very little about the areas I'm wondering around in with this PR: both hash collision probabilities, and stats. If there's something important I'm missing with the approach I'm taking, I'd like to know!
    • The approach required taking some shortcuts, since calculating the probabilities within better margins of error for some of the methods would take infeasibly long. See Why such complex logic for the stopping conditions? above for more on this.

    How to read the results

    For each api method: *mean is roughly: the average data set size at which a collision happened (arithmetic mean)

    • stddev is the standard deviation
    • min is the worst-case run - the smallest data set size at which first collision happened
    • max is the best-case run - the largest data set size at which first collision happened
    • runs is the number of times we tested the api method before deciding the stats are "good enough"
    • sum is the sum of all the runs - the total number of times the method was called
    • moe, the "margin of error", basically means: we are 95% certain the "real" average dataset size at which the first collision happens is within x % of mean - for example, a moe of 0.05 means we are 95% certain the "real" average data set size at which the first collision happens is within 5 % of mean
    • hasCollided, if we reached the maximum dataset size that we test collisions for (999,999)

    Results

    Incomplete list (have yet to tweak the script to actually finish running in a reasonable time for the other methods):

    {"methodName":"dateString","mean":"414.61","stddev":"214.23","moe":"0.05","runs":411,"min":5,"max":1412,"sum":170404,"hasCollided":true}
    {"methodName":"fullName","mean":"1574.32","stddev":"784.96","moe":"0.05","runs":383,"min":75,"max":4853,"sum":602964,"hasCollided":true}
    {"methodName":"streetAddress","mean":"22367.15","stddev":"12490.97","moe":"0.10","runs":122,"min":1154,"max":58605,"sum":2728792,"hasCollided":true}
    {"methodName":"float","mean":"186526.06","stddev":"103245.91","moe":"0.10","runs":118,"min":12602,"max":550474,"sum":22010075,"hasCollided":true}
    {"methodName":"ipv4","mean":"79714.37","stddev":"41439.56","moe":"0.10","runs":106,"min":6581,"max":205074,"sum":8449723,"hasCollided":true}
    {"methodName":"email","mean":"119657.21","stddev":"59025.15","moe":"0.10","runs":100,"min":5254,"max":275462,"sum":11965721,"hasCollided":true}
    
    opened by justinvdm 1
Releases(v0.1.0)
  • v0.1.0(May 9, 2022)

    This is the first release of Snaplet's Copycat! It includes the most important transformations for personally identifiable information in databases.

    Source code(tar.gz)
    Source code(zip)
A deterministic object hashing algorithm for Node.js

Deterministic-Object-Hash A deterministic object hashing algorithm for Node.js. The Problem Using JSON.stringify on two objects that are deeply equal

Zane Bauman 7 Nov 30, 2022
Minimal implementation of SLIP-0010 hierarchical deterministic (HD) wallets

micro-ed25519-hdkey Secure, minimal implementation of SLIP-0010 hierarchical deterministic (HD) wallets. Uses audited @noble/ed25519 under the hood. B

Paul Miller 11 Dec 25, 2022
Generate smooth, consistent and always-sexy box-shadows, no matter the size, ideal for design token generation.

smooth-shadow Generate smooth, consistent and always-sexy box-shadows, no matter the size, ideal for design token generation. Demo As Tobias already p

Thomas Strobl 4 Oct 15, 2022
A Bootstrap plugin to create input spinner elements for number input

bootstrap-input-spinner A Bootstrap / jQuery plugin to create input spinner elements for number input. Demo page with examples Examples with floating-

Stefan Haack 220 Nov 7, 2022
A phone input component that uses intl-tel-input for Laravel Filament

Filament Phone Input This package provides a phone input component for Laravel Filament. It uses International Telephone Input to provide a dropdown o

Yusuf Kaya 24 Nov 29, 2022
Have you always wanted to check if someone has checked out your story or not?

InstaStoryChecker - Search in your story viewers Have you always wanted to check if a certain person has checked out your story or not and you had to

Mohammad Saleh 4 Jul 7, 2022
CA9.io Portal Seed Server. Makes sure the project files are always accessable.

Torrent Seed Server What is this about? This project helps users of CA9.io Metaverse to keep their files and addons permanently available. Since we us

CA9.io, TM9657 GmbH 2 Feb 3, 2022
long-term project untuk menunggu lebaran (update versi always bro) wkwkwk

This is a Next.js project bootstrapped with create-next-app. Getting Started First, run the development server: npm run dev # or yarn dev Open http://

Dea Aprizal 61 Jan 3, 2023
The JSON logger you always wanted for Lambda.

MikroLog The JSON logger you always wanted for Lambda. MikroLog is like serverless: There is still a logger ("server"), but you get to think a lot les

Mikael Vesavuori 11 Nov 15, 2022
A pomodoro web app with features like always on top PIP mode!

Tomodoro A pomodoro web app with always on top mode! Features: Clean UI(Inspired from other pomodoro apps like Pomotroid) Themes Works Offline PIP/Alw

null 109 Dec 24, 2022
The best Blooket hacks on the platform! These hacks are always working, all gamemode hacks work and will be fixed when broke.

Support Discord Server: https://discord.gg/UCHtVM4A Blooket Hack The Blooket Hack provided by Jude Why you should use this tool: Always working. When

Jude 26 Dec 20, 2022
Always with personal privacy and anonymity in mind.

Clodbunker At the moment we aim to be a cutting-edge service that provides anonymous and secure cloud storage, with a strong focus on end-to-end encry

Cloudbunker 4 Mar 15, 2023
Minimal template engine with compiled output for JavaScript.

@fnando/seagull Minimal template engine with compiled output for JavaScript. Installation This package is available as a NPM package. To install it, u

Nando Vieira 5 Mar 1, 2022
Logs the output, time, arguments, and stacktrace of any function when it's called in a gorgeous way.

Function.prototype.log Logs the output, time, arguments, and stacktrace of any function when it's called. How to use: Like this: function yourFunction

--Explosion-- 4 Apr 9, 2022
Group and sort Eleventy’s verbose output by directory (and show file size with benchmarks)

eleventy-plugin-directory-output Group and sort Eleventy’s verbose output by directory (and show file size with benchmarks). Sample output from eleven

Eleventy 16 Oct 27, 2022
A string of four operations of the library, can solve the js digital calculation accuracy of scientific notation and formatting problems, support for thousands of decimal point formatting output operations

A string of four operations of the library, can solve the js digital calculation accuracy of scientific notation and formatting problems, support for thousands of decimal point formatting output operations

null 10 Apr 6, 2022
🔨 A more engineered, highly customizable, standard output format commitizen adapter.

cz-git Github | Installation | Website | 简体中文文档 Introduction A more engineered, highly customizable, standard output format commitizen adapter. What i

zhengqbbb 402 Dec 31, 2022
Script to fetch all NFT owners using moralis API. This script output is a data.txt file containing all owner addresses of a given NFT and their balances.

?? Moralis NFT Snapshot Moralis NFT API will only return 500 itens at a time when its is called. For that reason, a simple logic is needed to fetch al

Phill Menezes 6 Jun 23, 2022
A tool for analyzing the output of `tsc --generateTrace`

@typescript/analyze-trace Tool for analyzing the output of tsc --generateTrace automatically, rather than following the steps here. Note: The goal is

Microsoft 195 Dec 22, 2022