Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dts-e2e-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: 🔧 Set environment variables
run: |
echo "TASKHUB=default" >> $GITHUB_ENV
echo "ENDPOINT=localhost:8080" >> $GITHUB_ENV
echo "ENDPOINT=http://localhost:8080" >> $GITHUB_ENV

- name: ⚙️ NodeJS - Install
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand Down
4 changes: 2 additions & 2 deletions examples/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ npm run start:emulator
### Expected Output

```
Connecting to endpoint: localhost:8080, taskHub: default
Connecting to endpoint: http://localhost:8080, taskHub: default
Worker started successfully

--- Signaling entity operations ---
Expand Down Expand Up @@ -56,7 +56,7 @@ npm run start:emulator
### Expected Output

```
Connecting to endpoint: localhost:8080, taskHub: default
Connecting to endpoint: http://localhost:8080, taskHub: default
Worker started successfully

--- Initializing accounts ---
Expand Down
2 changes: 1 addition & 1 deletion examples/entity-counter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ npm run start
## Expected Output

```
Connecting to endpoint: localhost:8080, taskHub: default
Connecting to endpoint: http://localhost:8080, taskHub: default
Worker started successfully

--- Signaling entity operations ---
Expand Down
2 changes: 1 addition & 1 deletion examples/entity-counter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from "@microsoft/durabletask-js-azuremanaged";

// Read environment variables for DTS emulator or local sidecar
const endpoint = process.env.ENDPOINT || "localhost:4001";
const endpoint = process.env.ENDPOINT || "http://localhost:4001";
const taskHub = process.env.TASKHUB || "default";

// ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion examples/entity-counter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"scripts": {
"start": "ts-node --swc index.ts",
"start:emulator": "ENDPOINT=localhost:8080 TASKHUB=default ts-node --swc index.ts"
"start:emulator": "ENDPOINT=http://localhost:8080 TASKHUB=default ts-node --swc index.ts"
},
"dependencies": {
"@microsoft/durabletask-js": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion examples/entity-orchestration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ npm run start
## Expected Output

```
Connecting to endpoint: localhost:8080, taskHub: default
Connecting to endpoint: http://localhost:8080, taskHub: default
Worker started successfully

--- Initializing accounts ---
Expand Down
2 changes: 1 addition & 1 deletion examples/entity-orchestration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from "@microsoft/durabletask-js-azuremanaged";

// Read environment variables for DTS emulator or local sidecar
const endpoint = process.env.ENDPOINT || "localhost:4001";
const endpoint = process.env.ENDPOINT || "http://localhost:4001";
const taskHub = process.env.TASKHUB || "default";

// ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion examples/entity-orchestration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"scripts": {
"start": "ts-node --swc index.ts",
"start:emulator": "ENDPOINT=localhost:8080 TASKHUB=default ts-node --swc index.ts"
"start:emulator": "ENDPOINT=http://localhost:8080 TASKHUB=default ts-node --swc index.ts"
},
"dependencies": {
"@microsoft/durabletask-js": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion examples/work-item-filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
DurableTaskAzureManagedWorkerBuilder,
} from "@microsoft/durabletask-js-azuremanaged";

const endpoint = process.env.ENDPOINT || "localhost:8080";
const endpoint = process.env.ENDPOINT || "http://localhost:8080";
const taskHub = process.env.TASKHUB || "default";

// ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion examples/work-item-filters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"scripts": {
"start": "ts-node --swc index.ts",
"start:emulator": "ENDPOINT=localhost:8080 TASKHUB=default ts-node --swc index.ts"
"start:emulator": "ENDPOINT=http://localhost:8080 TASKHUB=default ts-node --swc index.ts"
},
"dependencies": {
"@microsoft/durabletask-js": "workspace:*",
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/durabletask-js-azuremanaged/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@
- Add `DurableTaskAzureManagedWorkerBuilder.channelRecreateFailureThreshold()` to control when
consecutive likely-poisoned failures recreate the gRPC channel.

### Breaking changes

- Schemeless endpoints now default to HTTPS. Connection-string and builder `.endpoint(...)` users
must prefix plaintext local or emulator endpoints with `http://`.

### Fixes

- Select TLS from the Azure-managed endpoint scheme independently of authentication, and require explicit opt-in
before sending token credentials over an insecure endpoint.

## v0.4.0 (2026-07-31)

### Changes
Expand Down
22 changes: 22 additions & 0 deletions packages/durabletask-js-azuremanaged/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ The connection string `Authentication` parameter supports the following values:
Endpoint=<endpoint>;Authentication=<auth-type>;TaskHub=<task-hub-name>[;ClientID=<client-id>][;TenantId=<tenant-id>]
```

## Transport Security

Endpoint transport and authentication are configured independently:

- `https://` endpoints always use TLS.
- `http://` endpoints use plaintext and are intended for local development and testing.
- Endpoints without a scheme default to HTTPS.
- Token credentials with an HTTP endpoint are rejected unless the client or worker builder explicitly calls
`.allowInsecureCredentials(true)`. This opt-in permits authentication metadata over plaintext; it never
downgrades an HTTPS endpoint.

`connectionString()` replaces only the Azure-managed connection options. Builder-level state such as the logger,
gRPC channel options, and worker registrations is preserved. Connection options configured before it are reset, so
`.allowInsecureCredentials(true)` must follow `.connectionString(...)`:

```typescript
new DurableTaskAzureManagedClientBuilder()
.connectionString("Endpoint=http://localhost:8080;Authentication=DefaultAzure;TaskHub=myTaskHub")
.allowInsecureCredentials(true)
.build();
```

## API Reference

### Classes
Expand Down
18 changes: 12 additions & 6 deletions packages/durabletask-js-azuremanaged/src/client-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export class DurableTaskAzureManagedClientBuilder {

/**
* Configures the builder using a connection string.
* This replaces the Azure-managed connection options. Builder-level settings such as the logger and gRPC
* channel options are preserved. Call connection option setters after this method.
*
* @param connectionString The connection string for Azure-managed Durable Task service.
* @returns This builder instance.
Expand Down Expand Up @@ -63,8 +65,7 @@ export class DurableTaskAzureManagedClientBuilder {
this._options
.setEndpointAddress(endpoint)
.setTaskHubName(taskHubName)
.setCredential(credential ?? null)
.setAllowInsecureCredentials(credential === null || credential === undefined);
.setCredential(credential ?? null);

return this;
}
Expand Down Expand Up @@ -157,10 +158,15 @@ export class DurableTaskAzureManagedClientBuilder {
...this._grpcChannelOptions,
};

// Use the core TaskHubGrpcClient with custom credentials and metadata generator
// For insecure connections, metadata is passed via the metadataGenerator parameter
// For secure connections, metadata is included in the channel credentials
return new TaskHubGrpcClient(hostAddress, combinedOptions, true, channelCredentials, metadataGenerator, this._logger);
// Use the core TaskHubGrpcClient with custom channel credentials and per-call metadata.
return new TaskHubGrpcClient(
hostAddress,
combinedOptions,
true,
channelCredentials,
metadataGenerator,
this._logger,
);
}
}

Expand Down
84 changes: 54 additions & 30 deletions packages/durabletask-js-azuremanaged/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,37 +87,54 @@ abstract class DurableTaskAzureManagedOptionsBase {
* @returns The normalized host address for gRPC connection.
*/
getHostAddress(): string {
let endpoint = this._endpointAddress;
const url = this.getEndpointUrl();
if (url.protocol === "http:" && !url.port) {
return `${url.hostname}:80`;
}
return url.host;
}

// Add https:// prefix if no protocol is specified
if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) {
private getEndpointUrl(): URL {
let endpoint = this._endpointAddress.trim();
if (/\s/.test(endpoint)) {
throw new Error("Invalid endpoint URL: expected an HTTP(S) URL or a schemeless authority.");
}

if (!/^https?:\/\/[^/\\]/i.test(endpoint)) {
if (/[/?#\\]/.test(endpoint)) {
throw new Error("Invalid endpoint URL: expected an HTTP(S) URL or a schemeless authority.");
}
endpoint = `https://${endpoint}`;
}

let url: URL;
try {
const url = new URL(endpoint);
let authority = url.hostname;
if (url.port) {
authority = `${authority}:${url.port}`;
}
return authority;
url = new URL(endpoint);
} catch (e) {
throw new Error(`Invalid endpoint URL: ${endpoint}`, { cause: e });
}

if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error(`Unsupported endpoint scheme '${url.protocol}'. Only HTTP and HTTPS are supported.`);
}

if (!url.hostname || url.username || url.password) {
throw new Error("Invalid endpoint URL: the endpoint must contain a hostname without user information.");
}

return url;
}

/**
* Creates a gRPC channel metadata generator.
* @param callerType The type of caller for user-agent header.
* @param workerId Optional worker ID (only for workers).
*/
protected createMetadataGeneratorInternal(
callerType: string,
workerId?: string,
): () => Promise<grpc.Metadata> {
protected createMetadataGeneratorInternal(callerType: string, workerId?: string): () => Promise<grpc.Metadata> {
// Create token cache only if credential is not null
let tokenCache: AccessTokenCache | null = null;
if (this._credential) {
this.validateTokenCredentialTransport(this.getEndpointUrl());
const scope = this._resourceId + "/.default";
tokenCache = new AccessTokenCache(this._credential, scope, this._tokenRefreshMargin);
}
Expand Down Expand Up @@ -146,20 +163,29 @@ abstract class DurableTaskAzureManagedOptionsBase {

/**
* Creates gRPC channel credentials based on the configured options.
* For insecure connections, returns insecure credentials.
* For secure connections, returns SSL credentials.
* HTTPS endpoints use SSL credentials; HTTP endpoints use insecure credentials.
* Token credentials over an insecure endpoint require explicit opt-in.
* Note: Metadata (taskhub, auth token, etc.) is passed per-call via the metadataGenerator
* rather than being composed into the channel credentials. This ensures consistent behavior
* across both secure and insecure connections.
*/
protected createChannelCredentialsInternal(_callerType: string, _workerId?: string): grpc.ChannelCredentials {
if (this._allowInsecureCredentials) {
return grpc.ChannelCredentials.createInsecure();
protected createChannelCredentialsInternal(): grpc.ChannelCredentials {
const endpoint = this.getEndpointUrl();
this.validateTokenCredentialTransport(endpoint);

if (endpoint.protocol.toLowerCase() === "https:") {
return grpc.ChannelCredentials.createSsl();
}

// For secure connections, use SSL credentials
// Metadata is passed per-call via the client/worker's metadataGenerator parameter
return grpc.ChannelCredentials.createSsl();
return grpc.ChannelCredentials.createInsecure();
}

private validateTokenCredentialTransport(endpoint: URL): void {
if (endpoint.protocol === "http:" && this._credential && !this._allowInsecureCredentials) {
throw new Error(
"Token credentials cannot be used with an insecure endpoint unless allowInsecureCredentials(true) is configured.",
);
}
}

/**
Expand All @@ -169,9 +195,7 @@ abstract class DurableTaskAzureManagedOptionsBase {
this._endpointAddress = connectionString.getEndpoint();
this._taskHubName = connectionString.getTaskHubName();

const credential = getCredentialFromAuthenticationType(connectionString);
this._credential = credential;
this._allowInsecureCredentials = credential === null;
this._credential = getCredentialFromAuthenticationType(connectionString);
}
}

Expand Down Expand Up @@ -243,7 +267,7 @@ export class DurableTaskAzureManagedClientOptions extends DurableTaskAzureManage
/**
* Creates a gRPC channel metadata generator for per-call metadata.
* Does NOT include workerid header (client only).
* This is used for insecure connections where metadata can't be added via channel credentials.
* This is used for both secure and insecure connections.
*/
createMetadataGenerator(): () => Promise<grpc.Metadata> {
return this.createMetadataGeneratorInternal("DurableTaskClient");
Expand All @@ -253,10 +277,10 @@ export class DurableTaskAzureManagedClientOptions extends DurableTaskAzureManage
* Creates gRPC channel credentials for the client.
* Does NOT include workerid header.
* For insecure connections, returns just insecure credentials (use createMetadataGenerator for metadata).
* For secure connections, returns SSL credentials composed with call credentials.
* For secure connections, returns SSL credentials.
*/
createChannelCredentials(): grpc.ChannelCredentials {
return this.createChannelCredentialsInternal("DurableTaskClient");
return this.createChannelCredentialsInternal();
}
}

Expand Down Expand Up @@ -354,7 +378,7 @@ export class DurableTaskAzureManagedWorkerOptions extends DurableTaskAzureManage
/**
* Creates a gRPC channel metadata generator for per-call metadata.
* Includes workerid header (worker only).
* This is used for insecure connections where metadata can't be added via channel credentials.
* This is used for both secure and insecure connections.
*/
createMetadataGenerator(): () => Promise<grpc.Metadata> {
return this.createMetadataGeneratorInternal("DurableTaskWorker", this._workerId);
Expand All @@ -364,9 +388,9 @@ export class DurableTaskAzureManagedWorkerOptions extends DurableTaskAzureManage
* Creates gRPC channel credentials for the worker.
* Includes workerid header.
* For insecure connections, returns just insecure credentials (use createMetadataGenerator for metadata).
* For secure connections, returns SSL credentials composed with call credentials.
* For secure connections, returns SSL credentials.
*/
createChannelCredentials(): grpc.ChannelCredentials {
return this.createChannelCredentialsInternal("DurableTaskWorker", this._workerId);
return this.createChannelCredentialsInternal();
}
}
5 changes: 3 additions & 2 deletions packages/durabletask-js-azuremanaged/src/worker-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export class DurableTaskAzureManagedWorkerBuilder {

/**
* Configures the builder using a connection string.
* This replaces the Azure-managed connection options. Worker registrations and builder-level settings are
* preserved. Call connection option setters after this method.
*
* @param connectionString The connection string for Azure-managed Durable Task service.
* @returns This builder instance.
Expand Down Expand Up @@ -81,8 +83,7 @@ export class DurableTaskAzureManagedWorkerBuilder {
this._options
.setEndpointAddress(endpoint)
.setTaskHubName(taskHubName)
.setCredential(credential ?? null)
.setAllowInsecureCredentials(credential === null || credential === undefined);
.setCredential(credential ?? null);

return this;
}
Expand Down
Loading
Loading