@blocks-network/sdk 1.0.7 → 1.0.11
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.
|
@@ -14,7 +14,16 @@ export interface RpcClientConfig {
|
|
|
14
14
|
authProvider?: AuthProvider;
|
|
15
15
|
baseUrl?: string;
|
|
16
16
|
agentAuth?: AgentAuth;
|
|
17
|
+
/**
|
|
18
|
+
* Optional caller-supplied request headers merged onto every RPC call.
|
|
19
|
+
* Request metadata only — NOT an authorization mechanism. Merged UNDER
|
|
20
|
+
* SDK-owned headers: a caller cannot override `Authorization`,
|
|
21
|
+
* `Content-Type`, `Blocks-Protocol-Version`, or `X-Write-Affinity`.
|
|
22
|
+
*/
|
|
23
|
+
rpcHeaders?: Record<string, string>;
|
|
17
24
|
}
|
|
25
|
+
export declare const PROTECTED_RPC_HEADERS: Set<string>;
|
|
26
|
+
export declare function stripProtectedHeaders(h: Record<string, string>): void;
|
|
18
27
|
/**
|
|
19
28
|
* Retry helper for transient network errors (DNS failures, timeouts, etc.)
|
|
20
29
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{preflightAuthOrThrow as
|
|
1
|
+
import{preflightAuthOrThrow as t}from"./auth-provider.js";import{CURRENT_PROTOCOL_VERSION as r,PROTOCOL_VERSION_HEADER as e}from"./protocol-version.js";import{captureAffinity as o,injectAffinity as n}from"./write-affinity.js";export const PROTECTED_RPC_HEADERS=new Set(["authorization","content-type",e.toLowerCase(),"x-write-affinity"]);export function stripProtectedHeaders(t){for(const r of Object.keys(t))PROTECTED_RPC_HEADERS.has(r.toLowerCase())&&delete t[r]}export async function withRetry(t,r=3,e=500){let o;for(let n=0;n<r;n++)try{return await t()}catch(t){o=t;const i=String(t);if(!(i.includes("ENOTFOUND")||i.includes("ETIMEDOUT")||i.includes("ECONNRESET")||i.includes("NetworkIssues"))||n===r-1)throw t;const a=e*Math.pow(2,n)+100*Math.random();await new Promise(t=>setTimeout(t,a))}throw o}export function rpcEndpoint(t,r){return r?`${r.replace(/\/+$/,"")}/api/v1/rpc`:`https://ps.pndsn.com/v1/blocks/sub-key/${t}/rpc`}export class RpcError extends Error{constructor(t,r,e){super(`[RPC] ${t}`),this.name="RpcError",this.rpcMessage=t,this.code=r,this.data=e}}const i=new Set(["EMBEDDED_JWT_REVOKED","EMBEDDED_JWT_LOGOUT","EMBEDDED_JWT_KILLED","EMBEDDED_JWT_SCOPE_DRIFT","AGENT_OUT_OF_SCOPE"]);export class BillingModeMismatchError extends RpcError{constructor(t,r,e,o,n){super(t,o,n),this.name="BillingModeMismatchError",this.expected=r,this.got=e}}export async function callRpc(a,s,c){const d=rpcEndpoint(a.subscribeKey,a.baseUrl),u=`rpc-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,p=t=>{const o={...a.rpcHeaders??{}};stripProtectedHeaders(o),o["Content-Type"]="application/json",o[e]=r;const i=a.authProvider?.getAuthHeader();return i&&(o.Authorization=i),t&&n(o),o},h={jsonrpc:"2.0",id:u,method:s,params:c},E=async()=>{let t;if(a.agentAuth?t=await a.agentAuth.authenticatedFetch(d,{method:"POST",headers:p(!1),body:JSON.stringify(h)}):(t=await fetch(d,{method:"POST",headers:p(!0),body:JSON.stringify(h)}),o(t.headers)),!t.ok)throw new RpcError(`HTTP ${t.status}`,t.status);const r=await t.json();if(r.error){const t=r.error.data?.message??r.error.message??"Unknown RPC error",e=function(t,r,e){if(!e||"object"!=typeof e)return null;const o=e;if("BillingModeMismatch"!==o.code)return null;const n=o.details;if(!n||"object"!=typeof n)return null;const i=n;return"free"!==i.expected&&"paid"!==i.expected||"free"!==i.got&&"paid"!==i.got?null:new BillingModeMismatchError(t,i.expected,i.got,r,e)}(t,r.error.code,r.error.data);if(e)throw e;throw new RpcError(t,r.error.code,r.error.data)}return r.result};return await t(a.authProvider),a.authProvider?.ensureReady&&await a.authProvider.ensureReady(),withRetry(async()=>{try{return await E()}catch(t){if(function(t){if(!(t instanceof RpcError))return!1;if(401===t.code)return!0;const r=t.data;return"string"==typeof r?.code&&i.has(r.code)}(t)&&a.authProvider&&!a.agentAuth){if(await a.authProvider.onAuthFailure())return E()}throw t}})}
|
|
@@ -55,6 +55,13 @@ export interface TaskClientOptions {
|
|
|
55
55
|
* viewer flow; non-browser SDK callers should not use this.
|
|
56
56
|
*/
|
|
57
57
|
anonFingerprint?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Optional caller-supplied RPC request headers. Forwarded to callRpc and
|
|
60
|
+
* merged under SDK-owned headers (cannot override Authorization,
|
|
61
|
+
* Content-Type, Blocks-Protocol-Version, or X-Write-Affinity). Request
|
|
62
|
+
* metadata only.
|
|
63
|
+
*/
|
|
64
|
+
rpcHeaders?: Record<string, string>;
|
|
58
65
|
}
|
|
59
66
|
/**
|
|
60
67
|
* A request part item. Each part may include a `partId` referencing a
|
|
@@ -243,6 +250,7 @@ export declare class TaskClient {
|
|
|
243
250
|
* not use this.
|
|
244
251
|
*/
|
|
245
252
|
anonFingerprint?: string;
|
|
253
|
+
rpcHeaders?: Record<string, string>;
|
|
246
254
|
}): Promise<TaskClient>;
|
|
247
255
|
/** Returns the authenticated user ID from ConsumerAuth, or null. */
|
|
248
256
|
getUserId(): string | null;
|
|
@@ -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 i}from"./task-session.js";import{TerminalDeliveryTracker as r}from"./terminal-delivery-tracker.js";import{preflightAuthOrThrow as o}from"./auth-provider.js";import{ConsumerAuth as a}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 f}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 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 i=new Map,r=[],o=[];let a,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(o.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(a=n,u=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const r=e.declaredStream,o=e.streams;for(const[e,a]of Object.entries(o)){if(!a||"object"!=typeof a||i.has(e))continue;const o=a.direction,c=k(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.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:a.affinity});continue}const l={taskId:t,streamId:e,agentName:n,channel:a.channel,token:a.token,agentDirection:o,localDirection:c,format:u,affinity:d,metadata:a.metadata,declaredStream:r};i.set(e,new g(l,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&r.push(t)}}}return{streams:i,artifacts:r,events:o,highWaterMark:c,terminalState:a}}async function K(e,t){const n=[];let s;for(;;){const i={channels:[t],count:100};s&&(i.start=s);const r=await e.fetchMessages(i),o=r.channels?.[t]??[];if(0===o.length)break;if(n.push(...o),o.length<100)break;s=o[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},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??{},i=s.billingMode;if("free"!==i&&"paid"!==i)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");const r=!!(s.apiKey||s.tokenEndpoint||s.tokenProvider),o=!!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(o&&"free"!==i)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const c=s.cdmUrl??p("BLOCKS_CDM_URL"),u=await b(c),d="paid"===i?u.network:u.playground,l=s.subscribeKey??p("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,h=s.publishKey??p("BLOCKS_PUBLISH_KEY")??d.publishKey,f=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(!f)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(r){const e=new a({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:f,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({billingMode:i,subscribeKey:l,publishKey:h,baseUrl:f,createSessionPubNub:m,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:i,subscribeKey:l,publishKey:h,baseUrl:f,createSessionPubNub:m,anonFingerprint:s.anonFingerprint})}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,i){let r;const o=s??this._subscribeKey,a=i??this._publishKey;if(this._createSessionPubNub&&o===this._subscribeKey)r=this._createSessionPubNub();else{const n=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;r=new e({subscribeKey:o,publishKey:a||void 0,userId:n,enableEventEngine:!0,...t()})}return n&&r.setToken(n),r}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},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),f(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(h(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),h(i.headers))}if(!i.ok){const e=await i.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${i.status}${e?` ${e}`:""}`)}return i.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};f(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 o(this.config.authProvider),this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const t=e.ownerId||this.defaultOwnerId||"",r=e.taskKind,a=e.duration;if("pipe"===r){if(null==a||!Number.isInteger(a)||a<1||a>43200)throw new Error("Pipe tasks require an integer duration between 1 and 43200 minutes")}else if(null!=a)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",i=t.contentType??"application/octet-stream";if(c(n.size)){const e=await n.getBytes(),r=u({data:e,mimeType:i,fileName:s}),{file:o,fileName:a,text:c,...d}=t;h.push({...d,artifactRef:r})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const r={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},o={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:i,partId:t.partId,uploadSessionId:b},a=await d(r,o,n.uploadBody);a.uploadSessionId&&(b=a.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 f={agentName:e.agentName,billingMode:this._billingMode,requestParts:h};b&&(f.uploadSessionId=b),e.idempotencyKey&&(f.idempotencyKey=e.idempotencyKey),f.ownerId=t;const p={};r&&(p.taskKind=r),null!=a&&(p.duration=a),e.consumerPublicKey&&(p.consumerPublicKey=e.consumerPublicKey),void 0!==e.stream&&(p.stream=e.stream),Object.keys(p).length>0&&(f.extensions={blocks:p}),e.pushNotificationConfig&&(f.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(f.retryPolicy=e.retryPolicy);const m=await n(this.config,"SendMessage",f),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 i({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),r=y(S);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=P(await K(r,v),m.taskId,e.agentName,_);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&C.has(o))return new i({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:o,skipSubscription:!0,preloadedStreams:a,preloadedArtifacts:c});const d="0"!==u?u:s,l=[];let b,h=!1;const f={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(f),S.subscribe({channels:[v],timetoken:d});const p=new i({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:a,preloadedArtifacts:c,externalSubscription:{listener:f,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,p}catch{return new i({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??"",i=n.state??"",r=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}if(await o(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??"",i=n.state??"",r=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:s,tokenResult:r,autoDrain:o,drainWindowMs:a}=e,c=t.taskId,{pamToken:u,channel:d}=r,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(u);try{const e=await b.time(),r=String(e.timetoken),h=y(b);let f,p=new Map,m=[],g=[],k="0";if(h?.fetchMessages){const e=P(await K(h,d),c,n,l);p=e.streams,m=e.artifacts,g=e.events,k=e.highWaterMark,f=e.terminalState}const w=f&&C.has(f)?f:s;if(C.has(w))return new i({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:w,skipSubscription:!0,preloadedStreams:p,preloadedArtifacts:m,preloadedEvents:g});const T="0"!==k?k:r,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 i({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:s,preloadedStreams:p,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,i){const o=s(t,n),a=new r;let c=!1;const u={message:e=>{if(e.channel!==o)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){_(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){_(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){_(e,"onArtifact",t,i.onError)}break;case"terminal":a.tryDeliver(t,e=>{if(i.onTerminal)try{i.onTerminal(e)}catch(t){_(t,"onTerminal",e,i.onError)}});break;case"cancel_requested":if(a.isDelivered)break;if(c)break;if(c=!0,i.onCancelRequested)try{i.onCancelRequested(t)}catch(e){_(e,"onCancelRequested",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){_(e,"onSystem",t,i.onError)}}}}};return e.addListener(u),e.subscribe({channels:[o],timetoken:1e3}),{unsubscribe(){e.removeListener(u),e.unsubscribe({channels:[o]})}}}(this.getPubNub(),e,t,n)}}function _(e,t,n,s){const i=e instanceof Error?e:new Error(String(e));if(s)try{s(i,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else S("warn",`subscribeToTask callback error in ${t}`,{event:"subscribe_callback_error",callbackType:t,error:i.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 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})}
|