@oxyhq/contracts 0.19.0 → 0.21.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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/deviceSession.js +62 -1
- package/dist/cjs/index.js +104 -3
- package/dist/cjs/moderationReputation.js +298 -0
- package/dist/cjs/reputation.js +297 -0
- package/dist/cjs/userInvalidation.js +89 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/deviceSession.js +61 -0
- package/dist/esm/index.js +26 -1
- package/dist/esm/moderationReputation.js +295 -0
- package/dist/esm/reputation.js +293 -0
- package/dist/esm/userInvalidation.js +85 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/deviceSession.d.ts +85 -0
- package/dist/types/index.d.ts +8 -2
- package/dist/types/keyRecovery.d.ts +6 -6
- package/dist/types/moderationReputation.d.ts +487 -0
- package/dist/types/reputation.d.ts +457 -0
- package/dist/types/userInvalidation.d.ts +94 -0
- package/package.json +1 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Oxy Trust — reputation API contracts.
|
|
4
|
+
*
|
|
5
|
+
* SINGLE SOURCE OF TRUTH for the reputation ledger's wire shapes: the closed
|
|
6
|
+
* value sets (`REPUTATION_CATEGORIES`, `TRUST_TIERS`, …), the response entities
|
|
7
|
+
* (`ReputationTransaction`, the two balance views, `ReputationDispute`,
|
|
8
|
+
* `ReputationRule`, the leaderboard entry) and the request bodies the write
|
|
9
|
+
* endpoints accept. The API validates its OUTPUT against these schemas and its
|
|
10
|
+
* INPUT with the same request schemas the SDK's input types are derived from;
|
|
11
|
+
* `@oxyhq/core`'s reputation mixin imports every type from here rather than
|
|
12
|
+
* declaring its own.
|
|
13
|
+
*
|
|
14
|
+
* Why this module exists: the balance endpoint was view-split server-side
|
|
15
|
+
* without the SDK type moving with it, and for hours the SDK affirmatively
|
|
16
|
+
* type-checked a read of `balance.reliability.reportAccuracyScore` against a
|
|
17
|
+
* response that no longer carried `reliability`. Nothing structural connected
|
|
18
|
+
* the API's hand-written serializers (which returned `Record<string, unknown>`)
|
|
19
|
+
* to the SDK's interfaces — only human attention. With the serializers
|
|
20
|
+
* annotated against these definitions, that divergence is a build failure.
|
|
21
|
+
*
|
|
22
|
+
* Design anchors:
|
|
23
|
+
* - **Ids are strings, timestamps are ISO 8601 strings.** The server holds
|
|
24
|
+
* `ObjectId`s and `Date`s; every serializer converts at the boundary, so a
|
|
25
|
+
* `Date` leaking into a field this module types as `string` fails to compile.
|
|
26
|
+
* - **The balance has two views, and the union is the contract.** See
|
|
27
|
+
* {@link ReputationBalanceView} — the compile-time assertions below are what
|
|
28
|
+
* stop the private view's fields becoming reachable on a stranger's balance.
|
|
29
|
+
* - **The closed value sets live here, not beside the mongoose models.** The
|
|
30
|
+
* API's model enums and the SDK's unions are the same `as const` tuple, so a
|
|
31
|
+
* seventh category cannot be added on one side only.
|
|
32
|
+
*
|
|
33
|
+
* The response entities are declared as explicit `interface`s with their runtime
|
|
34
|
+
* schemas annotated `z.ZodType<Interface>`, following `./links` and
|
|
35
|
+
* `./userResponse`: a `z.infer<>` of a nested-object schema can degrade to `{}`
|
|
36
|
+
* under a consumer's `moduleResolution: "node"` (node10) resolution, while a
|
|
37
|
+
* literal interface emits the field types verbatim in the `.d.ts` and survives
|
|
38
|
+
* both `node` and `bundler`.
|
|
39
|
+
*
|
|
40
|
+
* Platform-agnostic — zod only, no react/react-native/expo. ESM-safe (no
|
|
41
|
+
* `require()`).
|
|
42
|
+
*/
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.reverseReputationTransactionSchema = exports.upsertReputationRuleSchema = exports.resolveReputationDisputeSchema = exports.createReputationDisputeSchema = exports.awardReputationSchema = exports.reverseReputationTransactionResultSchema = exports.reputationInfluenceResultSchema = exports.reputationLeaderboardEntrySchema = exports.reputationLeaderboardUserSchema = exports.reputationRuleSchema = exports.reputationDisputeSchema = exports.reputationBalanceSchema = exports.reputationBalanceSummarySchema = exports.reputationReliabilitySchema = exports.reputationInfluenceSchema = exports.reputationBalanceBreakdownSchema = exports.reputationTransactionSchema = exports.reputationInfluenceContextSchema = exports.REPUTATION_INFLUENCE_CONTEXTS = exports.reputationDisputeStatusSchema = exports.REPUTATION_DISPUTE_STATUSES = exports.reputationTargetEntityTypeSchema = exports.REPUTATION_TARGET_ENTITY_TYPES = exports.trustTierSchema = exports.TRUST_TIERS = exports.reputationTransactionStatusSchema = exports.REPUTATION_TRANSACTION_STATUSES = exports.reputationCategorySchema = exports.REPUTATION_CATEGORIES = void 0;
|
|
45
|
+
exports.isFullReputationBalance = isFullReputationBalance;
|
|
46
|
+
const zod_1 = require("zod");
|
|
47
|
+
const userResponse_1 = require("./userResponse");
|
|
48
|
+
const moderationReputation_1 = require("./moderationReputation");
|
|
49
|
+
/* -------------------------------------------------------------------------- */
|
|
50
|
+
/* Closed value sets */
|
|
51
|
+
/* -------------------------------------------------------------------------- */
|
|
52
|
+
/**
|
|
53
|
+
* Category bucket a reputation transaction falls into. Drives the per-category
|
|
54
|
+
* balance breakdown; every rule and transaction carries exactly one.
|
|
55
|
+
*
|
|
56
|
+
* - `content` — posts, comments, media a user authored.
|
|
57
|
+
* - `social` — follows, likes, social interactions.
|
|
58
|
+
* - `trust` — identity / verification / trust-graph signals.
|
|
59
|
+
* - `moderation` — reports filed, moderation actions, review outcomes.
|
|
60
|
+
* - `physical` — real-world signals (event check-ins, verified purchases).
|
|
61
|
+
* - `penalty` — negative adjustments for abuse / policy violations.
|
|
62
|
+
* - `other` — anything that does not fit the buckets above.
|
|
63
|
+
*/
|
|
64
|
+
exports.REPUTATION_CATEGORIES = [
|
|
65
|
+
'content',
|
|
66
|
+
'social',
|
|
67
|
+
'trust',
|
|
68
|
+
'moderation',
|
|
69
|
+
'physical',
|
|
70
|
+
'penalty',
|
|
71
|
+
'other',
|
|
72
|
+
];
|
|
73
|
+
exports.reputationCategorySchema = zod_1.z.enum(exports.REPUTATION_CATEGORIES);
|
|
74
|
+
/**
|
|
75
|
+
* Transaction lifecycle status.
|
|
76
|
+
*
|
|
77
|
+
* - `active` — counts toward the balance.
|
|
78
|
+
* - `disputed` — under dispute; still counts until the dispute resolves.
|
|
79
|
+
* - `reversed` — superseded by a compensating reversal transaction; excluded.
|
|
80
|
+
* - `voided` — administratively excluded with no compensating entry.
|
|
81
|
+
*/
|
|
82
|
+
exports.REPUTATION_TRANSACTION_STATUSES = [
|
|
83
|
+
'active',
|
|
84
|
+
'disputed',
|
|
85
|
+
'reversed',
|
|
86
|
+
'voided',
|
|
87
|
+
];
|
|
88
|
+
exports.reputationTransactionStatusSchema = zod_1.z.enum(exports.REPUTATION_TRANSACTION_STATUSES);
|
|
89
|
+
/**
|
|
90
|
+
* Trust tiers, lowest → highest trust, plus the punitive `restricted`.
|
|
91
|
+
*
|
|
92
|
+
* Publicly visible: this is the contribution ladder the reputation system
|
|
93
|
+
* exists to publish. Note it doubles as the sanction marker — a `restricted`
|
|
94
|
+
* account is publicly identifiable as such.
|
|
95
|
+
*/
|
|
96
|
+
exports.TRUST_TIERS = ['restricted', 'new', 'trusted', 'high_trust', 'verified'];
|
|
97
|
+
exports.trustTierSchema = zod_1.z.enum(exports.TRUST_TIERS);
|
|
98
|
+
/** Kind of entity a transaction may target. */
|
|
99
|
+
exports.REPUTATION_TARGET_ENTITY_TYPES = [
|
|
100
|
+
'post',
|
|
101
|
+
'comment',
|
|
102
|
+
'report',
|
|
103
|
+
'purchase',
|
|
104
|
+
'event',
|
|
105
|
+
'check_in',
|
|
106
|
+
'manual_review',
|
|
107
|
+
'user',
|
|
108
|
+
'other',
|
|
109
|
+
];
|
|
110
|
+
exports.reputationTargetEntityTypeSchema = zod_1.z.enum(exports.REPUTATION_TARGET_ENTITY_TYPES);
|
|
111
|
+
/** Dispute lifecycle status. */
|
|
112
|
+
exports.REPUTATION_DISPUTE_STATUSES = [
|
|
113
|
+
'open',
|
|
114
|
+
'accepted',
|
|
115
|
+
'rejected',
|
|
116
|
+
'needs_review',
|
|
117
|
+
];
|
|
118
|
+
exports.reputationDisputeStatusSchema = zod_1.z.enum(exports.REPUTATION_DISPUTE_STATUSES);
|
|
119
|
+
/** Influence context selecting which capped weight axis to read. */
|
|
120
|
+
exports.REPUTATION_INFLUENCE_CONTEXTS = [
|
|
121
|
+
'default',
|
|
122
|
+
'report',
|
|
123
|
+
'moderation',
|
|
124
|
+
'ranking',
|
|
125
|
+
];
|
|
126
|
+
exports.reputationInfluenceContextSchema = zod_1.z.enum(exports.REPUTATION_INFLUENCE_CONTEXTS);
|
|
127
|
+
exports.reputationTransactionSchema = zod_1.z.object({
|
|
128
|
+
id: zod_1.z.string(),
|
|
129
|
+
userId: zod_1.z.string(),
|
|
130
|
+
points: zod_1.z.number(),
|
|
131
|
+
actionType: zod_1.z.string(),
|
|
132
|
+
category: exports.reputationCategorySchema,
|
|
133
|
+
applicationId: zod_1.z.string().optional(),
|
|
134
|
+
credentialId: zod_1.z.string().optional(),
|
|
135
|
+
sourceActionId: zod_1.z.string().optional(),
|
|
136
|
+
sourceActionType: zod_1.z.string().optional(),
|
|
137
|
+
targetEntityId: zod_1.z.string().optional(),
|
|
138
|
+
targetEntityType: exports.reputationTargetEntityTypeSchema.optional(),
|
|
139
|
+
status: exports.reputationTransactionStatusSchema,
|
|
140
|
+
reversedTransactionId: zod_1.z.string().optional(),
|
|
141
|
+
reason: zod_1.z.string().optional(),
|
|
142
|
+
metadata: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
143
|
+
createdByUserId: zod_1.z.string().optional(),
|
|
144
|
+
reviewedByUserId: zod_1.z.string().optional(),
|
|
145
|
+
reviewedAt: zod_1.z.string().optional(),
|
|
146
|
+
createdAt: zod_1.z.string(),
|
|
147
|
+
updatedAt: zod_1.z.string(),
|
|
148
|
+
});
|
|
149
|
+
exports.reputationBalanceBreakdownSchema = zod_1.z.object({
|
|
150
|
+
content: zod_1.z.number(),
|
|
151
|
+
social: zod_1.z.number(),
|
|
152
|
+
trust: zod_1.z.number(),
|
|
153
|
+
moderation: zod_1.z.number(),
|
|
154
|
+
physical: zod_1.z.number(),
|
|
155
|
+
penalties: zod_1.z.number(),
|
|
156
|
+
});
|
|
157
|
+
exports.reputationInfluenceSchema = zod_1.z.object({
|
|
158
|
+
defaultWeight: zod_1.z.number(),
|
|
159
|
+
reportWeight: zod_1.z.number(),
|
|
160
|
+
moderationWeight: zod_1.z.number(),
|
|
161
|
+
rankingFeedbackWeight: zod_1.z.number(),
|
|
162
|
+
});
|
|
163
|
+
exports.reputationReliabilitySchema = zod_1.z.object({
|
|
164
|
+
accurateReports: zod_1.z.number(),
|
|
165
|
+
rejectedReports: zod_1.z.number(),
|
|
166
|
+
reportAccuracyScore: zod_1.z.number(),
|
|
167
|
+
abuseScore: zod_1.z.number(),
|
|
168
|
+
});
|
|
169
|
+
/** The fields both balance views share. Kept as a shape so the full view can spread it. */
|
|
170
|
+
const balanceSummaryShape = {
|
|
171
|
+
userId: zod_1.z.string(),
|
|
172
|
+
total: zod_1.z.number(),
|
|
173
|
+
trustTier: exports.trustTierSchema,
|
|
174
|
+
};
|
|
175
|
+
exports.reputationBalanceSummarySchema = zod_1.z.object(balanceSummaryShape);
|
|
176
|
+
exports.reputationBalanceSchema = zod_1.z.object({
|
|
177
|
+
...balanceSummaryShape,
|
|
178
|
+
positive: zod_1.z.number(),
|
|
179
|
+
negative: zod_1.z.number(),
|
|
180
|
+
breakdown: exports.reputationBalanceBreakdownSchema,
|
|
181
|
+
influence: exports.reputationInfluenceSchema,
|
|
182
|
+
reliability: exports.reputationReliabilitySchema,
|
|
183
|
+
recalculatedAt: zod_1.z.string(),
|
|
184
|
+
updatedAt: zod_1.z.string(),
|
|
185
|
+
personhood: moderationReputation_1.reputationPersonhoodSchema.optional(),
|
|
186
|
+
contribution: moderationReputation_1.reputationContributionSchema.optional(),
|
|
187
|
+
conduct: moderationReputation_1.reputationConductSchema.optional(),
|
|
188
|
+
reporting: moderationReputation_1.reputationReportingSchema.optional(),
|
|
189
|
+
reviewing: moderationReputation_1.reputationReviewingSchema.optional(),
|
|
190
|
+
contextualInfluence: moderationReputation_1.reputationContextualInfluenceSchema.optional(),
|
|
191
|
+
});
|
|
192
|
+
/**
|
|
193
|
+
* The fields the full {@link ReputationBalance} carries beyond the public
|
|
194
|
+
* {@link ReputationBalanceSummary} that the API sends ALL-OR-NOTHING. The
|
|
195
|
+
* runtime discriminant between the two views.
|
|
196
|
+
*
|
|
197
|
+
* The V2 blocks (`conduct`, `contribution`, …) are deliberately NOT listed:
|
|
198
|
+
* they are optional on the wire, so requiring them here would make a balance
|
|
199
|
+
* from a server that predates them fail to narrow, hiding the whole private
|
|
200
|
+
* view. Read a V2 block by checking that block.
|
|
201
|
+
*/
|
|
202
|
+
const FULL_BALANCE_FIELDS = [
|
|
203
|
+
'positive',
|
|
204
|
+
'negative',
|
|
205
|
+
'breakdown',
|
|
206
|
+
'influence',
|
|
207
|
+
'reliability',
|
|
208
|
+
'recalculatedAt',
|
|
209
|
+
'updatedAt',
|
|
210
|
+
];
|
|
211
|
+
/**
|
|
212
|
+
* Whether a balance came back as the SUBJECT view, and so carries the
|
|
213
|
+
* breakdown / influence / reliability blocks.
|
|
214
|
+
*
|
|
215
|
+
* Checks every extra field rather than one representative: the point of the
|
|
216
|
+
* guard is that the caller then dereferences those blocks, so a partial payload
|
|
217
|
+
* must not narrow.
|
|
218
|
+
*
|
|
219
|
+
* @param balance - A balance from `getReputationBalance`.
|
|
220
|
+
*/
|
|
221
|
+
function isFullReputationBalance(balance) {
|
|
222
|
+
return FULL_BALANCE_FIELDS.every((field) => field in balance);
|
|
223
|
+
}
|
|
224
|
+
exports.reputationDisputeSchema = zod_1.z.object({
|
|
225
|
+
id: zod_1.z.string(),
|
|
226
|
+
transactionId: zod_1.z.string(),
|
|
227
|
+
userId: zod_1.z.string(),
|
|
228
|
+
reason: zod_1.z.string(),
|
|
229
|
+
status: exports.reputationDisputeStatusSchema,
|
|
230
|
+
evidence: zod_1.z.array(zod_1.z.string()).optional(),
|
|
231
|
+
resolvedAt: zod_1.z.string().optional(),
|
|
232
|
+
resolvedByUserId: zod_1.z.string().optional(),
|
|
233
|
+
createdAt: zod_1.z.string(),
|
|
234
|
+
updatedAt: zod_1.z.string(),
|
|
235
|
+
});
|
|
236
|
+
exports.reputationRuleSchema = zod_1.z.object({
|
|
237
|
+
id: zod_1.z.string(),
|
|
238
|
+
actionType: zod_1.z.string(),
|
|
239
|
+
points: zod_1.z.number(),
|
|
240
|
+
category: exports.reputationCategorySchema,
|
|
241
|
+
description: zod_1.z.string(),
|
|
242
|
+
cooldownInMinutes: zod_1.z.number(),
|
|
243
|
+
isEnabled: zod_1.z.boolean(),
|
|
244
|
+
});
|
|
245
|
+
exports.reputationLeaderboardUserSchema = zod_1.z.object({
|
|
246
|
+
id: zod_1.z.string(),
|
|
247
|
+
username: zod_1.z.string(),
|
|
248
|
+
name: userResponse_1.userNameSchema,
|
|
249
|
+
avatar: zod_1.z.string().optional(),
|
|
250
|
+
publicKey: zod_1.z.string().optional(),
|
|
251
|
+
});
|
|
252
|
+
exports.reputationLeaderboardEntrySchema = zod_1.z.object({
|
|
253
|
+
user: exports.reputationLeaderboardUserSchema,
|
|
254
|
+
total: zod_1.z.number(),
|
|
255
|
+
trustTier: exports.trustTierSchema,
|
|
256
|
+
rank: zod_1.z.number(),
|
|
257
|
+
});
|
|
258
|
+
exports.reputationInfluenceResultSchema = zod_1.z.object({
|
|
259
|
+
context: exports.reputationInfluenceContextSchema,
|
|
260
|
+
weight: zod_1.z.number(),
|
|
261
|
+
influence: exports.reputationInfluenceSchema,
|
|
262
|
+
});
|
|
263
|
+
exports.reverseReputationTransactionResultSchema = zod_1.z.object({
|
|
264
|
+
original: exports.reputationTransactionSchema,
|
|
265
|
+
reversal: exports.reputationTransactionSchema,
|
|
266
|
+
});
|
|
267
|
+
exports.awardReputationSchema = zod_1.z.object({
|
|
268
|
+
userId: zod_1.z.string().trim().min(1),
|
|
269
|
+
actionType: zod_1.z.string().trim().min(1),
|
|
270
|
+
applicationId: zod_1.z.string().trim().min(1).optional(),
|
|
271
|
+
credentialId: zod_1.z.string().trim().min(1).optional(),
|
|
272
|
+
sourceActionId: zod_1.z.string().trim().min(1).optional(),
|
|
273
|
+
sourceActionType: zod_1.z.string().trim().min(1).optional(),
|
|
274
|
+
targetEntityId: zod_1.z.string().trim().min(1).optional(),
|
|
275
|
+
targetEntityType: exports.reputationTargetEntityTypeSchema.optional(),
|
|
276
|
+
reason: zod_1.z.string().trim().max(500).optional(),
|
|
277
|
+
metadata: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
278
|
+
});
|
|
279
|
+
exports.createReputationDisputeSchema = zod_1.z.object({
|
|
280
|
+
transactionId: zod_1.z.string().trim().min(1),
|
|
281
|
+
reason: zod_1.z.string().trim().min(1).max(1000),
|
|
282
|
+
evidence: zod_1.z.array(zod_1.z.string().trim().min(1)).max(20).optional(),
|
|
283
|
+
});
|
|
284
|
+
exports.resolveReputationDisputeSchema = zod_1.z.object({
|
|
285
|
+
status: zod_1.z.enum(['accepted', 'rejected']),
|
|
286
|
+
});
|
|
287
|
+
exports.upsertReputationRuleSchema = zod_1.z.object({
|
|
288
|
+
actionType: zod_1.z.string().trim().min(1),
|
|
289
|
+
points: zod_1.z.number(),
|
|
290
|
+
category: exports.reputationCategorySchema,
|
|
291
|
+
description: zod_1.z.string().trim().min(1).max(500),
|
|
292
|
+
cooldownInMinutes: zod_1.z.number().int().min(0).default(0),
|
|
293
|
+
isEnabled: zod_1.z.boolean().default(true),
|
|
294
|
+
});
|
|
295
|
+
exports.reverseReputationTransactionSchema = zod_1.z.object({
|
|
296
|
+
reason: zod_1.z.string().trim().max(500).optional(),
|
|
297
|
+
});
|
|
@@ -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
|
+
});
|