From e4e7c8a029ae62fef56f88b6c53c21c26729fc0b Mon Sep 17 00:00:00 2001 From: rpone Date: Wed, 16 Sep 2026 03:07:35 +0800 Subject: [PATCH] feat(hypihub): identify requests with the distribution version Every HypiHub request now carries `user-agent: hypit/`, read from the distribution's own package.json. Wrapping the fetcher in the HypiHubClient constructor covers the API calls, OAuth token refreshes, multipart uploads and asset downloads, since they all run through `client.fetcher`. A manifest that is absent, unreadable or belongs to another package yields `hypit/unknown` rather than failing the module import. --- packages/provider-hypihub/src/provider.ts | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/provider-hypihub/src/provider.ts b/packages/provider-hypihub/src/provider.ts index 90700b0a..f7ee6623 100644 --- a/packages/provider-hypihub/src/provider.ts +++ b/packages/provider-hypihub/src/provider.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { requestDeadline } from "@hypit/runtime-kit"; import type { AsyncEndpoint, EndpointCredential, EndpointFulfillment, EndpointInvocationContext, EndpointPollContext, EndpointPricingReader, EndpointStartContext, EndpointOutcome, ImmediateEndpointHandler } from "@hypit/endpoint-kit"; import { defineEndpointPackage, wakeAfter } from "@hypit/endpoint-kit"; @@ -21,6 +22,29 @@ import type { RuntimeDoctorDiagnostic } from "@hypit/runtime-kit"; import { createHypiHubAuth, hypiHubCredentialNeedsRefresh } from "./oauth.js"; import type { HypiHubAuth } from "./oauth.js"; +function distributionVersion(): string { + try { + const manifest = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")) as { + readonly name?: unknown; readonly version?: unknown; + }; + if (manifest.name !== "@hypit/hypit" || typeof manifest.version !== "string" || manifest.version.length === 0) { + return "unknown"; + } + return manifest.version; + } catch { + return "unknown"; + } +} + +const userAgent = `hypit/${distributionVersion()}`; + +const identifiedFetch = (fetcher: typeof globalThis.fetch): typeof globalThis.fetch => + async (input, init) => { + const headers: Record = { "user-agent": userAgent }; + new Headers(init?.headers).forEach((value, name) => { headers[name] = value; }); + return await fetcher(input, { ...init, headers }); + }; + export const hypiHubProviderModuleRef = { name: "@hypit/provider-hypihub", version: "1" } as const; export type CreateHypiHubProviderOptions = { @@ -118,7 +142,7 @@ class HypiHubClient { this.timeout = options.timeout; this.oauthTimeout = options.oauthTimeout; this.downloadAttempts = options.downloadAttempts; - this.fetcher = options.fetcher; + this.fetcher = identifiedFetch(options.fetcher); this.uploader = new HypiHubUploader({ baseUrl: this.baseUrl, requestTimeoutMs: this.timeout,