({ rotateSecret = false } = {})
| 753 | } |
| 754 | |
| 755 | async hello({ rotateSecret = false } = {}) { |
| 756 | if (!this.hubUrl) return { ok: false, error: 'no_hub_url' }; |
| 757 | |
| 758 | if (this._helloRateLimitUntil > Date.now()) { |
| 759 | const waitSec = Math.ceil((this._helloRateLimitUntil - Date.now()) / 1000); |
| 760 | this.logger.warn(`[lifecycle] hello suppressed: rate limited for ${waitSec}s`); |
| 761 | return { ok: false, error: 'hello_rate_limit_active', waitSec }; |
| 762 | } |
| 763 | |
| 764 | const waitMs = this._hubUnreachableWaitMs(); |
| 765 | if (waitMs > 0) { |
| 766 | return { |
| 767 | ok: false, |
| 768 | error: 'hub_unreachable_backoff', |
| 769 | retryAfterMs: waitMs, |
| 770 | }; |
| 771 | } |
| 772 | |
| 773 | const endpoint = `${this.hubUrl}/a2a/hello`; |
| 774 | const nodeId = this.store.getState('node_id') |
| 775 | || _readLegacyNodeId() |
| 776 | || `node_${crypto.randomBytes(6).toString('hex')}`; |
| 777 | |
| 778 | const payload = { capabilities: {} }; |
| 779 | if (rotateSecret) payload.rotate_secret = true; |
| 780 | |
| 781 | const fp = _getEnvFingerprint(); |
| 782 | |
| 783 | const body = { |
| 784 | ...buildEnvelope('hello', payload, nodeId), |
| 785 | env_fingerprint: fp, |
| 786 | }; |
| 787 | |
| 788 | try { |
| 789 | const res = await hubFetch(endpoint, { |
| 790 | method: 'POST', |
| 791 | headers: this._buildHeaders(), |
| 792 | body: JSON.stringify(body), |
| 793 | signal: AbortSignal.timeout(HELLO_TIMEOUT), |
| 794 | }); |
| 795 | await throwIfHubUnreachableResponse(res, 'lifecycle hello'); |
| 796 | this._recordHubReachable(); |
| 797 | if (!res.ok) { |
| 798 | const errText = await readHubResponseText(res).catch(() => ''); |
| 799 | let errData = {}; |
| 800 | try { errData = errText ? JSON.parse(errText) : {}; } catch (_) { /* non-JSON API error */ } |
| 801 | const errMsg = safeHubErrorCode(errData, res.status); |
| 802 | if (res.status === 429) { |
| 803 | const retryAfter = parseInt(res.headers.get('retry-after') || '3600', 10); |
| 804 | this._helloRateLimitUntil = Date.now() + retryAfter * 1000; |
| 805 | this.logger.error(`[lifecycle] hello rate limited (429): retry after ${retryAfter}s`); |
| 806 | return { ok: false, error: 'hello_rate_limited', retryAfter }; |
| 807 | } |
| 808 | const divergenceReason = (res.status === 401 || res.status === 403) |
| 809 | ? extractSecretDivergenceReason(errData) |
| 810 | : null; |
| 811 | if (divergenceReason) { |
| 812 | this.logger.warn( |
no test coverage detected