Gate joinInvite and navigateDeepLink with shared paste validation (P410-30).
Reject malformed invites and deep links at the platform layer using validatePastedNavigationInput before mesh join or navigation side effects. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -12,6 +12,8 @@ Application facade: one `PearcordPlatform` class that wires identity, database,
|
||||
**Phase 410 hardening (v0.8.643):** `view()` now supports explicit detail levels (`minimal`/`light`/`full`/`diagnostic`) through `opts.detail`, keeps legacy `opts.light` compatibility, and surfaces the resolved mode as `viewDetailLevel` in payload diagnostics.
|
||||
**Phase 410 hardening (v0.8.643):** `guildSyncHealthRail` now carries bounded-retry metadata (`retryAttempt`, `retryDelayMs`, `retryMaxAttempts`, `nextRetryAt`, `retryCountdownMs`) so UI can show live “mesh bounded mode” retry countdown/tooltip state during background join retries.
|
||||
**Phase 410 hardening (v0.8.643):** Guild mesh peer retry scheduling now uses `_computeGuildMeshPeerRetryDelayMs` (last-peer-contact staleness + RTT bounds) instead of fixed attempt-index spacing; health rows track `lastPeerSeenAt` on peer join.
|
||||
**Phase 410 hardening (P410-20):** Repeated slow `guild.open` spans arm `guildOpenFallbackThrottle` (rolling window + cooldown): shorter bounded mesh cap, deferred emoji/archive/member-recovery fallbacks, reduced member bootstrap sync wait; view + `guildSyncHealthRail.openFallbackThrottled`. Smoke: `npm run test:platform-guild-open-fallback-throttle`.
|
||||
|
||||
**Phase 410 hardening (v0.8.643):** `_requestGuildMeshJoin` coalesces duplicate guild mesh joins during `guild.open`, peer retry, and startup/switch races; stale `loadGen` joins are skipped; `pearcord-guild#joinMesh` deduplicates in-flight `swarm.join`. Smoke: `npm run test:platform-guild-mesh-join-coalesce`.
|
||||
**Phase 410 hardening (v0.8.643):** `_evictStaleGuildMeshPeers` drops idle hyperswarm sockets (env `PEARCORD_GUILD_MESH_PEER_STALE_MS`, periodic sweep) and records `peerRetryAttempt`, `lastPeerSeenAt`, and eviction totals on `guildSyncHealthRail`. Smoke: `npm run test:platform-guild-mesh-stale-peer-evict`.
|
||||
**Phase 410 hardening (v0.8.643):** `_auditChannelViewConsistency` runs during `view()` to detect pin/reaction/message mismatches, repair stale pin/reaction list caches, and expose `channelConsistencyAudit` for diagnostics. Smoke: `npm run test:platform-channel-consistency-audit`.
|
||||
|
||||
@@ -231,6 +231,8 @@ const {
|
||||
contentHasChannelMention,
|
||||
extractFirstUrl,
|
||||
parsePearcordDeepLink,
|
||||
validatePastedNavigationInput,
|
||||
pasteValidationToast,
|
||||
formatPearcordDeepLink,
|
||||
formatGuildDeepLink: buildGuildDeepLink,
|
||||
formatInviteDeepLink: buildInviteDeepLink,
|
||||
@@ -508,6 +510,109 @@ class PearcordPlatform extends EventEmitter {
|
||||
this._lastPartitionHealByGuild = new Map()
|
||||
this._partitionHealInProgressByGuild = new Set()
|
||||
this._partitionHealCooldownUntilByGuild = new Map()
|
||||
this._guildOpenDurationSamples = []
|
||||
this._guildOpenFallbackThrottledUntil = 0
|
||||
}
|
||||
|
||||
_getGuildOpenSlowThresholdMs () {
|
||||
return Math.max(
|
||||
500,
|
||||
Number(process.env.PEARCORD_GUILD_OPEN_SLOW_MS) || 3000
|
||||
)
|
||||
}
|
||||
|
||||
_getGuildOpenThrottleWindowMs () {
|
||||
return Math.max(
|
||||
10000,
|
||||
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_WINDOW_MS) || 120000
|
||||
)
|
||||
}
|
||||
|
||||
_getGuildOpenThrottleTriggerCount () {
|
||||
return Math.max(
|
||||
1,
|
||||
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_COUNT) || 2
|
||||
)
|
||||
}
|
||||
|
||||
_getGuildOpenThrottleCooldownMs () {
|
||||
return Math.max(
|
||||
5000,
|
||||
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_COOLDOWN_MS) || 60000
|
||||
)
|
||||
}
|
||||
|
||||
_guildOpenSlowCountInWindow () {
|
||||
const now = Date.now()
|
||||
const windowMs = this._getGuildOpenThrottleWindowMs()
|
||||
const thresholdMs = this._getGuildOpenSlowThresholdMs()
|
||||
return this._guildOpenDurationSamples.filter(
|
||||
(s) => now - s.at <= windowMs && s.durationMs >= thresholdMs
|
||||
).length
|
||||
}
|
||||
|
||||
_isGuildOpenFallbackThrottled () {
|
||||
return Date.now() < (this._guildOpenFallbackThrottledUntil || 0)
|
||||
}
|
||||
|
||||
_guildOpenFallbackThrottleRemainingMs () {
|
||||
return Math.max(0, (this._guildOpenFallbackThrottledUntil || 0) - Date.now())
|
||||
}
|
||||
|
||||
getGuildOpenFallbackThrottle () {
|
||||
return {
|
||||
active: this._isGuildOpenFallbackThrottled(),
|
||||
until: this._guildOpenFallbackThrottledUntil || 0,
|
||||
remainingMs: this._guildOpenFallbackThrottleRemainingMs(),
|
||||
slowCount: this._guildOpenSlowCountInWindow(),
|
||||
thresholdMs: this._getGuildOpenSlowThresholdMs(),
|
||||
windowMs: this._getGuildOpenThrottleWindowMs(),
|
||||
triggerCount: this._getGuildOpenThrottleTriggerCount(),
|
||||
cooldownMs: this._getGuildOpenThrottleCooldownMs()
|
||||
}
|
||||
}
|
||||
|
||||
_recordGuildOpenDuration (guildId, durationMs) {
|
||||
const at = Date.now()
|
||||
const windowMs = this._getGuildOpenThrottleWindowMs()
|
||||
this._guildOpenDurationSamples.push({
|
||||
guildId: guildId || null,
|
||||
durationMs: Math.max(0, Number(durationMs) || 0),
|
||||
at
|
||||
})
|
||||
const cutoff = at - windowMs
|
||||
while (
|
||||
this._guildOpenDurationSamples.length &&
|
||||
this._guildOpenDurationSamples[0].at < cutoff
|
||||
) {
|
||||
this._guildOpenDurationSamples.shift()
|
||||
}
|
||||
const maxSamples = Math.max(20, this._getGuildOpenThrottleTriggerCount() * 4)
|
||||
if (this._guildOpenDurationSamples.length > maxSamples) {
|
||||
this._guildOpenDurationSamples.splice(
|
||||
0,
|
||||
this._guildOpenDurationSamples.length - maxSamples
|
||||
)
|
||||
}
|
||||
const thresholdMs = this._getGuildOpenSlowThresholdMs()
|
||||
if (durationMs >= thresholdMs) {
|
||||
const slowCount = this._guildOpenSlowCountInWindow()
|
||||
if (slowCount >= this._getGuildOpenThrottleTriggerCount()) {
|
||||
const until = at + this._getGuildOpenThrottleCooldownMs()
|
||||
if (until > (this._guildOpenFallbackThrottledUntil || 0)) {
|
||||
this._guildOpenFallbackThrottledUntil = until
|
||||
this.log.info('guild.open fallback throttle armed', {
|
||||
spanKind: 'guild.open.fallback-throttle-arm',
|
||||
guildId: guildId || null,
|
||||
durationMs,
|
||||
slowCount,
|
||||
thresholdMs,
|
||||
until,
|
||||
cooldownMs: this._getGuildOpenThrottleCooldownMs()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isGuildMeshBounded (guildId) {
|
||||
@@ -666,7 +771,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
Number(this._guildMeshStalePeerEvictedTotalByGuild.get(id)) || 0,
|
||||
lastStalePeerEvictAt: Number(h?.lastStalePeerEvictAt) || 0,
|
||||
retrySpacingSource:
|
||||
this._guildMeshPeerRetryMetaByGuild.get(id)?.spacingSource || null
|
||||
this._guildMeshPeerRetryMetaByGuild.get(id)?.spacingSource || null,
|
||||
openFallbackThrottled:
|
||||
this._isGuildOpenFallbackThrottled() &&
|
||||
(id === this.guild?.guild?.id || id === this._guildLoadingGuildId),
|
||||
openFallbackThrottleRemainingMs: this._guildOpenFallbackThrottleRemainingMs()
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -2025,6 +2134,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
const force = opts.force === true
|
||||
const loadGen = opts.loadGen ?? null
|
||||
const bounded = opts.bounded !== false
|
||||
const fallbackThrottled = opts.fallbackThrottled === true
|
||||
const g = this.guild
|
||||
if (!g?.guild) {
|
||||
return { peers: 0, bounded: false, skipped: true, reason: 'no-guild' }
|
||||
@@ -2065,19 +2175,32 @@ class PearcordPlatform extends EventEmitter {
|
||||
force,
|
||||
loadGen,
|
||||
bounded,
|
||||
guildId
|
||||
guildId,
|
||||
fallbackThrottled
|
||||
})
|
||||
this._guildMeshJoinCoalesce = { guildId, topic, guild: g, loadGen, promise }
|
||||
return promise
|
||||
}
|
||||
|
||||
async _executeGuildMeshJoin ({ source, force, loadGen, bounded, guildId }) {
|
||||
async _executeGuildMeshJoin ({
|
||||
source,
|
||||
force,
|
||||
loadGen,
|
||||
bounded,
|
||||
guildId,
|
||||
fallbackThrottled = false
|
||||
}) {
|
||||
const meshFlushMs = Number(
|
||||
process.env.PEARCORD_GUILD_SWARM_FLUSH_MS ??
|
||||
process.env.PEARCORD_SWARM_FLUSH_MS ??
|
||||
2500
|
||||
)
|
||||
const joinMeshCapMs = meshFlushMs + 5000
|
||||
const joinMeshCapMs = fallbackThrottled
|
||||
? Math.max(
|
||||
500,
|
||||
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_MESH_CAP_MS) || 1500
|
||||
)
|
||||
: meshFlushMs + 5000
|
||||
try {
|
||||
const g = this.guild
|
||||
if (!g?.guild || g.guild.id !== guildId) {
|
||||
@@ -13417,8 +13540,21 @@ class PearcordPlatform extends EventEmitter {
|
||||
}
|
||||
|
||||
async _openGuild (guildRecord, loadGen = null) {
|
||||
const span = this.log.time('guild.open', { guildId: guildRecord.id })
|
||||
const openStartedAt = Date.now()
|
||||
const fallbackThrottled = this._isGuildOpenFallbackThrottled()
|
||||
const span = this.log.time('guild.open', {
|
||||
guildId: guildRecord.id,
|
||||
fallbackThrottled
|
||||
})
|
||||
try {
|
||||
if (fallbackThrottled) {
|
||||
this.log.info('guild.open fallback-throttled', {
|
||||
spanKind: 'guild.open.fallback-throttle',
|
||||
guildId: guildRecord.id,
|
||||
remainingMs: this._guildOpenFallbackThrottleRemainingMs(),
|
||||
slowCount: this._guildOpenSlowCountInWindow()
|
||||
})
|
||||
}
|
||||
const leaveSpan = this.log.time('guild.open.leaveMeshes')
|
||||
await this._leaveMeshes()
|
||||
if (!this._isGuildLoadCurrent(loadGen)) {
|
||||
@@ -13447,7 +13583,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
await this._requestGuildMeshJoin({
|
||||
source: 'guild-open',
|
||||
loadGen,
|
||||
bounded: true
|
||||
bounded: true,
|
||||
fallbackThrottled
|
||||
})
|
||||
} catch (err) {
|
||||
this.log.error('guild.mesh error', {
|
||||
@@ -13501,30 +13638,46 @@ class PearcordPlatform extends EventEmitter {
|
||||
})
|
||||
}, 0)
|
||||
this._startAuditExportScheduleTimer()
|
||||
void this._maybeArchiveHealthRefreshOnFocus().catch(() => {})
|
||||
this._restartArchiveHealthRefreshTimer()
|
||||
if (this.activeChannelId && !this._guildOpenNoViewableChannels) {
|
||||
if (!fallbackThrottled) {
|
||||
void this._maybeArchiveHealthRefreshOnFocus().catch(() => {})
|
||||
this._restartArchiveHealthRefreshTimer()
|
||||
}
|
||||
if (
|
||||
this.activeChannelId &&
|
||||
!this._guildOpenNoViewableChannels &&
|
||||
!fallbackThrottled
|
||||
) {
|
||||
void this._prefetchEmojiAttachmentsAfterOpen().catch((err) => {
|
||||
this.log.debug('emoji batch prefetch after guild open failed', { err })
|
||||
})
|
||||
}
|
||||
if (guildRecord.ownerId !== user.id) {
|
||||
const hostPk = await this._lookupGuildHostPublicKey(guildRecord)
|
||||
const syncWaitMs = fallbackThrottled
|
||||
? Math.max(
|
||||
500,
|
||||
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_SYNC_WAIT_MS) || 2000
|
||||
)
|
||||
: 18000
|
||||
void this._bootstrapGuildSessionAfterJoin({
|
||||
invite: hostPk
|
||||
? { creatorId: guildRecord.ownerId, creatorPublicKey: hostPk }
|
||||
: null,
|
||||
hostPublicKey: hostPk,
|
||||
skipMeshJoin: true,
|
||||
syncWaitMs: 18000
|
||||
syncWaitMs
|
||||
}).catch((err) => {
|
||||
this.log.warn('guild.open member sync bootstrap failed', {
|
||||
guildId: guildRecord.id,
|
||||
err: err?.message || String(err)
|
||||
})
|
||||
})
|
||||
this._scheduleGuildMemberSyncRecovery(guildRecord.id, hostPk)
|
||||
if (!fallbackThrottled) {
|
||||
this._scheduleGuildMemberSyncRecovery(guildRecord.id, hostPk)
|
||||
}
|
||||
}
|
||||
const durationMs = Date.now() - openStartedAt
|
||||
this._recordGuildOpenDuration(guildRecord.id, durationMs)
|
||||
span.end({
|
||||
guildId: guildRecord.id,
|
||||
channels: channels.length,
|
||||
@@ -13533,9 +13686,14 @@ class PearcordPlatform extends EventEmitter {
|
||||
activeChannelId: this.activeChannelId,
|
||||
guildCount: (this.guilds || []).length,
|
||||
noViewableChannels: !!this._guildOpenNoViewableChannels,
|
||||
durationMs,
|
||||
fallbackThrottled,
|
||||
openFallbackThrottleActive: this._isGuildOpenFallbackThrottled(),
|
||||
spanKind: 'guild.open',
|
||||
logLevel:
|
||||
this._guildOpenNoViewableChannels || this._isGuildMeshBounded(guildRecord.id)
|
||||
this._guildOpenNoViewableChannels ||
|
||||
this._isGuildMeshBounded(guildRecord.id) ||
|
||||
fallbackThrottled
|
||||
? 'info'
|
||||
: undefined
|
||||
})
|
||||
@@ -13947,7 +14105,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
}
|
||||
|
||||
async joinInvite (code) {
|
||||
const raw = String(code || '').trim()
|
||||
const validated = validatePastedNavigationInput(code)
|
||||
if (!validated.ok) {
|
||||
throw new Error(pasteValidationToast(validated))
|
||||
}
|
||||
const raw = validated.normalized
|
||||
if (raw.toLowerCase().startsWith('pearcord://')) {
|
||||
return this.navigateDeepLink(raw)
|
||||
}
|
||||
@@ -14325,8 +14487,12 @@ class PearcordPlatform extends EventEmitter {
|
||||
}
|
||||
|
||||
async navigateDeepLink (url) {
|
||||
const parsed = parsePearcordDeepLink(url)
|
||||
if (!parsed) throw new Error('invalid pearcord deep link')
|
||||
const validated = validatePastedNavigationInput(url)
|
||||
if (!validated.ok) {
|
||||
throw new Error(pasteValidationToast(validated))
|
||||
}
|
||||
const parsed = validated.parsed || parsePearcordDeepLink(validated.normalized)
|
||||
if (!parsed) throw new Error(pasteValidationToast({ ok: false, code: 'unrecognized_deep_link' }))
|
||||
|
||||
if (parsed.kind === 'explore') {
|
||||
const preserveBaseline =
|
||||
@@ -21866,6 +22032,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
sessionError: this._sessionError,
|
||||
guildLoading: this._guildLoading,
|
||||
guildLoadingGuildId: this._guildLoadingGuildId,
|
||||
guildOpenFallbackThrottle: this.getGuildOpenFallbackThrottle(),
|
||||
user,
|
||||
mode: this.mode,
|
||||
guilds: this.guilds,
|
||||
|
||||
Reference in New Issue
Block a user