@blocks-network/sdk 1.0.15 → 1.0.16

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.
@@ -252,30 +252,29 @@ export declare function getAgent(agentName: string, options?: {
252
252
  baseUrl?: string;
253
253
  apiKey?: string;
254
254
  }): Promise<AgentEntry | null>;
255
- /**
256
- * Fetch the agent registry (all agents).
257
- */
258
- export declare function fetchAgentRegistry(options?: {
255
+ /** Shared options for the exported registry list helpers. */
256
+ interface ListOptions {
259
257
  limit?: number;
260
258
  cursor?: string;
261
259
  baseUrl?: string;
262
- }): Promise<AgentRegistryResult>;
260
+ /** Bearer credential, sent as `Authorization: Bearer <apiKey>`. Optional on
261
+ * Blocks Network, where the listing is world-readable; required on a Blocks
262
+ * Enterprise deployment, which returns an empty page to an unauthenticated
263
+ * caller. */
264
+ apiKey?: string;
265
+ }
266
+ /**
267
+ * Fetch the agent registry (all agents).
268
+ */
269
+ export declare function fetchAgentRegistry(options?: ListOptions): Promise<AgentRegistryResult>;
263
270
  /**
264
271
  * Fetch agents filtered by tag.
265
272
  */
266
- export declare function fetchAgentsByTag(tag: string, options?: {
267
- limit?: number;
268
- cursor?: string;
269
- baseUrl?: string;
270
- }): Promise<AgentRegistryResult>;
273
+ export declare function fetchAgentsByTag(tag: string, options?: ListOptions): Promise<AgentRegistryResult>;
271
274
  /**
272
275
  * Fetch agents filtered by listing type.
273
276
  */
274
- export declare function fetchAgentsByListing(listing: 'private' | 'public', options?: {
275
- limit?: number;
276
- cursor?: string;
277
- baseUrl?: string;
278
- }): Promise<AgentRegistryResult>;
277
+ export declare function fetchAgentsByListing(listing: 'private' | 'public', options?: ListOptions): Promise<AgentRegistryResult>;
279
278
  /**
280
279
  * Remove an agent from the registry.
281
280
  */
