@blocks-network/sdk 0.1.62 → 0.1.64
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.
- package/README.md +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/runtime/agent-registry.d.ts +16 -16
- package/dist/runtime/agent-registry.js +1 -1
- package/dist/runtime/auth-provider.d.ts +30 -0
- package/dist/runtime/auth-provider.js +1 -1
- package/dist/runtime/channel-manager.d.ts +9 -9
- package/dist/runtime/channel-manager.js +1 -1
- package/dist/runtime/consumer-auth.d.ts +18 -0
- package/dist/runtime/consumer-auth.js +1 -1
- package/dist/runtime/file-upload.js +1 -1
- package/dist/runtime/pubnub-client.d.ts +1 -1
- package/dist/runtime/rpc-client.js +1 -1
- package/dist/runtime/task-client.js +1 -1
- package/dist/stream/index.d.ts +1 -1
- package/dist/stream/types.d.ts +33 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Create an `agent-card.json`:
|
|
|
34
34
|
"provider": { "organization": "Your Org" }
|
|
35
35
|
},
|
|
36
36
|
"capabilities": { "taskKinds": ["request"] },
|
|
37
|
-
"
|
|
37
|
+
"tags": [{ "id": "main", "name": "Main" }],
|
|
38
38
|
"runtime": {
|
|
39
39
|
"handler": "./handler.ts"
|
|
40
40
|
}
|
|
@@ -77,7 +77,7 @@ The agent card follows the A2A specification with a `runtime` extension:
|
|
|
77
77
|
"provider": { "organization": "Your Org" }
|
|
78
78
|
},
|
|
79
79
|
"capabilities": { "taskKinds": ["request"] },
|
|
80
|
-
"
|
|
80
|
+
"tags": [{ "id": "main", "name": "Main" }],
|
|
81
81
|
"runtime": {
|
|
82
82
|
"handler": "./handler.ts",
|
|
83
83
|
"handlerExport": "default",
|
package/dist/index.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ export { startAgentInstance } from './runtime/agent-instance.js';
|
|
|
2
2
|
export type { ArtifactEntry, HandlerFn, HandlerResult, RequestPart, StartTaskMessage, ExpireTaskMessage, TaskContext, CreateStreamOptions, AgentInstanceOptions, AgentInstancePresenceState, AgentInstanceHandle, } from './runtime/agent-instance.js';
|
|
3
3
|
export type { StreamObject, OnActivateCallback } from './runtime/stream-context.js';
|
|
4
4
|
export type { InboundMessage, StreamError } from './stream/index.js';
|
|
5
|
-
export type { AgentCard, OutputAgentCard,
|
|
5
|
+
export type { AgentCard, OutputAgentCard, AgentTag } from './runtime/agent-registry.js';
|
|
6
6
|
export { createPubNubClient, type PubNubClientConfig } from './runtime/pubnub-client.js';
|
|
7
7
|
export { createChannelManager, ChannelManager, streamChannel } from './runtime/channel-manager.js';
|
|
8
8
|
export { buildArtifactRef, shouldInlineArtifact, decodeInlineArtifact, downloadArtifact, type ArtifactRef, type DownloadedArtifact, } from './runtime/artifacts.js';
|
|
9
|
-
export { connectAgent, fetchAgentRegistry, getAgent, removeAgent,
|
|
9
|
+
export { connectAgent, fetchAgentRegistry, getAgent, removeAgent, fetchAgentsByTag, fetchAgentsByListing, type AgentEntry, type AgentRegistryResult, } from './runtime/agent-registry.js';
|
|
10
10
|
export { DEFAULTS } from './defaults.js';
|
|
11
|
-
/** Platform-wide upload ceiling (bytes). Mirrors
|
|
11
|
+
/** Platform-wide upload ceiling (bytes). Mirrors the service's MAX_FILE_SIZE_BYTES. */
|
|
12
12
|
export declare const BLOCKS_MAX_UPLOAD_BYTES: number;
|
|
13
13
|
export { loadBlocksConfig, type BlocksConfig } from './config-loader.js';
|
|
14
14
|
export { TaskClient, AnonTaskAccessDeniedError } from './runtime/task-client.js';
|
|
@@ -22,7 +22,7 @@ export type { StreamDescriptor } from './runtime/stream-ref.js';
|
|
|
22
22
|
export { AgentAuth, AgentAuthFatalError } from './runtime/agent-auth.js';
|
|
23
23
|
export type { RegistrationPayload, RegistrationResult } from './runtime/agent-auth.js';
|
|
24
24
|
export type { AuthProvider } from './runtime/auth-provider.js';
|
|
25
|
-
export { ConsumerAuth } from './runtime/consumer-auth.js';
|
|
25
|
+
export { ConsumerAuth, AuthRefreshFailedError } from './runtime/consumer-auth.js';
|
|
26
26
|
export type { ConsumerAuthOptions, TokenEndpointConfig, TokenEndpointCredentials, TokenResult, } from './runtime/consumer-auth.js';
|
|
27
27
|
export { StreamRegistry } from './runtime/stream-registry.js';
|
|
28
28
|
export { CredentialCache } from './runtime/credential-cache.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{startAgentInstance}from"./runtime/agent-instance.js";export{createPubNubClient}from"./runtime/pubnub-client.js";export{createChannelManager,ChannelManager,streamChannel}from"./runtime/channel-manager.js";export{buildArtifactRef,shouldInlineArtifact,decodeInlineArtifact,downloadArtifact}from"./runtime/artifacts.js";export{connectAgent,fetchAgentRegistry,getAgent,removeAgent,
|
|
1
|
+
export{startAgentInstance}from"./runtime/agent-instance.js";export{createPubNubClient}from"./runtime/pubnub-client.js";export{createChannelManager,ChannelManager,streamChannel}from"./runtime/channel-manager.js";export{buildArtifactRef,shouldInlineArtifact,decodeInlineArtifact,downloadArtifact}from"./runtime/artifacts.js";export{connectAgent,fetchAgentRegistry,getAgent,removeAgent,fetchAgentsByTag,fetchAgentsByListing}from"./runtime/agent-registry.js";export{DEFAULTS}from"./defaults.js";import{DEFAULTS as e}from"./defaults.js";export const BLOCKS_MAX_UPLOAD_BYTES=e.maxUploadBytes;export{loadBlocksConfig}from"./config-loader.js";export{TaskClient,AnonTaskAccessDeniedError}from"./runtime/task-client.js";export{RpcError,BillingModeMismatchError}from"./runtime/rpc-client.js";export{TaskSession}from"./runtime/task-session.js";export{textPart,filePart,filePartFromPath}from"./runtime/part-helpers.js";export{StreamRef,StreamUnavailableError}from"./runtime/stream-ref.js";export{AgentAuth,AgentAuthFatalError}from"./runtime/agent-auth.js";export{ConsumerAuth,AuthRefreshFailedError}from"./runtime/consumer-auth.js";export{StreamRegistry}from"./runtime/stream-registry.js";export{CredentialCache}from"./runtime/credential-cache.js";export{fetchCdmConfig,DEFAULT_CDM_URL}from"./runtime/cdm-config.js";
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Agent Registry - REST-based agent card storage and discovery.
|
|
3
3
|
*
|
|
4
4
|
* All registry operations (register, fetch, get, remove) use the server-side
|
|
5
|
-
* REST endpoints at /api/v1/registry/agents which handle
|
|
5
|
+
* REST endpoints at /api/v1/registry/agents which handle registry metadata operations,
|
|
6
6
|
* membership indexing, and audit events server-side.
|
|
7
7
|
*/
|
|
8
|
-
import { registryAllChannel,
|
|
8
|
+
import { registryAllChannel, registryTagChannel, registryVisibilityChannel, registryLogChannel, normalizeTagSlug } from './channel-manager.js';
|
|
9
9
|
import type { AgentAuth } from './agent-auth.js';
|
|
10
10
|
/**
|
|
11
11
|
* Scaling configuration for multi-instance support.
|
|
@@ -17,9 +17,9 @@ export interface AgentScaling {
|
|
|
17
17
|
maxRunningTimeSec?: number;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Tag object within an AgentCard.
|
|
21
21
|
*/
|
|
22
|
-
export interface
|
|
22
|
+
export interface AgentTag {
|
|
23
23
|
id: string;
|
|
24
24
|
name: string;
|
|
25
25
|
description?: string;
|
|
@@ -29,7 +29,7 @@ export interface AgentSkill {
|
|
|
29
29
|
* Agent card payload (input schema) — authored by developers in agent-card.json.
|
|
30
30
|
*
|
|
31
31
|
* Uses the 9-section structure: identity, capabilities, io, streams,
|
|
32
|
-
* security, services,
|
|
32
|
+
* security, services, tags, extensions, runtime.
|
|
33
33
|
*/
|
|
34
34
|
export interface AgentCard {
|
|
35
35
|
identity: {
|
|
@@ -89,7 +89,7 @@ export interface AgentCard {
|
|
|
89
89
|
services?: {
|
|
90
90
|
webhooks?: boolean;
|
|
91
91
|
};
|
|
92
|
-
|
|
92
|
+
tags: AgentTag[];
|
|
93
93
|
extensions?: Record<string, unknown>;
|
|
94
94
|
runtime?: {
|
|
95
95
|
handler?: string;
|
|
@@ -125,7 +125,7 @@ export interface OutputAgentCard {
|
|
|
125
125
|
streams?: AgentCard['streams'];
|
|
126
126
|
security?: AgentCard['security'];
|
|
127
127
|
services?: AgentCard['services'];
|
|
128
|
-
|
|
128
|
+
tags: AgentTag[];
|
|
129
129
|
extensions?: Record<string, unknown>;
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
@@ -138,8 +138,8 @@ export interface AgentEntry {
|
|
|
138
138
|
displayName: string;
|
|
139
139
|
/** Human-readable description */
|
|
140
140
|
description?: string;
|
|
141
|
-
/**
|
|
142
|
-
|
|
141
|
+
/** Tag objects from agent card */
|
|
142
|
+
tags?: AgentTag[];
|
|
143
143
|
/** Multi-instance scaling configuration */
|
|
144
144
|
scaling?: AgentScaling;
|
|
145
145
|
/** Full agent card payload */
|
|
@@ -171,8 +171,8 @@ export interface ConnectAgentOptions {
|
|
|
171
171
|
billingMode: 'free' | 'paid';
|
|
172
172
|
/** Human-readable description */
|
|
173
173
|
description?: string;
|
|
174
|
-
/**
|
|
175
|
-
|
|
174
|
+
/** Tag objects advertised by this agent */
|
|
175
|
+
tags?: AgentTag[];
|
|
176
176
|
/** Multi-instance scaling configuration */
|
|
177
177
|
scaling?: AgentScaling;
|
|
178
178
|
/** Full agent card payload */
|
|
@@ -208,7 +208,7 @@ export interface RegistryAuditEvent {
|
|
|
208
208
|
source: string;
|
|
209
209
|
agentName?: string;
|
|
210
210
|
visibility?: 'public' | 'private';
|
|
211
|
-
|
|
211
|
+
tags?: string[];
|
|
212
212
|
cardHash?: string;
|
|
213
213
|
cardRef?: string;
|
|
214
214
|
summaryHash?: string;
|
|
@@ -232,7 +232,7 @@ declare function detectDeviceOs(): string;
|
|
|
232
232
|
* Register or update an agent in the registry via REST.
|
|
233
233
|
*
|
|
234
234
|
* Sends a POST request to the dedicated registry endpoint which handles
|
|
235
|
-
* all
|
|
235
|
+
* all registry metadata operations (UUID metadata, memberships, audit events)
|
|
236
236
|
* server-side.
|
|
237
237
|
*/
|
|
238
238
|
export declare function connectAgent(agentName: string, options: ConnectAgentOptions): Promise<{
|
|
@@ -261,9 +261,9 @@ export declare function fetchAgentRegistry(options?: {
|
|
|
261
261
|
baseUrl?: string;
|
|
262
262
|
}): Promise<AgentRegistryResult>;
|
|
263
263
|
/**
|
|
264
|
-
* Fetch agents filtered by
|
|
264
|
+
* Fetch agents filtered by tag.
|
|
265
265
|
*/
|
|
266
|
-
export declare function
|
|
266
|
+
export declare function fetchAgentsByTag(tag: string, options?: {
|
|
267
267
|
limit?: number;
|
|
268
268
|
cursor?: string;
|
|
269
269
|
baseUrl?: string;
|
|
@@ -283,4 +283,4 @@ export declare function removeAgent(agentName: string, options?: {
|
|
|
283
283
|
baseUrl?: string;
|
|
284
284
|
agentAuth?: AgentAuth;
|
|
285
285
|
}): Promise<boolean>;
|
|
286
|
-
export { detectDeviceOs, registryAllChannel,
|
|
286
|
+
export { detectDeviceOs, registryAllChannel, registryTagChannel, registryVisibilityChannel, registryLogChannel, normalizeTagSlug, };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:os";import{registryAllChannel as n,
|
|
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};
|
|
@@ -33,7 +33,37 @@ export interface AuthProvider {
|
|
|
33
33
|
* calls are no-ops after the first success.
|
|
34
34
|
*/
|
|
35
35
|
ensureReady?(): Promise<void>;
|
|
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
|
|
40
|
+
* authenticated request and throws the error directly so consumers see
|
|
41
|
+
* the typed `AuthRefreshFailedError` instead of an opaque downstream
|
|
42
|
+
* 401. Implementations without proactive refresh return null / omit.
|
|
43
|
+
*/
|
|
44
|
+
getLastAuthError?(): Error | null;
|
|
36
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Pre-flight gate used by authenticated SDK paths (RPC, file upload,
|
|
48
|
+
* TaskClient methods) to surface a recorded permanent-refresh error
|
|
49
|
+
* before issuing the request.
|
|
50
|
+
*
|
|
51
|
+
* Behavior:
|
|
52
|
+
* - No `getLastAuthError` / no recorded error -> returns immediately.
|
|
53
|
+
* - Error recorded -> attempts one reactive refresh via
|
|
54
|
+
* `onAuthFailure()`. On success the error clears atomically with the
|
|
55
|
+
* new token apply and this returns silently (caller proceeds with the
|
|
56
|
+
* refreshed token). On failure the error stays recorded and this
|
|
57
|
+
* throws it so the caller sees `AuthRefreshFailedError` instead of an
|
|
58
|
+
* opaque downstream 401.
|
|
59
|
+
*
|
|
60
|
+
* The two-phase shape (record-then-retry-then-throw) preserves the
|
|
61
|
+
* documented reactive recovery path: a transient outage that exhausts
|
|
62
|
+
* proactive retries and then resolves still lets the next authenticated
|
|
63
|
+
* call recover, instead of permanently wedging the client until it is
|
|
64
|
+
* rebuilt.
|
|
65
|
+
*/
|
|
66
|
+
export declare function preflightAuthOrThrow(provider: AuthProvider | undefined): Promise<void>;
|
|
37
67
|
/**
|
|
38
68
|
* Wraps a static JWT string. No refresh capability -- onAuthFailure()
|
|
39
69
|
* always returns false.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class StaticAuthProvider{constructor(t){this.token=t}getAuthHeader(){return`Bearer ${this.token}`}async onAuthFailure(){return!1}}
|
|
1
|
+
export async function preflightAuthOrThrow(t){if(!t?.getLastAuthError)return;if(!t.getLastAuthError())return;await t.onAuthFailure();const r=t.getLastAuthError();if(r)throw r}export class StaticAuthProvider{constructor(t){this.token=t}getAuthHeader(){return`Bearer ${this.token}`}async onAuthFailure(){return!1}}
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* - agent.{agentId}.control - Control plane per agent ID
|
|
7
7
|
* - obs.{agentName}.log - Agent-level observability
|
|
8
8
|
*
|
|
9
|
-
* Registry channels (
|
|
9
|
+
* Registry channels (membership indexes):
|
|
10
10
|
* - registry.all - All agents (membership index)
|
|
11
11
|
* - registry.public - Public agents (membership index)
|
|
12
12
|
* - registry.private - Private agents (membership index)
|
|
13
|
-
* - registry.
|
|
13
|
+
* - registry.tag.{tag} - Agents with specific tag (membership index)
|
|
14
14
|
* - registry.log - Audit log channel (pub/sub with history)
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* Channel metadata (not pub/sub):
|
|
17
17
|
* - uuid:{agentName} - Agent card stored as User Object
|
|
18
18
|
*
|
|
19
19
|
* All channels respect PubNub's 3-level maximum depth constraint.
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const registryAllChannel: () => string;
|
|
27
27
|
/**
|
|
28
|
-
* Registry
|
|
29
|
-
* Format: registry.
|
|
28
|
+
* Registry tag channel - membership index for agents with a tag.
|
|
29
|
+
* Format: registry.tag.{tag}
|
|
30
30
|
*
|
|
31
|
-
*
|
|
31
|
+
* Tag slugs are normalized: lowercase, alphanumeric + `.` or `_`.
|
|
32
32
|
*/
|
|
33
|
-
export declare const
|
|
33
|
+
export declare const registryTagChannel: (tag: string) => string;
|
|
34
34
|
/**
|
|
35
35
|
* Registry visibility channel - membership index for public/private agents.
|
|
36
36
|
* Format: registry.public or registry.private
|
|
@@ -42,7 +42,7 @@ export declare const registryVisibilityChannel: (isPublic: boolean) => string;
|
|
|
42
42
|
*/
|
|
43
43
|
export declare const registryLogChannel: () => string;
|
|
44
44
|
/**
|
|
45
|
-
* Normalize a
|
|
45
|
+
* Normalize a tag string to a stable slug.
|
|
46
46
|
* - Lowercase
|
|
47
47
|
* - Replace non-alphanumeric characters (except `.` and `_`) with `_`
|
|
48
48
|
* - Collapse multiple underscores
|
|
@@ -52,7 +52,7 @@ export declare const registryLogChannel: () => string;
|
|
|
52
52
|
* - "text.embeddings" -> "text.embeddings"
|
|
53
53
|
* - "Image Generation" -> "image_generation"
|
|
54
54
|
*/
|
|
55
|
-
export declare const
|
|
55
|
+
export declare const normalizeTagSlug: (tag: string) => string;
|
|
56
56
|
/**
|
|
57
57
|
* Validates that an owner ID is not a reserved prefix.
|
|
58
58
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const registryAllChannel=()=>"registry.all";export const
|
|
1
|
+
export const registryAllChannel=()=>"registry.all";export const registryTagChannel=r=>`registry.tag.${normalizeTagSlug(r)}`;export const registryVisibilityChannel=r=>r?"registry.public":"registry.private";export const registryLogChannel=()=>"registry.log";export const normalizeTagSlug=r=>r.toLowerCase().replace(/[^a-z0-9._]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"");const r=["agent","obs","sys","u","anonymous","system","stream"];export function validateOwnerId(e){return!(!e||"string"!=typeof e)&&!r.includes(e.toLowerCase())}export class ChannelManager{constructor(r={}){if(!r.agentName)throw new Error("agentName is required for ChannelManager");this.agentName=r.agentName}getAgentName(){return this.agentName}taskChannel(r,e){if(!e)throw new Error("orgId required for task channel");if(!r)throw new Error("taskId required for task channel");return`u.${e}.${r}`}controlChannel(r){return`agent.${r}.control`}obsChannel(r){return`obs.${r??this.agentName}.log`}userTaskPattern(r){if(!r)throw new Error("orgId required for user task pattern");return`u.${r}.*`}agentWildcard(r){return`agent.${r??this.agentName}.*`}streamChannel(r,e){if(!r)throw new Error("streamId required for stream channel");return`stream.${e??this.agentName}.${r}`}streamWildcard(r){return`stream.${r??this.agentName}.*`}}export const createChannelManager=r=>new ChannelManager({agentName:r});export const taskChannel=(r,e)=>{if(!e)throw new Error("orgId required for task channel");if(!r)throw new Error("taskId required for task channel");return`u.${e}.${r}`};export const userTaskPattern=r=>{if(!r)throw new Error("orgId required for user task pattern");return`u.${r}.*`};export const streamChannel=(r,e)=>{if(!r)throw new Error("agentName required for stream channel");if(!e)throw new Error("streamId required for stream channel");return`stream.${r}.${e}`};
|
|
@@ -49,6 +49,16 @@ export interface ConsumerAuthOptions {
|
|
|
49
49
|
baseUrl?: string;
|
|
50
50
|
onAuthError?: (error: Error) => void;
|
|
51
51
|
}
|
|
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.
|
|
57
|
+
*/
|
|
58
|
+
export declare class AuthRefreshFailedError extends Error {
|
|
59
|
+
readonly cause: Error;
|
|
60
|
+
constructor(cause: Error);
|
|
61
|
+
}
|
|
52
62
|
export declare class ConsumerAuth implements AuthProvider {
|
|
53
63
|
private readonly _apiKey?;
|
|
54
64
|
private readonly _tokenEndpoint?;
|
|
@@ -64,6 +74,7 @@ export declare class ConsumerAuth implements AuthProvider {
|
|
|
64
74
|
private _refreshPromise;
|
|
65
75
|
private _initialized;
|
|
66
76
|
private _initPromise;
|
|
77
|
+
private _lastAuthError;
|
|
67
78
|
constructor(options: ConsumerAuthOptions);
|
|
68
79
|
/**
|
|
69
80
|
* Acquire the initial token. Must be called before any other method.
|
|
@@ -71,6 +82,13 @@ export declare class ConsumerAuth implements AuthProvider {
|
|
|
71
82
|
init(): Promise<void>;
|
|
72
83
|
getAuthHeader(): string | null;
|
|
73
84
|
getUserId(): string | null;
|
|
85
|
+
/**
|
|
86
|
+
* Returns the last permanent refresh failure, or null. Set when proactive
|
|
87
|
+
* refresh exhausts its 3 retries; cleared on a successful reactive refresh.
|
|
88
|
+
* Callers (TaskClient.sendMessage / connect) use this to fail fast before
|
|
89
|
+
* making any authenticated request.
|
|
90
|
+
*/
|
|
91
|
+
getLastAuthError(): AuthRefreshFailedError | null;
|
|
74
92
|
/**
|
|
75
93
|
* Reactive refresh triggered by a 401 response. Returns true if a new
|
|
76
94
|
* token was acquired. Concurrent callers share a single in-flight
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{log as e}from"./logger.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as
|
|
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 +1 @@
|
|
|
1
|
-
import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as o}from"./protocol-version.js";import{captureAffinity as
|
|
1
|
+
import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as o}from"./protocol-version.js";import{preflightAuthOrThrow as a}from"./auth-provider.js";import{captureAffinity as e,injectAffinity as r}from"./write-affinity.js";async function i(i,n,s){await a(i.authProvider);const u=`${i.baseUrl.replace(/\/+$/,"")}${n}`,d=a=>{const e={"Content-Type":"application/json",[o]:t},n=i.authProvider?.getAuthHeader();return n&&(e.Authorization=n),a&&r(e),{method:"POST",credentials:"omit",headers:e,body:JSON.stringify(s)}};if(i.agentAuth)return i.agentAuth.authenticatedFetch(u,d(!1));const c=await fetch(u,d(!0));if(e(c.headers),401===c.status&&i.authProvider){if(await i.authProvider.onAuthFailure()){const t=await fetch(u,d(!0));return e(t.headers),t}}return c}export async function requestUpload(t,o){const a=await i(t,"/api/v1/files/request-upload",o);if(!a.ok){const t=await a.text().catch(()=>"");throw new Error(`request-upload failed: HTTP ${a.status} ${t}`)}return a.json()}export async function uploadToStorage(t,o,a,e,r){const i=new FormData;for(const t of o)i.append(t.key,t.value);const n=a instanceof Blob?a:new Blob([a],{type:r});i.append("file",n,e);const s=await fetch(t,{method:"POST",body:i});if(!s.ok){const t=await s.text().catch(()=>"");throw new Error(`S3 upload failed: HTTP ${s.status} ${t}`)}}export async function confirmUpload(t,o,a){const e=await i(t,"/api/v1/files/confirm-upload",{uploadId:o});if(!e.ok){const t=await e.text().catch(()=>"");throw new Error(`confirm-upload failed: HTTP ${e.status} ${t}`)}return e.json()}export async function uploadFile(t,o,a){const e=await requestUpload(t,o);if(await uploadToStorage(e.uploadUrl,e.formFields,a,o.fileName,o.mimeType),"provider-output"===o.role){return await confirmUpload(t,e.uploadId,!0)}const r=e;return await confirmUpload(t,e.uploadId),{uploadSessionId:r.uploadSessionId,uploadId:e.uploadId}}
|
|
@@ -13,7 +13,7 @@ export interface PubNubClientConfig {
|
|
|
13
13
|
*/
|
|
14
14
|
announceSuccessfulHeartbeats?: boolean;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Silent-park fix. When true, the client uses an
|
|
17
17
|
* ExponentialRetryPolicy with maximumRetry expanded to ~30 days
|
|
18
18
|
* (43_200 attempts at the 60s cap). PubNub's Event Engine otherwise
|
|
19
19
|
* exhausts the default 6-attempt budget after ~3.5 min and parks in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CURRENT_PROTOCOL_VERSION as
|
|
1
|
+
import{preflightAuthOrThrow as r}from"./auth-provider.js";import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as e}from"./protocol-version.js";import{captureAffinity as o,injectAffinity as n}from"./write-affinity.js";export async function withRetry(r,t=3,e=500){let o;for(let n=0;n<t;n++)try{return await r()}catch(r){o=r;const i=String(r);if(!(i.includes("ENOTFOUND")||i.includes("ETIMEDOUT")||i.includes("ECONNRESET")||i.includes("NetworkIssues"))||n===t-1)throw r;const a=e*Math.pow(2,n)+100*Math.random();await new Promise(r=>setTimeout(r,a))}throw o}export function rpcEndpoint(r,t){return t?`${t.replace(/\/+$/,"")}/api/v1/rpc`:`https://ps.pndsn.com/v1/blocks/sub-key/${r}/rpc`}export class RpcError extends Error{constructor(r,t,e){super(`[RPC] ${r}`),this.name="RpcError",this.rpcMessage=r,this.code=t,this.data=e}}export class BillingModeMismatchError extends RpcError{constructor(r,t,e,o,n){super(r,o,n),this.name="BillingModeMismatchError",this.expected=t,this.got=e}}export async function callRpc(i,a,s){const c=rpcEndpoint(i.subscribeKey,i.baseUrl),d=`rpc-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,u=r=>{const o={"Content-Type":"application/json",[e]:t},a=i.authProvider?.getAuthHeader();return a&&(o.Authorization=a),r&&n(o),o},h={jsonrpc:"2.0",id:d,method:a,params:s},p=async()=>{let r;if(i.agentAuth?r=await i.agentAuth.authenticatedFetch(c,{method:"POST",headers:u(!1),body:JSON.stringify(h)}):(r=await fetch(c,{method:"POST",headers:u(!0),body:JSON.stringify(h)}),o(r.headers)),!r.ok)throw new RpcError(`HTTP ${r.status}`,r.status);const t=await r.json();if(t.error){const r=t.error.data?.message??t.error.message??"Unknown RPC error",e=function(r,t,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(r,i.expected,i.got,t,e)}(r,t.error.code,t.error.data);if(e)throw e;throw new RpcError(r,t.error.code,t.error.data)}return t.result};return await r(i.authProvider),i.authProvider?.ensureReady&&await i.authProvider.ensureReady(),withRetry(async()=>{try{return await p()}catch(r){if(r instanceof RpcError&&401===r.code&&i.authProvider&&!i.agentAuth){if(await i.authProvider.onAuthFailure())return p()}throw r}})}
|
|
@@ -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{ConsumerAuth as r}from"./consumer-auth.js";import{shouldInlineArtifact as o,buildArtifactRef as a}from"./artifacts.js";import{uploadFile as c}from"./file-upload.js";import{normalizeFileInput as u}from"./file-input.js";import{fetchCdmConfig as d}from"./cdm-config.js";import{captureAffinity as l,injectAffinity as b}from"./write-affinity.js";import{getEnv as h}from"../env.js";import{log as p}from"./logger.js";import{StreamRef as f}from"./stream-ref.js";import{invertDirection as m}from"../stream/index.js";import{asPubNubFetcher as g}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as k,PROTOCOL_VERSION_HEADER as y}from"./protocol-version.js";import{getAgent as w}from"./agent-registry.js";const T=(e,t,n)=>p("[TaskClient]",e,t,n),I=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 S(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=m(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){T("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 f(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 C(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 o=!!(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"!==i)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const c=s.cdmUrl??h("BLOCKS_CDM_URL"),u=await d(c),l="paid"===i?u.network:u.playground,b=s.subscribeKey??h("BLOCKS_SUBSCRIBE_KEY")??l.subscribeKey,p=s.publishKey??h("BLOCKS_PUBLISH_KEY")??l.publishKey,f=s.baseUrl??h("BLOCKS_BACKEND_URL")??u.api.baseUrl;if(!b)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:b,publishKey:p||void 0,userId:n,enableEventEngine:!0,...t()})};if(o){const e=new r({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:f,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({billingMode:i,subscribeKey:b,publishKey:p,baseUrl:f,createSessionPubNub:m,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:i,subscribeKey:b,publishKey:p,baseUrl:f,createSessionPubNub:m,anonFingerprint:s.anonFingerprint})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await w(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",[y]:k},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),b(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(l(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),l(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",[y]:k};b(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(l(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()");this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const t=e.ownerId||this.defaultOwnerId||"",r=e.taskKind,d=e.duration;if("pipe"===r){if(null==d||!Number.isInteger(d)||d<1||d>43200)throw new Error("Pipe tasks require a duration between 1 and 43200 minutes")}else if(null!=d)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let l;const b=[];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=u(t.file),s=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(o(n.size)){const e=await n.getBytes(),r=a({data:e,mimeType:i,fileName:s}),{file:o,fileName:c,text:u,...d}=t;b.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:l},a=await c(r,o,n.uploadBody);a.uploadSessionId&&(l=a.uploadSessionId);const{file:u,fileName:d,...h}=t;b.push({partId:h.partId,...h.contentType?{contentType:h.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;b.push(s)}const h={agentName:e.agentName,billingMode:this._billingMode,requestParts:b};l&&(h.uploadSessionId=l),e.idempotencyKey&&(h.idempotencyKey=e.idempotencyKey),h.ownerId=t;const p={};r&&(p.taskKind=r),null!=d&&(p.duration=d),e.consumerPublicKey&&(p.consumerPublicKey=e.consumerPublicKey),Object.keys(p).length>0&&(h.extensions={blocks:p}),e.pushNotificationConfig&&(h.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(h.retryPolicy=e.retryPolicy);const f=await n(this.config,"SendMessage",h),m=f.extensions?.blocks,k=m?.readToken??null,y=m?.streamChannels?.status??void 0,w=m?.subscribeKey??this._subscribeKey,T=m?.publishKey??this._publishKey;if(!0===f.idempotent&&!!f.state&&I.has(f.state))return new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:y,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:w,publishKey:T},rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,preClosed:!0,state:f.state});const P=this.createPerSessionPubNub(k,w,T),K={subscribeKey:w,publishKey:T},_=y??s(f.taskId,f.orgId??t);try{const n=await P.time(),s=String(n.timetoken),r=g(P);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=S(await C(r,_),f.taskId,e.agentName,K);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&I.has(o))return new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.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 p={message:e=>{if(e.channel!==_)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})}};P.addListener(p),P.subscribe({channels:[_],timetoken:d});const m=new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:a,preloadedArtifacts:c,externalSubscription:{listener:p,channel:_,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,m}catch{return new i({taskId:f.taskId,ownerId:t,orgId:f.orgId??t,readToken:k,statusChannel:_,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.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(!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=g(b);let p,f=new Map,m=[],k=[],y="0";if(h?.fetchMessages){const e=S(await C(h,d),c,n,l);f=e.streams,m=e.artifacts,k=e.events,y=e.highWaterMark,p=e.terminalState}const w=p&&I.has(p)?p:s;if(I.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:f,preloadedArtifacts:m,preloadedEvents:k});const T="0"!==y?y:r,P=[];let K,_=!1;const N={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?K(t,n):P.push({message:t,timetoken:n})}};b.addListener(N),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:f,preloadedArtifacts:m,preloadedEvents:k,externalSubscription:{listener:N,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of P)t.timetoken>T&&(E._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return P.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 r=s(t,n),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){P(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){P(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){P(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){P(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){P(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,n)}}function P(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 T("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 i}from"./task-session.js";import{preflightAuthOrThrow as r}from"./auth-provider.js";import{ConsumerAuth as o}from"./consumer-auth.js";import{shouldInlineArtifact as a,buildArtifactRef as c}from"./artifacts.js";import{uploadFile as u}from"./file-upload.js";import{normalizeFileInput as d}from"./file-input.js";import{fetchCdmConfig as l}from"./cdm-config.js";import{captureAffinity as b,injectAffinity as h}from"./write-affinity.js";import{getEnv as p}from"../env.js";import{log as f}from"./logger.js";import{StreamRef as m}from"./stream-ref.js";import{invertDirection as g}from"../stream/index.js";import{asPubNubFetcher as k}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as y,PROTOCOL_VERSION_HEADER as w}from"./protocol-version.js";import{getAgent as T}from"./agent-registry.js";const I=(e,t,n)=>f("[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 C(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=g(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){I("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 m(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 P(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),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"!==i)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const c=s.cdmUrl??p("BLOCKS_CDM_URL"),u=await l(c),d="paid"===i?u.network:u.playground,b=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(!b)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:b,publishKey:h||void 0,userId:n,enableEventEngine:!0,...t()})};if(r){const e=new o({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:f,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({billingMode:i,subscribeKey:b,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:b,publishKey:h,baseUrl:f,createSessionPubNub:m,anonFingerprint:s.anonFingerprint})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await T(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",[w]:y},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),h(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(b(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),b(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",[w]:y};h(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(b(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 r(this.config.authProvider),this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();const t=e.ownerId||this.defaultOwnerId||"",o=e.taskKind,l=e.duration;if("pipe"===o){if(null==l||!Number.isInteger(l)||l<1||l>43200)throw new Error("Pipe tasks require an integer duration between 1 and 43200 minutes")}else if(null!=l)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=d(t.file),s=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(a(n.size)){const e=await n.getBytes(),r=c({data:e,mimeType:i,fileName:s}),{file:o,fileName:a,text:u,...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 u(r,o,n.uploadBody);a.uploadSessionId&&(b=a.uploadSessionId);const{file:c,fileName:d,...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={};o&&(f.taskKind=o),null!=l&&(f.duration=l),e.consumerPublicKey&&(f.consumerPublicKey=e.consumerPublicKey),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,y=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 i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,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 K=this.createPerSessionPubNub(y,T,I),_={subscribeKey:T,publishKey:I},N=w??s(m.taskId,m.orgId??t);try{const n=await K.time(),s=String(n.timetoken),r=k(K);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=C(await P(r,N),m.taskId,e.agentName,_);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&S.has(o))return new i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,statusChannel:N,agentName:e.agentName,pubnub:K,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 p={message:e=>{if(e.channel!==N)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})}};K.addListener(p),K.subscribe({channels:[N],timetoken:d});const f=new i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,statusChannel:N,agentName:e.agentName,pubnub:K,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:p,channel:N,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 i({taskId:m.taskId,ownerId:t,orgId:m.orgId??t,readToken:y,statusChannel:N,agentName:e.agentName,pubnub:K,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 r(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??"",o=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:o,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=k(b);let p,f=new Map,m=[],g=[],y="0";if(h?.fetchMessages){const e=C(await P(h,d),c,n,l);f=e.streams,m=e.artifacts,g=e.events,y=e.highWaterMark,p=e.terminalState}const w=p&&S.has(p)?p:s;if(S.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:f,preloadedArtifacts:m,preloadedEvents:g});const T="0"!==y?y:r,I=[];let K,_=!1;const N={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?K(t,n):I.push({message:t,timetoken:n})}};b.addListener(N),b.subscribe({channels:[d],timetoken:T});const v=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:f,preloadedArtifacts:m,preloadedEvents:g,externalSubscription:{listener:N,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of I)t.timetoken>T&&(v._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return I.length=0,_=!0,v}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 r=s(t,n),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){K(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){K(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){K(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){K(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){K(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,n)}}function K(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 I("warn",`subscribeToTask callback error in ${t}`,{event:"subscribe_callback_error",callbackType:t,error:i.message})}
|
package/dist/stream/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export { StreamClient } from './stream-client.js';
|
|
|
8
8
|
export type { StreamError } from './stream-client.js';
|
|
9
9
|
export { type StreamDescriptor, type StreamAffinity, invertDirection } from './descriptor.js';
|
|
10
10
|
export { validateStreamId } from './validate.js';
|
|
11
|
-
export type { StreamFormat, StreamDirection, StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage, StreamBundleConfig, } from './types.js';
|
|
11
|
+
export type { StreamFormat, StreamDirection, StreamClientOptions, StreamClientFromDescriptorOptions, InboundMessage, InboundBytesMessage, InboundEventsMessage, InboundRawMessage, StreamBundleConfig, } from './types.js';
|
package/dist/stream/types.d.ts
CHANGED
|
@@ -75,11 +75,40 @@ export interface StreamClientFromDescriptorOptions {
|
|
|
75
75
|
*/
|
|
76
76
|
consumerUserId?: string;
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Normalized inbound message yielded by the inbound async iterator.
|
|
80
|
+
*
|
|
81
|
+
* Discriminated by `format`. `data` shape:
|
|
82
|
+
* - `format: 'bytes'` → `string[]` (chunks; `encoding` is `'utf8'` or `'base64'`)
|
|
83
|
+
* - `format: 'events'` → `unknown[]` (one or more events from a single producer flush)
|
|
84
|
+
* - `format: 'raw'` → `Record<string, unknown>` (passthrough for unknown message types)
|
|
85
|
+
*
|
|
86
|
+
* Note: the array-vs-single shape is the SDK's own producer-side bundler
|
|
87
|
+
* (`stream-bundle.ts`) coalescing writes by size/latency — NOT PubNub transport
|
|
88
|
+
* batching. A single producer `write()` already yields a 1-element array on
|
|
89
|
+
* the wire. Application code should prefer `bytes()` / `events()` which
|
|
90
|
+
* flatten on the consumer's behalf; reach for `inbound` only when you
|
|
91
|
+
* need raw envelope metadata.
|
|
92
|
+
*/
|
|
93
|
+
export type InboundMessage = InboundBytesMessage | InboundEventsMessage | InboundRawMessage;
|
|
94
|
+
export interface InboundBytesMessage {
|
|
95
|
+
format: 'bytes';
|
|
96
|
+
data: string[];
|
|
97
|
+
seq: number;
|
|
98
|
+
ts: number;
|
|
99
|
+
encoding: string;
|
|
100
|
+
}
|
|
101
|
+
export interface InboundEventsMessage {
|
|
102
|
+
format: 'events';
|
|
103
|
+
data: unknown[];
|
|
104
|
+
seq: number;
|
|
105
|
+
ts: number;
|
|
106
|
+
encoding: string;
|
|
107
|
+
}
|
|
108
|
+
export interface InboundRawMessage {
|
|
109
|
+
format: 'raw';
|
|
110
|
+
data: Record<string, unknown>;
|
|
81
111
|
seq: number;
|
|
82
112
|
ts: number;
|
|
83
|
-
format: 'bytes' | 'events' | 'raw';
|
|
84
113
|
encoding: string;
|
|
85
114
|
}
|