@oxyhq/contracts 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical contract for the Oxy user-invalidation broadcast.
4
+ *
5
+ * Oxy owns identity, but consumers cache it: Mention keeps a Redis summary per
6
+ * post author, and every backend using `@oxyhq/core` holds the SDK's own GET
7
+ * response cache. Both go stale the moment a profile is edited, and neither has
8
+ * any way to find out — the writer is a different process in a different repo.
9
+ * This is the signal that tells them.
10
+ *
11
+ * The channel name and the payload shape are wire contracts between oxy-api (the
12
+ * publisher) and every consuming backend (the subscribers), so they live here
13
+ * rather than in either side. A hand-typed copy of the channel name fails as
14
+ * "the invalidation never arrives" — silently, because pub/sub has no delivery
15
+ * receipt and a message nobody is listening for is indistinguishable from a
16
+ * message nobody sent.
17
+ *
18
+ * DELIVERY IS AT-MOST-ONCE, AND THAT IS THE DESIGN. Every consumer's cache still
19
+ * carries its own TTL, so a dropped message degrades to exactly the behaviour
20
+ * before this signal existed and never to something worse. That property is what
21
+ * makes a bare Redis PUBLISH sufficient here and an outbox, retries, delivery
22
+ * receipts and payload signatures unnecessary. Do not treat a received event as
23
+ * authoritative for anything except "re-read this user from Oxy".
24
+ *
25
+ * PRIVACY — the payload carries NO user data, only an id, a reason and a
26
+ * timestamp. The channel rides the shared Valkey that every Oxy backend can
27
+ * subscribe to, so anything placed on it is readable by every service in the
28
+ * ecosystem. Never add a name, handle, email, avatar or any profile field: a
29
+ * subscriber that wants the new values re-reads them from Oxy through its normal
30
+ * authenticated path, where the usual authorization applies.
31
+ *
32
+ * Platform-agnostic — zod only, no react/react-native/expo.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.oxyUserInvalidationEventSchema = exports.OXY_PUBLISHED_USER_CHANGE_REASONS = exports.OXY_USER_CHANGE_REASONS = exports.OXY_USER_INVALIDATION_CHANNEL = void 0;
36
+ exports.isPublishedOxyUserChangeReason = isPublishedOxyUserChangeReason;
37
+ const zod_1 = require("zod");
38
+ /** Redis pub/sub channel carrying user-invalidation events. */
39
+ exports.OXY_USER_INVALIDATION_CHANNEL = 'oxy:user:invalidate';
40
+ /**
41
+ * Why a user record changed, as classified by the writer in oxy-api.
42
+ *
43
+ * - `profile` — anything a consumer renders or caches as IDENTITY: display name,
44
+ * username, avatar, bio, verification, federation fields, account status. This
45
+ * is the DEFAULT for every writer, so a site that forgets to classify itself
46
+ * over-invalidates (correct, marginally slower) rather than under-invalidates
47
+ * (silently wrong). Keep that asymmetry if you add a reason.
48
+ * - `graph` — follow-edge churn only (follower/following counts). High frequency,
49
+ * and bulk follow/unfollow moves up to 200 edges in one call. Nothing renders
50
+ * identity from it and a stale count is harmless to ranking, so it is NOT
51
+ * broadcast — see {@link OXY_PUBLISHED_USER_CHANGE_REASONS}.
52
+ */
53
+ exports.OXY_USER_CHANGE_REASONS = ['profile', 'graph'];
54
+ /**
55
+ * The reasons that are actually put on the wire.
56
+ *
57
+ * A reason absent from this list is a local cache eviction in oxy-api and
58
+ * nothing more: no message is published at all, rather than a message every
59
+ * subscriber receives and discards. The distinction matters at bulk-follow
60
+ * scale, where the discarded variant is a 200-message burst on a channel every
61
+ * Oxy backend is subscribed to.
62
+ *
63
+ * This is deliberately a shared list rather than a check inside the publisher:
64
+ * a subscriber needs to know what it can receive, and the schema below rejects
65
+ * anything else, so publisher and subscriber cannot drift into disagreeing about
66
+ * which events exist. Adding a reason therefore forces an explicit decision about
67
+ * whether it broadcasts.
68
+ */
69
+ exports.OXY_PUBLISHED_USER_CHANGE_REASONS = ['profile'];
70
+ /** Whether a change of this kind is broadcast to consumers at all. */
71
+ function isPublishedOxyUserChangeReason(reason) {
72
+ return exports.OXY_PUBLISHED_USER_CHANGE_REASONS.includes(reason);
73
+ }
74
+ /**
75
+ * A single user-invalidation event.
76
+ *
77
+ * `at` is the publisher's epoch-ms clock, carried for diagnosis (measuring
78
+ * end-to-end propagation, spotting a wedged subscriber) — never for ordering or
79
+ * conflict resolution. Two Oxy tasks publish from unsynchronised clocks, and the
80
+ * event says only "re-read this user", which is idempotent and order-independent.
81
+ */
82
+ exports.oxyUserInvalidationEventSchema = zod_1.z.object({
83
+ /** The Oxy user whose record changed. */
84
+ userId: zod_1.z.string().min(1),
85
+ /** Why it changed. Only broadcast reasons appear on the wire. */
86
+ reason: zod_1.z.enum(exports.OXY_PUBLISHED_USER_CHANGE_REASONS),
87
+ /** Publisher's epoch-ms timestamp. Diagnostic only. */
88
+ at: zod_1.z.number().int().nonnegative(),
89
+ });
@@ -105,9 +105,23 @@ exports.userResponseSchema = zod_1.z
105
105
  * entry; present only when the account has verified at least one domain.
106
106
  */
107
107
  verifiedDomains: zod_1.z.array(identity_1.verifiedDomainSchema).optional(),
108
+ /**
109
+ * Account-graph classification — what KIND of account this is.
110
+ *
111
+ * ORTHOGONAL to `type` (`local` / `federated` / `agent` / `automated`),
112
+ * which says where the account lives and how it is driven; the two
113
+ * coexist and neither substitutes for the other. A `channel` is a
114
+ * publishing identity nobody can act as, so a consumer that renders
115
+ * authored content reads THIS to tell a channel's post from a person's.
116
+ *
117
+ * Optional because a DTO produced from a source that never carried the
118
+ * column omits it; absent should be read as `personal`, the column's
119
+ * default, not as unknown.
120
+ */
121
+ kind: accountGraph_1.accountKindSchema.optional(),
108
122
  /**
109
123
  * Real-estate / team taxonomy for `kind: 'organization'` accounts.
110
- * Absent on personal, project, and bot accounts.
124
+ * Absent on personal, project, bot, and channel accounts.
111
125
  */
112
126
  organizationCategory: accountGraph_1.organizationCategorySchema.optional(),
113
127
  /**
@@ -131,6 +145,12 @@ exports.userProfileUpdateSchema = zod_1.z
131
145
  .object({
132
146
  first: zod_1.z.string().optional(),
133
147
  last: zod_1.z.string().optional(),
148
+ /**
149
+ * Explicit display name, stored rather than composed. Wins over
150
+ * `first`/`last` when set; send `''` to clear it and fall back
151
+ * to the composed pair.
152
+ */
153
+ displayName: zod_1.z.string().optional(),
134
154
  })
135
155
  .optional(),
136
156
  username: zod_1.z.string().optional(),