Releases: interop-alliance/social-core
Releases · interop-alliance/social-core
Release list
v0.8.1
Fixed
normalizeLabelstrips the wrapper Apple's Contacts API puts around its
predefined label constants, so_$!<Home>!$_normalizes tohomeinstead of
reaching a wallet's UI verbatim. A wrapper around blank text falls back to
other. Labels the user typed themselves arrive unwrapped and are unaffected.
The wrapper is stripped, nothing more; labels are not mapped onto a canonical
cross-platform vocabulary.upgradeContactDatare-normalizes every stored entry label, so contacts
written before this fix are repaired when they are loaded. The import path
alone could not reach them: a row edited in the wallet is never overwritten by
a re-import, and a wallet that only receives contacts over sync never imports
at all.
Contacts imported on iOS before this release get new labels, so their payloads
change on the first write after it. Expect one round of sync traffic for those
rows.
v0.8.0
Changed
- BREAKING: the
deviceIdfield onContactHeadPayloadand
ContactRevisionPayload(and their structural guards) is renamed to
writerId. It is an unkeyed, clearable attribution label for the writing
agent, not a hardware identity. No alias is kept.
Fixed
remotePayloadWinscomparesupdatedAtby parsed epoch time instead of
lexically, so stamps written at different fractional-second precisions order
chronologically (2026-08-03T12:00:00Zcompared lexically against
2026-08-03T12:00:00.500Zpicked the earlier stamp). If either stamp is
unparseable the comparison falls back to the previous lexical compare; the
exact-tiewriterIdtiebreak is unchanged.
v0.7.0
Added
- The self-contact seed every wallet starts a new account with:
selfContact({ dids, email })and the display name it is stored under,
SELF_CONTACT_NAME. The name is convergence-critical -- a pull path
recognizes another replica's copy of a seed by its exact display name -- so
every wallet now spells it from one place. The seed is emitted through
normalizeContact, so it carries the same shape every other write path
produces. A wallet's own additional seeds stay app-side. isUnlinkedSeedTwin(local, incoming, seedNames): the pure predicate a pull
path absorbs another replica's copy of a seed with, instead of inserting a
duplicate. True when the incoming contact is named byseedNames(the
caller's own seed set), the local row carries the same name, and the local row
has not been customized (no phone, email, organization, or note). The caller
supplies only rows still unlinked to the remote feed, and keeps its own
storage transaction around it.- Contact display helpers, so a contact reads identically on both wallets:
initialsFor(initials avatar),secondaryLineFor(the line under a name),
contactMatchesQuery+compareContactsByName(contact-list search and
order), andACTION_LABELS+snapshotLines(revision history). buildContact({ existing, displayName, givenName, familyName, organization, note, phoneNumbers, emailAddresses, dids }):
the headless half of a contact edit form. Carries through every field the form
does not surface, trims and clears emptied fields toundefined, normalizes
entry labels, drops blank rows while preserving carried-through entry
metadata, and folds the DID rows back throughsetDids. The row shape is
exported asContactFormRow.isDidUrl(url): the single "is this a DID?" predicate, now shared by the read
side (getDids/setDids, whose behavior is unchanged) and by an edit
form's write-side validation -- which must refuse exactly what the read side
would not surface, or persist an entry that is invisible on every screen.
Fixed
planImportMergeno longer duplicates a re-imported contact whosenativeId
churned but whose content is unchanged and which the existing content
heuristics could not reach -- notably a nameless, phone-number-only row (the
DID and name+phone/email rules both need something to key on). The content
fallback now tries an exact content match first: equal on every field but the
churn-prone id metadata (the contact's ownnativeIdand the per-entryid
hints on phones / emails, which Android renumbers when the OS re-aggregates
raw contacts). Candidates are still restricted to rows no incoming contact
claims bynativeId, and an ambiguous match resolves deterministically to the
first unclaimed row inexistingorder.
v0.6.0
Added
setDids(contact, dids): the write-side counterpart ofgetDids. Replaces a
contact'sdid:entries (by the same scheme predicate, including mangled
http(s)://did:urls) with the given list, carrying non-DIDurlAddresses
entries through untouched, labeling each DID entry'did', trimming, dropping
blanks, and collapsing exact repeats to the first occurrence.
Changed
getDidsnow dedupes its result (exact comparison, first occurrence wins): a
merge from another replica can leave the same DID on two entries, and a
contact holding one DID twice means nothing beyond holding it once.
v0.5.0
Changed
ContactData.phoneNumbersandContactData.emailAddressesare now optional,
like every other multi-entry field: a contact reachable only by DID has
neither, and should not have to carry an empty array to say so.
isContactDataaccepts them absent (still rejecting a present-but-malformed
array), andnormalizeContactis unchanged -- it keeps emitting[]for
both, so its output stays byte-identical. Consumers that read either field
without a?? []guard need updating.
v0.4.0
Changed
- BREAKING:
planImportMergenow returns a fourth bucket,stale: the
existing source-imported rows (nativeIdnon-null) that the batch neither
matched bynativeIdnor claimed by content -- orphans whose source id
vanished, wallet-edited ones included. It is advisory: the planner still never
deletes anything, so a caller decides whether to offer cleanup, re-link, or
ignore them. Wallet-only rows (nullnativeId) are never stale. Callers that
exhaustively destructure or type the return value need updating. planImportMergefalls back to a content match when an incoming contact's
nativeIdmatches no row, so a contact whose source id churned -- notably
Android's aggregate contact id, which changes when the OS re-links raw
contacts across accounts -- rebinds to its existing row instead of duplicating
it. Only rows holding anativeIdthat no contact in the batch claims are
candidates, and each row can be claimed by at most one incoming contact (first
match wins, in incoming order). A shared DID matches on its own, even when the
display names differ. Otherwise a candidate matches when the normalized
display names are equal and non-empty AND the two share at least one
normalized phone or email -- or, when both sides have no phones, emails, or
DIDs at all, on the name alone. Phones compare bydigitswhen present, else
by the digits ofnumber; emails compare trimmed and lowercased; names
compare NFKC-normalized, lowercased, and whitespace-collapsed; DIDs are taken
fromurlAddressesviagetDidsand compared verbatim. A matched row that
was never wallet-edited is overwritten, which rebinds it to the new
nativeId; a wallet-edited one is skipped, keeping the invariant that a
wallet edit is never overwritten by an import while still not duplicating the
row. Contacts and rows with a null/undefinednativeIdnever participate.
v0.3.0
Changed
- BREAKING: postal addresses in
ContactData/ContactInputnow spell the
postal codepostalCode(waspostCode) and the post-office boxpoBox(was
pobox). - BREAKING: the administrative subdivision is carried as
regiononly; the
parallelstatefield is gone. Mapping a source that reports astateonto
regionis the importer's job, keeping exactly one spelling per field in
ContactInput. Documents stored under the old shape upgrade via
upgradeContactData(see below). phoneNumbersentries can now carrydigits,countryCode, and anid;
emailAddressesentries can carry anid. All are optional, so an importer
that has none of them produces the same output as before. Note the caveat
documented on the fields: like the contact-levelnativeId, a sub-recordid
can churn when a platform re-aggregates raw contacts across accounts, so treat
it as a diff hint rather than permanent identity.ContactInputphone / email entries now take an optional, nullablenumber/
email, so an importer can pass a source's entries straight through without
pre-filtering.normalizeContacttrims them and DROPS entries that are empty
afterwards -- and it now does so before deciding whether the contact has any
contact method at all, so a placeholder entry can no longer keep a nameless
contact that has nothing to reach it by. Callers that relied on empty entries
surviving normalization will see shorter arrays.isContactDatachecks the new field spellings and the new optional sub-record
fields. It ignores fields it does not know, so a document stored under the old
spellings still validates: decrypt, validate, and upgrade in whichever order
suits the consumer.- Documented
ContactInput.birthday.monthas 1-indexed (1 = January) at the
contract boundary.normalizeContactrequiresmonth > 0, so an importer
whose source uses the 0-indexed JSDateconvention must add 1 before
calling; passing a raw 0-indexed month silently drops every January birthday
and shifts the rest back by one.
Added
upgradeContactData, plus theupgradeContactHeadPayload/
upgradeContactRevisionPayloadconveniences, for the read path: renames
postCode/poboxand foldsstateintoregionwhenregionis absent.
Apply it when loading stored head / revision documents so pre-change documents
compare cleanly against a fresh import and last-write-wins sees no spurious
edit. Input already in the current shape is returned unchanged, so it is
idempotent and safe to apply on every load.
v0.2.0
Added
getDidshelper returning a contact's DIDs fromurlAddresses, matching on
thedid:scheme rather than the entry label (native address books do not
reliably round-trip custom labels), andunmangleDidUrlto tolerate the
http(s)://prefix some contacts apps prepend to edited URLs.
v0.1.1
Release v0.1.1 Signed-off-by: Dmitri Zagidulin <dzagidulin@gmail.com>