1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
require "securerandom"

module ProTacts
  # Ids in the shape jj spells a change id: letters from k to z, so an id
  # is never a hash and never a word its author meant. Minted rather
  # than chosen, which is what lets a person read one
  # (db/migrations/005_group_identity.rb).
  module ChangeId
    ALPHABET = "klmnopqrstuvwxyz".chars.freeze #: Array[String]

    # The length a contact's id is minted at. Store#put overwrites
    # rather than refuses, so the width is the only guard a create has:
    # 48 bits, for a family's book. The admin create, the import, and
    # Store#reid draw at it; #minted? measures ids against it.
    CONTACT_LENGTH = 12 #: Integer

    # SecureRandom rather than rand: the draws have to be independent of
    # anything a caller can observe or seed.
    #: (Integer length) -> String
    def self.mint(length)
      SecureRandom.alphanumeric(length, chars: ALPHABET)
    end

    # Whether an id is one this server minted for a contact: the shape
    # #mint draws at CONTACT_LENGTH, and nothing else. Ids that fail
    # this — a client's UUID, a seed from before the shape settled —
    # are what rake contacts:reid moves.
    #: (String id) -> bool
    def self.minted?(id)
      /\A[#{ALPHABET.join}]{#{CONTACT_LENGTH}}\z/.match?(id)
    end
  end
end