@@ -1 +1 @@
1
- import t from"node:os";import{registryAllChannel as n,registryTagChannel as e,registryVisibilityChannel as a,registryLogChannel as r,normalizeTagSlug as o}from"./channel-manager.js";import{getEnv as i}from"../env.js";import{CURRENT_PROTOCOL_VERSION as s,SUPPORTED_PROTOCOL_VERSIONS as c,SDK_VERSION as l,PROTOCOL_VERSION_HEADER as u}from"./protocol-version.js";import{captureAffinity as g,injectAffinity as d}from"./write-affinity.js";function m(){try{return t.platform()}catch{return"unknown"}}async function f(t,n=3,e=500){let a;for(let r=0;r<n;r++)try{return await t()}catch(t){a=t;const o=String(t);if(!(o.includes("ENOTFOUND")||o.includes("ETIMEDOUT")||o.includes("ECONNRESET")||o.includes("NetworkIssues"))||r===n-1)throw t;const i=e*Math.pow(2,r)+100*Math.random();await new Promise(t=>setTimeout(t,i))}throw a}function h(t,n){let e=`${function(t){if(t)return t.replace(/\/+$/,"");throw new Error("[AgentRegistry] baseUrl is required: provide it via options or CDM config")}(n)}/api/v1/registry/agents`;if(t){const n=new URLSearchParams;for(const[e,a]of Object.entries(t))void 0!==a&&""!==a&&n.set(e,a);const a=n.toString();a&&(e+="?"+a)}return e}async function p(t,n){const e=new Headers(n?.headers);e.has(u)||e.set(u,s);const a={};d(a);for(const[t,n]of Object.entries(a))e.set(t,n);const r=await fetch(t,{...n,headers:e});if(g(r.headers),404===r.status)return null;if(!r.ok){const t=(await r.json().catch(()=>({}))).message??`HTTP ${r.status}`;throw new Error(`[AgentRegistry] Request failed: ${t}`)}return{data:await r.json(),status:r.status}}function y(t){return{agentName:t.agentName,displayName:t.name??t.agentName,description:t.description,tags:t.tags,scaling:t.scaling,card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:t.listing??"public",billingMode:t.billingMode,createdAt:t.registeredAt}}export async function connectAgent(t,n){if(!/^[a-zA-Z0-9_]+$/.test(t))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");!function(t){if(t&&"object"==typeof t&&"skills"in t)throw new Error("[blocks-sdk] agent-card field `skills` was renamed to `tags`. Action: rename the field in your code (or in any agent-card.json you load) — same shape, only the key name changes. This SDK ships no compatibility alias. If you publish via `blocks-cli`, upgrading the CLI rewrites the field for you, but programmatic callers (this SDK) must rename.")}(n.card);h(void 0,n.baseUrl);const e=i("BLOCKS_CLI_VERSION"),a={agentName:t,instanceId:n.instanceId,billingMode:n.billingMode,listing:n.listing,expectedInstances:n.scaling?.expectedInstances,concurrency:n.scaling?.concurrency,maxPendingBacklog:n.scaling?.maxPendingBacklog,maxRunningTimeSec:n.scaling?.maxRunningTimeSec,deviceOs:m(),sdkLanguage:"Node",sdkVersion:l,protocolVersions:[...c],preferredProtocolVersion:s};let r;if(e&&(a.cliVersion=e),!n.agentAuth)throw new Error("[AgentRegistry] agentAuth is required. Set BLOCKS_API_KEY and pass agentAuth to connectAgent.");const o=await f(async()=>n.agentAuth.init(a));return r=o.pamToken??void 0,{pamToken:r,agentId:o.agentId,controlChannel:o.controlChannel}}export async function getAgent(t,n){const e=h({agentName:t},n?.baseUrl),a=n?.apiKey?{headers:{Authorization:`Bearer ${n.apiKey}`}}:void 0,r=await f(async()=>p(e,a));return r?y(r.data.agent):null}export async function fetchAgentRegistry(t){const n={include:"full"};t?.limit&&(n.limit=String(t.limit)),t?.cursor&&(n.cursor=t.cursor);const e=h(n,t?.baseUrl),a=await f(async()=>p(e));return a?{agents:a.data.agents.map(y),next:a.data.next??void 0,totalCount:a.data.totalCount}:{agents:[],totalCount:0}}export async function fetchAgentsByTag(t,n){const e={include:"full",tag:t};n?.limit&&(e.limit=String(n.limit)),n?.cursor&&(e.cursor=n.cursor);const a=h(e,n?.baseUrl),r=await f(async()=>p(a));return r?{agents:r.data.agents.map(y),next:r.data.next??void 0,totalCount:r.data.totalCount}:{agents:[],totalCount:0}}export async function fetchAgentsByListing(t,n){const e={include:"full",listing:t};n?.limit&&(e.limit=String(n.limit)),n?.cursor&&(e.cursor=n.cursor);const a=h(e,n?.baseUrl),r=await f(async()=>p(a));return r?{agents:r.data.agents.map(y),next:r.data.next??void 0,totalCount:r.data.totalCount}:{agents:[],totalCount:0}}export async function removeAgent(t,n){const e=h({agentName:t},n?.baseUrl);if(n?.agentAuth){return 404!==(await f(()=>n.agentAuth.authenticatedFetch(e,{method:"DELETE"}))).status}const a=i("BLOCKS_API_KEY");return null!==await f(async()=>p(e,{method:"DELETE",headers:{...a?{Authorization:`Bearer ${a}`}:{}}}))}export{m as detectDeviceOs,n as registryAllChannel,e as registryTagChannel,a as registryVisibilityChannel,r as registryLogChannel,o as normalizeTagSlug};
1
+ import t from"node:os";import{registryAllChannel as e,registryTagChannel as n,registryVisibilityChannel as a,registryLogChannel as r,normalizeTagSlug as i}from"./channel-manager.js";import{getEnv as o}from"../env.js";import{CURRENT_PROTOCOL_VERSION as s,SUPPORTED_PROTOCOL_VERSIONS as c,SDK_VERSION as u,PROTOCOL_VERSION_HEADER as l}from"./protocol-version.js";import{captureAffinity as g,injectAffinity as d}from"./write-affinity.js";function m(){try{return t.platform()}catch{return"unknown"}}async function f(t,e=3,n=500){let a;for(let r=0;r<e;r++)try{return await t()}catch(t){a=t;const i=String(t);if(!(i.includes("ENOTFOUND")||i.includes("ETIMEDOUT")||i.includes("ECONNRESET")||i.includes("NetworkIssues"))||r===e-1)throw t;const o=n*Math.pow(2,r)+100*Math.random();await new Promise(t=>setTimeout(t,o))}throw a}function h(t,e){let n=`${function(t){if(t)return t.replace(/\/+$/,"");throw new Error("[AgentRegistry] baseUrl is required: provide it via options or CDM config")}(e)}/api/v1/registry/agents`;if(t){const e=new URLSearchParams;for(const[n,a]of Object.entries(t))void 0!==a&&""!==a&&e.set(n,a);const a=e.toString();a&&(n+="?"+a)}return n}async function p(t,e){const n=new Headers(e?.headers);n.has(l)||n.set(l,s);const a={};d(a);for(const[t,e]of Object.entries(a))n.set(t,e);const r=await fetch(t,{...e,headers:n});if(g(r.headers),404===r.status)return null;if(!r.ok){const t=(await r.json().catch(()=>({}))).message??`HTTP ${r.status}`;throw new Error(`[AgentRegistry] Request failed: ${t}`)}return{data:await r.json(),status:r.status}}function y(t){return{agentName:t.agentName,displayName:t.name??t.agentName,description:t.description,tags:t.tags,scaling:t.scaling,card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:t.listing??"public",billingMode:t.billingMode,createdAt:t.registeredAt}}export async function connectAgent(t,e){if(!/^[a-zA-Z0-9_]+$/.test(t))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");!function(t){if(t&&"object"==typeof t&&"skills"in t)throw new Error("[blocks-sdk] agent-card field `skills` was renamed to `tags`. Action: rename the field in your code (or in any agent-card.json you load) — same shape, only the key name changes. This SDK ships no compatibility alias. If you publish via `blocks-cli`, upgrading the CLI rewrites the field for you, but programmatic callers (this SDK) must rename.")}(e.card);h(void 0,e.baseUrl);const n=o("BLOCKS_CLI_VERSION"),a={agentName:t,instanceId:e.instanceId,billingMode:e.billingMode,listing:e.listing,expectedInstances:e.scaling?.expectedInstances,concurrency:e.scaling?.concurrency,maxPendingBacklog:e.scaling?.maxPendingBacklog,maxRunningTimeSec:e.scaling?.maxRunningTimeSec,deviceOs:m(),sdkLanguage:"Node",sdkVersion:u,protocolVersions:[...c],preferredProtocolVersion:s};let r;if(n&&(a.cliVersion=n),!e.agentAuth)throw new Error("[AgentRegistry] agentAuth is required. Set BLOCKS_API_KEY and pass agentAuth to connectAgent.");const i=await f(async()=>e.agentAuth.init(a));return r=i.pamToken??void 0,{pamToken:r,agentId:i.agentId,controlChannel:i.controlChannel}}export async function getAgent(t,e){const n=h({agentName:t},e?.baseUrl),a=e?.apiKey?{headers:{Authorization:`Bearer ${e.apiKey}`}}:void 0,r=await f(async()=>p(n,a));return r?y(r.data.agent):null}async function w(t,e){const n={...t};e?.limit&&(n.limit=String(e.limit)),e?.cursor&&(n.cursor=e.cursor);const a=h(n,e?.baseUrl),r=e?.apiKey?{headers:{Authorization:`Bearer ${e.apiKey}`}}:void 0,i=await f(async()=>p(a,r));return i?{agents:i.data.agents.map(y),next:i.data.next??void 0,totalCount:i.data.totalCount}:{agents:[],totalCount:0}}export async function fetchAgentRegistry(t){return w({include:"full"},t)}export async function fetchAgentsByTag(t,e){return w({include:"full",tag:t},e)}export async function fetchAgentsByListing(t,e){return w({include:"full",listing:t},e)}export async function removeAgent(t,e){const n=h({agentName:t},e?.baseUrl);if(e?.agentAuth){return 404!==(await f(()=>e.agentAuth.authenticatedFetch(n,{method:"DELETE"}))).status}const a=o("BLOCKS_API_KEY");return null!==await f(async()=>p(n,{method:"DELETE",headers:{...a?{Authorization:`Bearer ${a}`}:{}}}))}export{m as detectDeviceOs,e as registryAllChannel,n as registryTagChannel,a as registryVisibilityChannel,r as registryLogChannel,i as normalizeTagSlug};
@@ -34,9 +34,9 @@ export interface AuthProvider {
34
34
  */
35
35
  ensureReady?(): Promise<void>;
36
36
  /**
37
- * Returns the recorded permanent-refresh error when the provider is in
38
- * a known-broken state (e.g. proactive refresh exhausted its retries),
39
- * or null otherwise. The transport calls this before issuing any
37
+ * Returns the recorded refresh error when the provider is in a
38
+ * known-broken state — proactive refresh exhausted its retries, or a
39
+ * reactive refresh failed — or null otherwise. The transport calls this before issuing any
40
40
  * authenticated request and throws the error directly so consumers see
41
41
  * the typed `AuthRefreshFailedError` instead of an opaque downstream
42
42
  * 401. Implementations without proactive refresh return null / omit.
@@ -50,10 +50,22 @@ export interface ConsumerAuthOptions {
50
50
  onAuthError?: (error: Error) => void;
51
51
  }
52
52
  /**
53
- * Thrown by `TaskClient.sendMessage()` and `TaskClient.connect()` when the
54
- * underlying `ConsumerAuth` is in a known-broken refresh state (proactive
55
- * refresh permanently failed after 3 retries). Carries the original failure
56
- * as `.cause`. Cleared by a subsequent successful reactive refresh.
53
+ * Set when the underlying `ConsumerAuth` enters a known-broken refresh state —
54
+ * proactive refresh permanently failed after 3 retries, or a reactive refresh
55
+ * failed.
56
+ *
57
+ * Thrown by any authenticated call that runs the `preflightAuthOrThrow` gate:
58
+ * the RPC methods on `TaskClient` and `TaskSession`, `connect()`, and the
59
+ * file-upload helpers. Enumerating them here would drift the moment one is
60
+ * added, so the rule is the gate rather than a list.
61
+ *
62
+ * Also thrown by `getAgentCard()`, for a different reason: it cannot tell a
63
+ * rejected credential from a missing agent on its own, because the registry read
64
+ * is on optional auth, so a stale bearer degrades to anonymous and 404s rather
65
+ * than 401ing.
66
+ *
67
+ * Carries the original failure as `.cause`. Cleared by a subsequent successful
68
+ * token apply.
57
69
  */
58
70
  export declare class AuthRefreshFailedError extends Error {
59
71
  readonly cause: Error;
@@ -83,8 +95,9 @@ export declare class ConsumerAuth implements AuthProvider {
83
95
  getAuthHeader(): string | null;
84
96
  getUserId(): string | null;
85
97
  /**
86
- * Returns the last permanent refresh failure, or null. Set when proactive
87
- * refresh exhausts its 3 retries; cleared on a successful reactive refresh.
98
+ * Returns the last refresh failure, or null. Set when proactive refresh
99
+ * exhausts its 3 retries, and when a reactive refresh fails; cleared
100
+ * atomically on the next successful token apply.
88
101
  * Callers (TaskClient.sendMessage / connect) use this to fail fast before
89
102
  * making any authenticated request.
90
103
  */
@@ -1 +1 @@
1
- import{log as e}from"./logger.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as r}from"./protocol-version.js";import{captureAffinity as i,injectAffinity as s}from"./write-affinity.js";const n=(t,r,i)=>e("[ConsumerAuth]",t,r,i),o="API_KEY_INVALID";export class AuthRefreshFailedError extends Error{constructor(e){super(`Consumer auth refresh permanently failed: ${e.message}`),this.name="AuthRefreshFailedError",this.cause=e}}export class ConsumerAuth{constructor(e){this._token=null,this._refreshToken=null,this._userId=null,this._expiresIn=0,this._destroyed=!1,this._refreshTimer=null,this._refreshPromise=null,this._initialized=!1,this._initPromise=null,this._lastAuthError=null,this._apiKey=e.apiKey,this._tokenEndpoint=e.tokenEndpoint,this._tokenProvider=e.tokenProvider,this._baseUrl=(e.baseUrl??"").replace(/\/+$/,""),this._onAuthError=e.onAuthError}async init(){if(this._apiKey)await this._initApiKey();else if(this._tokenEndpoint)await this._initTokenEndpoint();else{if(!this._tokenProvider)throw new Error("ConsumerAuth requires one of: apiKey, tokenEndpoint, or tokenProvider");await this._initTokenProvider()}this._scheduleRefresh(),this._initialized=!0}getAuthHeader(){return this._token?`Bearer ${this._token}`:null}getUserId(){return this._userId}getLastAuthError(){return this._lastAuthError}async onAuthFailure(){return!this._destroyed&&this._doRefreshDedup()}destroy(){this._destroyed=!0,this._refreshTimer&&(clearTimeout(this._refreshTimer),this._refreshTimer=null)}async ensureReady(){this._initialized||(this._initPromise||(this._initPromise=this.init().then(()=>{this._initialized=!0},e=>{throw this._initPromise=null,e})),await this._initPromise)}async _initApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/consumer-token`,n={"Content-Type":"application/json",[r]:t};s(n);const o=await fetch(e,{method:"POST",headers:n,body:JSON.stringify({apiKey:this._apiKey})});if(i(o.headers),!o.ok){const e=await o.text().catch(()=>"");throw new Error(`consumer-token failed: HTTP ${o.status}${e?` ${e}`:""}`)}const h=await o.json();this._refreshToken=h.refreshToken,this._applyTokenResult({token:h.accessToken,expiresIn:h.expiresIn,userId:h.userId})}async _refreshApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/refresh`,t={Authorization:`Bearer ${this._apiKey}`,"Content-Type":"application/json"};s(t);const r=await fetch(e,{method:"POST",headers:t,body:JSON.stringify({refreshToken:this._refreshToken})});if(i(r.headers),!r.ok){const e=await r.json().catch(()=>({}));if("REFRESH_TOKEN_INVALID"===e.code)return void await this._initApiKey();if(e.code===o)throw new Error(`API key invalid or revoked: ${e.error??o}`);throw new Error(`token refresh failed: ${e.error??`HTTP ${r.status}`}`)}const n=await r.json();this._token=n.accessToken,this._refreshToken=n.refreshToken,"number"==typeof n.expiresIn&&(this._expiresIn=n.expiresIn),this._lastAuthError=null}async _initTokenEndpoint(){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}async _fetchTokenEndpoint(){const e=this._tokenEndpoint,t="string"==typeof e,r=t?e:e.url;if("string"!=typeof r||!r)throw new Error("TokenEndpointConfig.url is required and must be a non-empty string");const i=t?void 0:e.credentials,s=t?{}:e.headers??{},n=t?{}:e.body??{},o={method:"POST",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(n)};void 0!==i&&(o.credentials=i);const h=await fetch(r,o);if(!h.ok){const e=await h.text().catch(()=>"");throw new Error(`token endpoint failed: HTTP ${h.status}${e?` ${e}`:""}`)}return h.json()}async _initTokenProvider(){const e=await this._tokenProvider();this._applyTokenResult(e)}_applyTokenResult(e){this._token=e.token,this._expiresIn=e.expiresIn,void 0!==e.userId&&(this._userId=e.userId),this._lastAuthError=null}async _refreshOnce(){if(this._apiKey)await this._refreshApiKey();else if(this._tokenEndpoint){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}else if(this._tokenProvider){const e=await this._tokenProvider();this._applyTokenResult(e)}}_doRefreshDedup(){return this._refreshPromise||(this._refreshPromise=this._refreshOnce().then(()=>(this._scheduleRefresh(),!0)).catch(e=>{const t=e instanceof Error?e:new Error(String(e));return n("warn","reactive refresh failed",{event:"consumer_auth_reactive_refresh_failed",error:t.message}),!1}).finally(()=>{this._refreshPromise=null})),this._refreshPromise}_scheduleRefresh(){if(this._destroyed||this._expiresIn<=0)return;this._refreshTimer&&clearTimeout(this._refreshTimer);const e=1e3*this._expiresIn*.8;this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(0)},e)}async _proactiveRefreshWithRetry(e){if(!this._destroyed)try{await this._refreshOnce(),this._scheduleRefresh()}catch(t){if(e<2){const t=Math.min(5e3*Math.pow(2,e),3e4);this._destroyed||(this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(e+1)},t))}else{const e=t instanceof Error?t:new Error(String(t));this._lastAuthError=new AuthRefreshFailedError(e),n("warn","proactive refresh permanently failed",{event:"consumer_auth_proactive_refresh_failed",error:e.message}),this._onAuthError&&this._onAuthError(e)}}}}
1
+ import{log as e}from"./logger.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as r}from"./protocol-version.js";import{captureAffinity as i,injectAffinity as s}from"./write-affinity.js";const n=(t,r,i)=>e("[ConsumerAuth]",t,r,i),o="API_KEY_INVALID";export class AuthRefreshFailedError extends Error{constructor(e){super(`Consumer auth refresh permanently failed: ${e.message}`),this.name="AuthRefreshFailedError",this.cause=e}}export class ConsumerAuth{constructor(e){this._token=null,this._refreshToken=null,this._userId=null,this._expiresIn=0,this._destroyed=!1,this._refreshTimer=null,this._refreshPromise=null,this._initialized=!1,this._initPromise=null,this._lastAuthError=null,this._apiKey=e.apiKey,this._tokenEndpoint=e.tokenEndpoint,this._tokenProvider=e.tokenProvider,this._baseUrl=(e.baseUrl??"").replace(/\/+$/,""),this._onAuthError=e.onAuthError}async init(){if(this._apiKey)await this._initApiKey();else if(this._tokenEndpoint)await this._initTokenEndpoint();else{if(!this._tokenProvider)throw new Error("ConsumerAuth requires one of: apiKey, tokenEndpoint, or tokenProvider");await this._initTokenProvider()}this._scheduleRefresh(),this._initialized=!0}getAuthHeader(){return this._token?`Bearer ${this._token}`:null}getUserId(){return this._userId}getLastAuthError(){return this._lastAuthError}async onAuthFailure(){return!this._destroyed&&this._doRefreshDedup()}destroy(){this._destroyed=!0,this._refreshTimer&&(clearTimeout(this._refreshTimer),this._refreshTimer=null)}async ensureReady(){this._initialized||(this._initPromise||(this._initPromise=this.init().then(()=>{this._initialized=!0},e=>{throw this._initPromise=null,e})),await this._initPromise)}async _initApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/consumer-token`,n={"Content-Type":"application/json",[r]:t};s(n);const o=await fetch(e,{method:"POST",headers:n,body:JSON.stringify({apiKey:this._apiKey})});if(i(o.headers),!o.ok){const e=await o.text().catch(()=>"");throw new Error(`consumer-token failed: HTTP ${o.status}${e?` ${e}`:""}`)}const h=await o.json();this._refreshToken=h.refreshToken,this._applyTokenResult({token:h.accessToken,expiresIn:h.expiresIn,userId:h.userId})}async _refreshApiKey(){const e=`${this._baseUrl}/api/v1/auth/agent/refresh`,t={Authorization:`Bearer ${this._apiKey}`,"Content-Type":"application/json"};s(t);const r=await fetch(e,{method:"POST",headers:t,body:JSON.stringify({refreshToken:this._refreshToken})});if(i(r.headers),!r.ok){const e=await r.json().catch(()=>({}));if("REFRESH_TOKEN_INVALID"===e.code)return void await this._initApiKey();if(e.code===o)throw new Error(`API key invalid or revoked: ${e.error??o}`);throw new Error(`token refresh failed: ${e.error??`HTTP ${r.status}`}`)}const n=await r.json();this._token=n.accessToken,this._refreshToken=n.refreshToken,"number"==typeof n.expiresIn&&(this._expiresIn=n.expiresIn),this._lastAuthError=null}async _initTokenEndpoint(){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}async _fetchTokenEndpoint(){const e=this._tokenEndpoint,t="string"==typeof e,r=t?e:e.url;if("string"!=typeof r||!r)throw new Error("TokenEndpointConfig.url is required and must be a non-empty string");const i=t?void 0:e.credentials,s=t?{}:e.headers??{},n=t?{}:e.body??{},o={method:"POST",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(n)};void 0!==i&&(o.credentials=i);const h=await fetch(r,o);if(!h.ok){const e=await h.text().catch(()=>"");throw new Error(`token endpoint failed: HTTP ${h.status}${e?` ${e}`:""}`)}return h.json()}async _initTokenProvider(){const e=await this._tokenProvider();this._applyTokenResult(e)}_applyTokenResult(e){this._token=e.token,this._expiresIn=e.expiresIn,void 0!==e.userId&&(this._userId=e.userId),this._lastAuthError=null}async _refreshOnce(){if(this._apiKey)await this._refreshApiKey();else if(this._tokenEndpoint){const e=await this._fetchTokenEndpoint();this._applyTokenResult(e)}else if(this._tokenProvider){const e=await this._tokenProvider();this._applyTokenResult(e)}}_doRefreshDedup(){return this._refreshPromise||(this._refreshPromise=this._refreshOnce().then(()=>(this._scheduleRefresh(),!0)).catch(e=>{const t=e instanceof Error?e:new Error(String(e));return this._lastAuthError=new AuthRefreshFailedError(t),n("warn","reactive refresh failed",{event:"consumer_auth_reactive_refresh_failed",error:t.message}),!1}).finally(()=>{this._refreshPromise=null})),this._refreshPromise}_scheduleRefresh(){if(this._destroyed||this._expiresIn<=0)return;this._refreshTimer&&clearTimeout(this._refreshTimer);const e=1e3*this._expiresIn*.8;this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(0)},e)}async _proactiveRefreshWithRetry(e){if(!this._destroyed)try{await this._refreshOnce(),this._scheduleRefresh()}catch(t){if(e<2){const t=Math.min(5e3*Math.pow(2,e),3e4);this._destroyed||(this._refreshTimer=setTimeout(()=>{this._refreshTimer=null,this._proactiveRefreshWithRetry(e+1)},t))}else{const e=t instanceof Error?t:new Error(String(t));this._lastAuthError=new AuthRefreshFailedError(e),n("warn","proactive refresh permanently failed",{event:"consumer_auth_proactive_refresh_failed",error:e.message}),this._onAuthError&&this._onAuthError(e)}}}}
@@ -257,8 +257,29 @@ export declare class TaskClient {
257
257
  /**
258
258
  * Look up an agent's card by name from the registry.
259
259
  * Returns null if the agent is not found or has no card.
260
+ *
261
+ * Forwards the client's credential when it has one. The registry read is
262
+ * mounted on optional auth, so this is not required on Blocks Network — but a
263
+ * Blocks Enterprise deployment serves agent metadata to authenticated callers
264
+ * only, and would answer an unauthenticated lookup with null even for a
265
+ * correctly configured client.
266
+ *
267
+ * Throws `AuthRefreshFailedError` when a configured credential cannot be
268
+ * produced. `null` means "no such agent, or no card"; it must not also mean
269
+ * "your token expired", or an auth problem would read as a missing agent.
260
270
  */
261
271
  getAgentCard(agentName: string): Promise<AgentCard | null>;
272
+ /**
273
+ * The client's bearer credential, or undefined when it has none.
274
+ *
275
+ * `getAgent`'s `apiKey` option is sent verbatim as `Authorization: Bearer
276
+ * <value>`, so whatever bearer credential the provider is holding works. In
277
+ * practice that is the consumer JWT in all three `ConsumerAuth` modes: API-key
278
+ * mode exchanges the key for a JWT during `init()` and never sends the raw key
279
+ * here. The provider owns refresh, so reading the header per call rather than
280
+ * caching it here keeps a rotated token from going stale.
281
+ */
282
+ private bearerCredential;
262
283
  /**
263
284
  * Update keyset keys after an environment switch.
264
285
  * Updates both the RPC config and the keys used for PubNub client creation.
@@ -1 +1 @@
1
- import e from"pubnub";import{buildPubNubLogConfig as t}from"./pubnub-client.js";import{callRpc as n}from"./rpc-client.js";import{taskChannel as s}from"./channel-manager.js";import{TaskSession as r}from"./task-session.js";import{TerminalDeliveryTracker as i}from"./terminal-delivery-tracker.js";import{preflightAuthOrThrow as a}from"./auth-provider.js";import{ConsumerAuth as o}from"./consumer-auth.js";import{shouldInlineArtifact as c,buildArtifactRef as u}from"./artifacts.js";import{uploadFile as d}from"./file-upload.js";import{normalizeFileInput as l}from"./file-input.js";import{fetchCdmConfig as b}from"./cdm-config.js";import{captureAffinity as h,injectAffinity as p}from"./write-affinity.js";import{getEnv as f}from"../env.js";import{log as m}from"./logger.js";import{StreamRef as g}from"./stream-ref.js";import{invertDirection as k}from"../stream/index.js";import{asPubNubFetcher as y}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as w,PROTOCOL_VERSION_HEADER as T}from"./protocol-version.js";import{getAgent as I}from"./agent-registry.js";const S=(e,t,n)=>m("[TaskClient]",e,t,n),C=new Set(["completed","failed","canceled"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}function P(e,t,n,s){const r=new Map,i=[],a=[];let o,c="0",u="0";for(const d of e){d.timetoken&&d.timetoken>c&&(c=d.timetoken);const e=d.message;if(e&&"object"==typeof e&&e.type){if(a.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(o=n,u=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const i=e.declaredStream,a=e.streams;for(const[e,o]of Object.entries(a)){if(!o||"object"!=typeof o||r.has(e))continue;const a=o.direction,c=k(a),u=o.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=o.affinity;if("dedicated"!==d&&"shared"!==d){S("warn",`history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity`,{event:"history_preload_invalid_affinity",streamId:e,taskId:t,receivedAffinity:o.affinity});continue}const l={taskId:t,streamId:e,agentName:n,channel:o.channel,token:o.token,agentDirection:a,localDirection:c,format:u,affinity:d,metadata:o.metadata,declaredStream:i};r.set(e,new g(l,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&i.push(t)}}}return{streams:r,artifacts:i,events:a,highWaterMark:c,terminalState:o}}async function K(e,t){const n=[];let s;for(;;){const r={channels:[t],count:100};s&&(r.start=s);const i=await e.fetchMessages(r),a=i.channels?.[t]??[];if(0===a.length)break;if(n.push(...a),a.length<100)break;s=a[0].timetoken}return n.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),n}export class TaskClient{constructor(e){if(this._ownsPubNub=!1,"free"!==e.billingMode&&"paid"!==e.billingMode)throw new Error("TaskClient requires a billingMode option ('free' or 'paid')");this._billingMode=e.billingMode,this._anonFingerprint=e.anonFingerprint??null,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth,rpcHeaders:e.rpcHeaders},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(n){const s=n??{},r=s.billingMode;if("free"!==r&&"paid"!==r)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");const i=!!(s.apiKey||s.tokenEndpoint||s.tokenProvider),a=!!s.anonFingerprint;if([s.apiKey,s.tokenEndpoint,s.tokenProvider,s.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(a&&"free"!==r)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const c=s.cdmUrl??f("BLOCKS_CDM_URL"),u=await b(c),d="paid"===r?u.network:u.playground,l=s.subscribeKey??f("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,h=s.publishKey??f("BLOCKS_PUBLISH_KEY")??d.publishKey,p=s.baseUrl??f("BLOCKS_BACKEND_URL")??u.api.baseUrl;if(!l)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!p)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const m=()=>{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:l,publishKey:h||void 0,userId:n,enableEventEngine:!0,...t()})};if(i){const e=new o({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:p,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({billingMode:r,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:m,defaultOwnerId:e.getUserId()??void 0,rpcHeaders:s.rpcHeaders});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:r,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:m,anonFingerprint:s.anonFingerprint,rpcHeaders:s.rpcHeaders})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await I(e,{baseUrl:this.config.baseUrl});return t?.card??null}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(n,s,r){let i;const a=s??this._subscribeKey,o=r??this._publishKey;if(this._createSessionPubNub&&a===this._subscribeKey)i=this._createSessionPubNub();else{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;i=new e({subscribeKey:a,publishKey:o||void 0,userId:n,enableEventEngine:!0,...t()})}return n&&i.setToken(n),i}async fetchConsumerReadToken(e,t="consumer"){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const n=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[T]:w},r=this.config.authProvider?.getAuthHeader();return r&&(s.Authorization=r),p(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let r=await s();if(h(r.headers),401===r.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(r=await s(),h(r.headers))}if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${r.status}${e?` ${e}`:""}`)}return r.json()}async fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[T]:w};p(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(h(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");await a(this.config.authProvider),this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const t=e.ownerId||this.defaultOwnerId||"",i=e.taskKind,o=e.duration;if("pipe"===i){if(null==o||!Number.isInteger(o)||o<1||o>43200)throw new Error("Pipe tasks require an integer duration between 1 and 43200 minutes")}else if(null!=o)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let b;const h=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const n=l(t.file),s=t.fileName??"unnamed",r=t.contentType??"application/octet-stream";if(c(n.size)){const e=await n.getBytes(),i=u({data:e,mimeType:r,fileName:s}),{file:a,fileName:o,text:c,...d}=t;h.push({...d,artifactRef:i})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const i={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},a={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:r,partId:t.partId,uploadSessionId:b},o=await d(i,a,n.uploadBody);o.uploadSessionId&&(b=o.uploadSessionId);const{file:c,fileName:u,...l}=t;h.push({partId:l.partId,...l.contentType?{contentType:l.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;h.push(s)}const p={agentName:e.agentName,billingMode:this._billingMode,requestParts:h};b&&(p.uploadSessionId=b),e.idempotencyKey&&(p.idempotencyKey=e.idempotencyKey),p.ownerId=t;const f={};i&&(f.taskKind=i),null!=o&&(f.duration=o),e.consumerPublicKey&&(f.consumerPublicKey=e.consumerPublicKey),void 0!==e.stream&&(f.stream=e.stream),Object.keys(f).length>0&&(p.extensions={blocks:f}),e.pushNotificationConfig&&(p.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(p.retryPolicy=e.retryPolicy);const m=await n(this.config,"SendMessage",p),g=m.extensions?.blocks,k=g?.readToken??null,w=g?.streamChannels?.status??void 0,T=g?.subscribeKey??this._subscribeKey,I=g?.publishKey??this._publishKey;if(!0===m.idempotent&&!!m.state&&C.has(m.state))return new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:w,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:T,publishKey:I},rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,preClosed:!0,state:m.state});const S=this.createPerSessionPubNub(k,T,I),_={subscribeKey:T,publishKey:I},v=w??s(m.taskId,m.orgId??t);try{const n=await S.time(),s=String(n.timetoken),i=y(S);let a,o=new Map,c=[],u="0";if(i?.fetchMessages){const t=P(await K(i,v),m.taskId,e.agentName,_);o=t.streams,c=t.artifacts,u=t.highWaterMark,a=t.terminalState}if(a&&C.has(a))return new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:v,agentName:e.agentName,pubnub:S,ownsSubscribeClient:!0,sdkOptions:_,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:a,skipSubscription:!0,preloadedStreams:o,preloadedArtifacts:c});const d="0"!==u?u:s,l=[];let b,h=!1;const p={message:e=>{if(e.channel!==v)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");h&&b?b(t,n):l.push({message:t,timetoken:n})}};S.addListener(p),S.subscribe({channels:[v],timetoken:d});const f=new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:v,agentName:e.agentName,pubnub:S,ownsSubscribeClient:!0,sdkOptions:_,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:o,preloadedArtifacts:c,externalSubscription:{listener:p,channel:v,onReady:e=>{b=e}}});if(b){const e=b;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.length=0,h=!0,f}catch{return new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:v,agentName:e.agentName,pubnub:S,ownsSubscribeClient:!0,sdkOptions:_,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}}async connect(e){const{taskId:t}=e;if(this._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",r=n.state??"",i=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:r,tokenResult:i,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}if(await a(this.config.authProvider),!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",r=n.state??"",i=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:r,tokenResult:i,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:s,tokenResult:i,autoDrain:a,drainWindowMs:o}=e,c=t.taskId,{pamToken:u,channel:d}=i,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(u);try{const e=await b.time(),i=String(e.timetoken),h=y(b);let p,f=new Map,m=[],g=[],k="0";if(h?.fetchMessages){const e=P(await K(h,d),c,n,l);f=e.streams,m=e.artifacts,g=e.events,k=e.highWaterMark,p=e.terminalState}const w=p&&C.has(p)?p:s;if(C.has(w))return new r({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:a,drainWindowMs:o,state:w,skipSubscription:!0,preloadedStreams:f,preloadedArtifacts:m,preloadedEvents:g});const T="0"!==k?k:i,I=[];let S,_=!1;const v={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");_&&S?S(t,n):I.push({message:t,timetoken:n})}};b.addListener(v),b.subscribe({channels:[d],timetoken:T});const E=new r({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:a,drainWindowMs:o,state:s,preloadedStreams:f,preloadedArtifacts:m,preloadedEvents:g,externalSubscription:{listener:v,channel:d,onReady:e=>{S=e}}});if(S){const e=S;for(const t of I)t.timetoken>T&&(E._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return I.length=0,_=!0,E}catch(e){throw b.destroy(),e}}async getTask(e){return(await n(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return n(this.config,"ListTasks",{...e})}async cancelTask(e){await n(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await n(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await n(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await n(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await n(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,n){return function(e,t,n,r){const a=s(t,n),o=new i;let c=!1;const u={message:e=>{if(e.channel!==a)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(r.onEvent)try{r.onEvent(t)}catch(e){_(e,"onEvent",t,r.onError)}switch(t.type){case"progress":if(r.onProgress)try{r.onProgress(t)}catch(e){_(e,"onProgress",t,r.onError)}break;case"artifact":if(r.onArtifact)try{r.onArtifact(t)}catch(e){_(e,"onArtifact",t,r.onError)}break;case"terminal":o.tryDeliver(t,e=>{if(r.onTerminal)try{r.onTerminal(e)}catch(t){_(t,"onTerminal",e,r.onError)}});break;case"cancel_requested":if(o.isDelivered)break;if(c)break;if(c=!0,r.onCancelRequested)try{r.onCancelRequested(t)}catch(e){_(e,"onCancelRequested",t,r.onError)}break;case"system":if(r.onSystem)try{r.onSystem(t)}catch(e){_(e,"onSystem",t,r.onError)}}}}};return e.addListener(u),e.subscribe({channels:[a],timetoken:1e3}),{unsubscribe(){e.removeListener(u),e.unsubscribe({channels:[a]})}}}(this.getPubNub(),e,t,n)}}function _(e,t,n,s){const r=e instanceof Error?e:new Error(String(e));if(s)try{s(r,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else S("warn",`subscribeToTask callback error in ${t}`,{event:"subscribe_callback_error",callbackType:t,error:r.message})}
1
+ import e from"pubnub";import{buildPubNubLogConfig as t}from"./pubnub-client.js";import{callRpc as n}from"./rpc-client.js";import{taskChannel as s}from"./channel-manager.js";import{TaskSession as r}from"./task-session.js";import{TerminalDeliveryTracker as i}from"./terminal-delivery-tracker.js";import{preflightAuthOrThrow as a}from"./auth-provider.js";import{ConsumerAuth as o}from"./consumer-auth.js";import{shouldInlineArtifact as c,buildArtifactRef as u}from"./artifacts.js";import{uploadFile as d}from"./file-upload.js";import{normalizeFileInput as l}from"./file-input.js";import{fetchCdmConfig as h}from"./cdm-config.js";import{captureAffinity as f,injectAffinity as b}from"./write-affinity.js";import{getEnv as p}from"../env.js";import{log as m}from"./logger.js";import{StreamRef as g}from"./stream-ref.js";import{invertDirection as k}from"../stream/index.js";import{asPubNubFetcher as y}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as w,PROTOCOL_VERSION_HEADER as T}from"./protocol-version.js";import{getAgent as I}from"./agent-registry.js";const C=(e,t,n)=>m("[TaskClient]",e,t,n),S=new Set(["completed","failed","canceled"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}function P(e,t,n,s){const r=new Map,i=[],a=[];let o,c="0",u="0";for(const d of e){d.timetoken&&d.timetoken>c&&(c=d.timetoken);const e=d.message;if(e&&"object"==typeof e&&e.type){if(a.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(o=n,u=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const i=e.declaredStream,a=e.streams;for(const[e,o]of Object.entries(a)){if(!o||"object"!=typeof o||r.has(e))continue;const a=o.direction,c=k(a),u=o.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=o.affinity;if("dedicated"!==d&&"shared"!==d){C("warn",`history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity`,{event:"history_preload_invalid_affinity",streamId:e,taskId:t,receivedAffinity:o.affinity});continue}const l={taskId:t,streamId:e,agentName:n,channel:o.channel,token:o.token,agentDirection:a,localDirection:c,format:u,affinity:d,metadata:o.metadata,declaredStream:i};r.set(e,new g(l,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&i.push(t)}}}return{streams:r,artifacts:i,events:a,highWaterMark:c,terminalState:o}}async function v(e,t){const n=[];let s;for(;;){const r={channels:[t],count:100};s&&(r.start=s);const i=await e.fetchMessages(r),a=i.channels?.[t]??[];if(0===a.length)break;if(n.push(...a),a.length<100)break;s=a[0].timetoken}return n.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),n}export class TaskClient{constructor(e){if(this._ownsPubNub=!1,"free"!==e.billingMode&&"paid"!==e.billingMode)throw new Error("TaskClient requires a billingMode option ('free' or 'paid')");this._billingMode=e.billingMode,this._anonFingerprint=e.anonFingerprint??null,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth,rpcHeaders:e.rpcHeaders},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(n){const s=n??{},r=s.billingMode;if("free"!==r&&"paid"!==r)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");const i=!!(s.apiKey||s.tokenEndpoint||s.tokenProvider),a=!!s.anonFingerprint;if([s.apiKey,s.tokenEndpoint,s.tokenProvider,s.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(a&&"free"!==r)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const c=s.cdmUrl??p("BLOCKS_CDM_URL"),u=await h(c),d="paid"===r?u.network:u.playground,l=s.subscribeKey??p("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,f=s.publishKey??p("BLOCKS_PUBLISH_KEY")??d.publishKey,b=s.baseUrl??p("BLOCKS_BACKEND_URL")??u.api.baseUrl;if(!l)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!b)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const m=()=>{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:l,publishKey:f||void 0,userId:n,enableEventEngine:!0,...t()})};if(i){const e=new o({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:b,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({billingMode:r,subscribeKey:l,publishKey:f,baseUrl:b,createSessionPubNub:m,defaultOwnerId:e.getUserId()??void 0,rpcHeaders:s.rpcHeaders});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:r,subscribeKey:l,publishKey:f,baseUrl:b,createSessionPubNub:m,anonFingerprint:s.anonFingerprint,rpcHeaders:s.rpcHeaders})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=this.config.authProvider,n=async()=>I(e,{baseUrl:this.config.baseUrl,apiKey:await this.bearerCredential()}),s=await n();if(s)return s.card??null;const r=this.config.agentAuth;if(!t&&r){await r.refresh();const e=await n();return e?.card??null}if(!t)return null;if(await t.onAuthFailure()){const e=await n();return e?.card??null}const i=t.getLastAuthError?.();if(i)throw i;return null}async bearerCredential(){const e=this.config.authProvider;if(!e)return this.config.agentAuth?.getAccessToken()??void 0;await a(e),await(e.ensureReady?.());const t=e.getAuthHeader();if(!t)return;const n="Bearer ";return t.startsWith(n)?t.slice(7):t}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(n,s,r){let i;const a=s??this._subscribeKey,o=r??this._publishKey;if(this._createSessionPubNub&&a===this._subscribeKey)i=this._createSessionPubNub();else{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;i=new e({subscribeKey:a,publishKey:o||void 0,userId:n,enableEventEngine:!0,...t()})}return n&&i.setToken(n),i}async fetchConsumerReadToken(e,t="consumer"){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const n=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[T]:w},r=this.config.authProvider?.getAuthHeader();return r&&(s.Authorization=r),b(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let r=await s();if(f(r.headers),401===r.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(r=await s(),f(r.headers))}if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${r.status}${e?` ${e}`:""}`)}return r.json()}async fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[T]:w};b(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(f(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");await a(this.config.authProvider),this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const t=e.ownerId||this.defaultOwnerId||"",i=e.taskKind,o=e.duration;if("pipe"===i){if(null==o||!Number.isInteger(o)||o<1||o>43200)throw new Error("Pipe tasks require an integer duration between 1 and 43200 minutes")}else if(null!=o)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let h;const f=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const n=l(t.file),s=t.fileName??"unnamed",r=t.contentType??"application/octet-stream";if(c(n.size)){const e=await n.getBytes(),i=u({data:e,mimeType:r,fileName:s}),{file:a,fileName:o,text:c,...d}=t;f.push({...d,artifactRef:i})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const i={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},a={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:r,partId:t.partId,uploadSessionId:h},o=await d(i,a,n.uploadBody);o.uploadSessionId&&(h=o.uploadSessionId);const{file:c,fileName:u,...l}=t;f.push({partId:l.partId,...l.contentType?{contentType:l.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;f.push(s)}const b={agentName:e.agentName,billingMode:this._billingMode,requestParts:f};h&&(b.uploadSessionId=h),e.idempotencyKey&&(b.idempotencyKey=e.idempotencyKey),b.ownerId=t;const p={};i&&(p.taskKind=i),null!=o&&(p.duration=o),e.consumerPublicKey&&(p.consumerPublicKey=e.consumerPublicKey),void 0!==e.stream&&(p.stream=e.stream),Object.keys(p).length>0&&(b.extensions={blocks:p}),e.pushNotificationConfig&&(b.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(b.retryPolicy=e.retryPolicy);const m=await n(this.config,"SendMessage",b),g=m.extensions?.blocks,k=g?.readToken??null,w=g?.streamChannels?.status??void 0,T=g?.subscribeKey??this._subscribeKey,I=g?.publishKey??this._publishKey;if(!0===m.idempotent&&!!m.state&&S.has(m.state))return new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:w,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:T,publishKey:I},rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,preClosed:!0,state:m.state});const C=this.createPerSessionPubNub(k,T,I),K={subscribeKey:T,publishKey:I},_=w??s(m.taskId,m.orgId??t);try{const n=await C.time(),s=String(n.timetoken),i=y(C);let a,o=new Map,c=[],u="0";if(i?.fetchMessages){const t=P(await v(i,_),m.taskId,e.agentName,K);o=t.streams,c=t.artifacts,u=t.highWaterMark,a=t.terminalState}if(a&&S.has(a))return new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:C,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:a,skipSubscription:!0,preloadedStreams:o,preloadedArtifacts:c});const d="0"!==u?u:s,l=[];let h,f=!1;const b={message:e=>{if(e.channel!==_)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");f&&h?h(t,n):l.push({message:t,timetoken:n})}};C.addListener(b),C.subscribe({channels:[_],timetoken:d});const p=new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:C,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:o,preloadedArtifacts:c,externalSubscription:{listener:b,channel:_,onReady:e=>{h=e}}});if(h){const e=h;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.length=0,f=!0,p}catch{return new r({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:C,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}}async connect(e){const{taskId:t}=e;if(this._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",r=n.state??"",i=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:r,tokenResult:i,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}if(await a(this.config.authProvider),!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",r=n.state??"",i=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:r,tokenResult:i,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:s,tokenResult:i,autoDrain:a,drainWindowMs:o}=e,c=t.taskId,{pamToken:u,channel:d}=i,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},h=this.createPerSessionPubNub(u);try{const e=await h.time(),i=String(e.timetoken),f=y(h);let b,p=new Map,m=[],g=[],k="0";if(f?.fetchMessages){const e=P(await v(f,d),c,n,l);p=e.streams,m=e.artifacts,g=e.events,k=e.highWaterMark,b=e.terminalState}const w=b&&S.has(b)?b:s;if(S.has(w))return new r({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:h,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:a,drainWindowMs:o,state:w,skipSubscription:!0,preloadedStreams:p,preloadedArtifacts:m,preloadedEvents:g});const T="0"!==k?k:i,I=[];let C,K=!1;const _={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");K&&C?C(t,n):I.push({message:t,timetoken:n})}};h.addListener(_),h.subscribe({channels:[d],timetoken:T});const E=new r({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:h,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:a,drainWindowMs:o,state:s,preloadedStreams:p,preloadedArtifacts:m,preloadedEvents:g,externalSubscription:{listener:_,channel:d,onReady:e=>{C=e}}});if(C){const e=C;for(const t of I)t.timetoken>T&&(E._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return I.length=0,K=!0,E}catch(e){throw h.destroy(),e}}async getTask(e){return(await n(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return n(this.config,"ListTasks",{...e})}async cancelTask(e){await n(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await n(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await n(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await n(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await n(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,n){return function(e,t,n,r){const a=s(t,n),o=new i;let c=!1;const u={message:e=>{if(e.channel!==a)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(r.onEvent)try{r.onEvent(t)}catch(e){K(e,"onEvent",t,r.onError)}switch(t.type){case"progress":if(r.onProgress)try{r.onProgress(t)}catch(e){K(e,"onProgress",t,r.onError)}break;case"artifact":if(r.onArtifact)try{r.onArtifact(t)}catch(e){K(e,"onArtifact",t,r.onError)}break;case"terminal":o.tryDeliver(t,e=>{if(r.onTerminal)try{r.onTerminal(e)}catch(t){K(t,"onTerminal",e,r.onError)}});break;case"cancel_requested":if(o.isDelivered)break;if(c)break;if(c=!0,r.onCancelRequested)try{r.onCancelRequested(t)}catch(e){K(e,"onCancelRequested",t,r.onError)}break;case"system":if(r.onSystem)try{r.onSystem(t)}catch(e){K(e,"onSystem",t,r.onError)}}}}};return e.addListener(u),e.subscribe({channels:[a],timetoken:1e3}),{unsubscribe(){e.removeListener(u),e.unsubscribe({channels:[a]})}}}(this.getPubNub(),e,t,n)}}function K(e,t,n,s){const r=e instanceof Error?e:new Error(String(e));if(s)try{s(r,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else C("warn",`subscribeToTask callback error in ${t}`,{event:"subscribe_callback_error",callbackType:t,error:r.message})}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocks-network/sdk",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "type": "module",
5
5
  "description": "Blocks Network SDK",
6
6
  "exports": {