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
34
35
36
37
38
require "digest"
module ProTacts
# A sync token as this server spells one: the ctag it was minted at,
# the book it was minted for, and the database that minted it, in a
# URI. Opaque to the client (RFC 6578 section 3) and the URI form is
# conventional, but not opaque to this server, which refuses a token
# another book minted (docs/plans/2026-09-12-per-user-books.md, "The
# wire") or another database did
# (docs/plans/2026-09-24-sync-tokens-name-their-database.md).
# Built on the ctag so that a client polling either one sees changes
# at the same points.
module SyncToken
PREFIX = "http://pro-tacts/sync/" #: String
# The book half: the first 16 hex digits of the SHA-256 of the
# requester's login.
#: (String login) -> String
def self.book(login)
Digest::SHA256.hexdigest(login)[0, 16].to_s
end
#: (String ctag, String book, String database_id) -> String
def self.mint(ctag, book, database_id)
"#{PREFIX}#{ctag}/#{book}/#{database_id}"
end
# The sequence a token carries, or nil for one this database did
# not mint — another database's (a reseed's, a restore's), another
# book's, a rename's leftover, or a string that is no token of
# ours at all. Whether the sequence is one the change log can
# still answer from is the caller's to judge.
#: (String token, String book, String database_id) -> Integer?
def self.read(token, book, database_id)
token[%r{\A#{Regexp.escape(PREFIX)}(\d+)/#{Regexp.escape(book)}/#{Regexp.escape(database_id)}\z}, 1]&.to_i
end
end
end