@distrohelena/canton-typescript-sdk 0.1.17 → 0.1.19

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 CHANGED
@@ -127,6 +127,49 @@ already be a Ledger API user with the appropriate rights on each participant.
127
127
  Extra participants use the existing shared-secret onboarding flow; the current
128
128
  OAuth2-plus-extras limitation still applies.
129
129
 
130
+ ### Optional localnet TLS
131
+
132
+ Set `LOCALNET_TLS=1` to enable TLS on every participant Ledger API and Admin API
133
+ listener, including generated extra participants. TLS is disabled by default,
134
+ so `LOCALNET_TLS=0` preserves the existing Quickstart behavior. The launcher
135
+ uses direct Compose mode when TLS is enabled so it can apply the generated
136
+ configuration overlay.
137
+
138
+ By default, development-only material is generated in
139
+ `.generated/localnet-tls`: `ca.crt`, `server.crt`, and `server.key`. Set
140
+ `LOCALNET_TLS_ROTATE=1` to replace it. To provide your own material, set all
141
+ three variables together:
142
+
143
+ ```bash
144
+ LOCALNET_TLS=1 \
145
+ LOCALNET_TLS_CERT_CHAIN_PATH=/path/to/server-chain.pem \
146
+ LOCALNET_TLS_PRIVATE_KEY_PATH=/path/to/server-key.pem \
147
+ LOCALNET_TLS_CA_CERT_PATH=/path/to/root-ca.pem \
148
+ canton-localnet-start
149
+ ```
150
+
151
+ The server certificate must cover the hostname used by the client, normally
152
+ `localhost` for host-side SDK calls. Client certificate authentication is not
153
+ enabled; authentication remains controlled by the existing Quickstart
154
+ `AUTH_MODE` and optional ES256 settings.
155
+
156
+ The SDK gRPC channels remain TLS by default. For generated localnet material,
157
+ pass the generated CA to the client:
158
+
159
+ ```ts
160
+ import { readFileSync } from "node:fs";
161
+
162
+ const client = new CantonClient(new CantonClientOptions({
163
+ transportKind: TransportKind.grpc,
164
+ ledgerEndpoint: "localhost:3901",
165
+ ledgerAdminEndpoint: "localhost:3902",
166
+ participantAdminEndpoint: "localhost:3902",
167
+ grpcTlsRootCertificates: readFileSync(
168
+ ".generated/localnet-tls/ca.crt",
169
+ ),
170
+ }));
171
+ ```
172
+
130
173
  The live suite runs single-worker with an extended timeout because it mutates and reads a shared localnet.
131
174
 
132
175
  Prerequisites:
@@ -9,6 +9,7 @@ export declare class CantonClientOptions {
9
9
  readonly ledgerAdminEndpoint?: string;
10
10
  readonly participantAdminEndpoint?: string;
11
11
  readonly grpcChannelSecurity: GrpcChannelSecurity;
12
+ readonly grpcTlsRootCertificates?: Uint8Array;
12
13
  readonly ledgerGrpcChannelSecurity?: GrpcChannelSecurity;
13
14
  readonly ledgerAdminGrpcChannelSecurity?: GrpcChannelSecurity;
14
15
  readonly participantAdminGrpcChannelSecurity?: GrpcChannelSecurity;
@@ -25,6 +26,7 @@ export declare class CantonClientOptions {
25
26
  ledgerAdminEndpoint?: string;
26
27
  participantAdminEndpoint?: string;
27
28
  grpcChannelSecurity?: GrpcChannelSecurity;
29
+ grpcTlsRootCertificates?: Uint8Array;
28
30
  ledgerGrpcChannelSecurity?: GrpcChannelSecurity;
29
31
  ledgerAdminGrpcChannelSecurity?: GrpcChannelSecurity;
30
32
  participantAdminGrpcChannelSecurity?: GrpcChannelSecurity;
@@ -5,6 +5,7 @@ export class CantonClientOptions {
5
5
  ledgerAdminEndpoint;
6
6
  participantAdminEndpoint;
7
7
  grpcChannelSecurity;
8
+ grpcTlsRootCertificates;
8
9
  ledgerGrpcChannelSecurity;
9
10
  ledgerAdminGrpcChannelSecurity;
10
11
  participantAdminGrpcChannelSecurity;
@@ -22,6 +23,7 @@ export class CantonClientOptions {
22
23
  this.participantAdminEndpoint = init.participantAdminEndpoint;
23
24
  this.grpcChannelSecurity =
24
25
  init.grpcChannelSecurity ?? GrpcChannelSecurity.tls;
26
+ this.grpcTlsRootCertificates = init.grpcTlsRootCertificates;
25
27
  this.ledgerGrpcChannelSecurity = init.ledgerGrpcChannelSecurity;
26
28
  this.ledgerAdminGrpcChannelSecurity =
27
29
  init.ledgerAdminGrpcChannelSecurity;
@@ -1,7 +1,7 @@
1
1
  import { IAuthProvider } from "../../core/auth/auth-provider.interface.js";
2
2
  import { RequestOptions } from "../../core/types/request-options.js";
3
3
  import { GrpcChannelSecurity } from "../../core/types/grpc-channel-security.js";
4
- export declare function createGrpcChannelCredentials(security: GrpcChannelSecurity): import("@grpc/grpc-js").ChannelCredentials;
4
+ export declare function createGrpcChannelCredentials(security: GrpcChannelSecurity, tlsRootCertificates?: Uint8Array): import("@grpc/grpc-js").ChannelCredentials;
5
5
  export declare function buildGrpcCallOptionsAsync(authProvider?: IAuthProvider, defaultTimeoutMs?: number, options?: RequestOptions): Promise<{
6
6
  meta: Record<string, string>;
7
7
  timeout?: number;
@@ -1,10 +1,14 @@
1
1
  import { credentials } from "@grpc/grpc-js";
2
+ import { Buffer } from "node:buffer";
2
3
  import { resolveRequestTimeoutMs } from "../../core/types/request-timeout.js";
3
4
  import { GrpcChannelSecurity } from "../../core/types/grpc-channel-security.js";
4
- export function createGrpcChannelCredentials(security) {
5
- return security === GrpcChannelSecurity.insecure
6
- ? credentials.createInsecure()
7
- : credentials.createSsl();
5
+ export function createGrpcChannelCredentials(security, tlsRootCertificates) {
6
+ if (security === GrpcChannelSecurity.insecure) {
7
+ return credentials.createInsecure();
8
+ }
9
+ return tlsRootCertificates === undefined
10
+ ? credentials.createSsl()
11
+ : credentials.createSsl(Buffer.from(tlsRootCertificates));
8
12
  }
9
13
  export async function buildGrpcCallOptionsAsync(authProvider, defaultTimeoutMs, options) {
10
14
  const timeout = resolveRequestTimeoutMs(defaultTimeoutMs, options);
@@ -32,7 +32,7 @@ import { buildGrpcCallOptionsAsync, createGrpcChannelCredentials, } from "./grpc
32
32
  export function createGrpcOperations(options, endpoint, grpcChannelSecurity, dependencies = {}) {
33
33
  const rpcTransport = new ProtobufGrpcTransport({
34
34
  host: normalizeGrpcHost(endpoint),
35
- channelCredentials: createGrpcChannelCredentials(grpcChannelSecurity),
35
+ channelCredentials: createGrpcChannelCredentials(grpcChannelSecurity, options.grpcTlsRootCertificates),
36
36
  clientOptions: options.grpcConnectTimeoutMs === undefined
37
37
  ? undefined
38
38
  : {
@@ -98,6 +98,158 @@ es256_runtime_dir() {
98
98
  printf '%s\n' "${START_LOCAL_ES256_RUNTIME_DIR:-$REPO_ROOT/.generated/localnet-es256}"
99
99
  }
100
100
 
101
+ resolve_tls_enabled() {
102
+ local value="${LOCALNET_TLS:-0}"
103
+ if [[ "$value" != "0" && "$value" != "1" ]]; then
104
+ echo "LOCALNET_TLS must be 0 or 1." >&2
105
+ return 1
106
+ fi
107
+ printf '%s\n' "$value"
108
+ }
109
+
110
+ resolve_tls_rotate() {
111
+ local value="${LOCALNET_TLS_ROTATE:-0}"
112
+ if [[ "$value" != "0" && "$value" != "1" ]]; then
113
+ echo "LOCALNET_TLS_ROTATE must be 0 or 1." >&2
114
+ return 1
115
+ fi
116
+ printf '%s\n' "$value"
117
+ }
118
+
119
+ tls_runtime_dir() {
120
+ printf '%s\n' "${START_LOCAL_TLS_RUNTIME_DIR:-$REPO_ROOT/.generated/localnet-tls}"
121
+ }
122
+
123
+ prepare_tls_runtime_files() {
124
+ local enabled
125
+ enabled="$(resolve_tls_enabled)"
126
+ if [[ "$enabled" != "1" ]]; then
127
+ return 0
128
+ fi
129
+
130
+ local runtime_dir
131
+ runtime_dir="$(tls_runtime_dir)"
132
+ local ca_key_path="$runtime_dir/ca.key"
133
+ local ca_certificate_path="$runtime_dir/ca.crt"
134
+ local server_key_path="$runtime_dir/server.key"
135
+ local server_certificate_path="$runtime_dir/server.crt"
136
+ local compose_file="$runtime_dir/compose-localnet.yaml"
137
+ local tls_fragment_file="$runtime_dir/canton-tls.conf"
138
+ local canton_config_file="$runtime_dir/canton-localnet.conf"
139
+ local supplied_count=0
140
+
141
+ [[ -n "${LOCALNET_TLS_CERT_CHAIN_PATH:-}" ]] && ((supplied_count += 1))
142
+ [[ -n "${LOCALNET_TLS_PRIVATE_KEY_PATH:-}" ]] && ((supplied_count += 1))
143
+ [[ -n "${LOCALNET_TLS_CA_CERT_PATH:-}" ]] && ((supplied_count += 1))
144
+
145
+ mkdir -p "$runtime_dir"
146
+ if (( supplied_count > 0 )); then
147
+ if (( supplied_count != 3 )); then
148
+ echo "LOCALNET_TLS_CERT_CHAIN_PATH, LOCALNET_TLS_PRIVATE_KEY_PATH, and LOCALNET_TLS_CA_CERT_PATH must be set together." >&2
149
+ return 1
150
+ fi
151
+ server_certificate_path="$LOCALNET_TLS_CERT_CHAIN_PATH"
152
+ server_key_path="$LOCALNET_TLS_PRIVATE_KEY_PATH"
153
+ ca_certificate_path="$LOCALNET_TLS_CA_CERT_PATH"
154
+ if [[ ! -r "$server_certificate_path" || ! -r "$server_key_path" || ! -r "$ca_certificate_path" ]]; then
155
+ echo "Supplied TLS certificate, private key, or CA certificate is not readable." >&2
156
+ return 1
157
+ fi
158
+ if ! openssl x509 -in "$server_certificate_path" -noout >/dev/null 2>&1 \
159
+ || ! openssl pkey -in "$server_key_path" -noout >/dev/null 2>&1 \
160
+ || ! openssl x509 -in "$ca_certificate_path" -noout >/dev/null 2>&1 \
161
+ || ! openssl verify -CAfile "$ca_certificate_path" "$server_certificate_path" >/dev/null 2>&1; then
162
+ echo "Supplied TLS material is not valid PEM certificate/key material." >&2
163
+ return 1
164
+ fi
165
+ local certificate_public_key private_public_key
166
+ if ! certificate_public_key="$(openssl x509 -in "$server_certificate_path" -pubkey -noout | openssl pkey -pubin -outform DER | sha256sum | awk '{print $1}')" \
167
+ || ! private_public_key="$(openssl pkey -in "$server_key_path" -pubout | openssl pkey -pubin -outform DER | sha256sum | awk '{print $1}')" \
168
+ || [[ "$certificate_public_key" != "$private_public_key" ]]; then
169
+ echo "Supplied TLS private key does not match the server certificate." >&2
170
+ return 1
171
+ fi
172
+ else
173
+ if [[ "$(resolve_tls_rotate)" == "1" ]]; then
174
+ rm -f "$ca_key_path" "$ca_certificate_path" "$server_key_path" "$server_certificate_path"
175
+ fi
176
+ if [[ ! -e "$ca_key_path" || ! -e "$ca_certificate_path" ]]; then
177
+ rm -f "$ca_key_path" "$ca_certificate_path"
178
+ openssl genrsa -out "$ca_key_path" 2048 >/dev/null 2>&1
179
+ chmod 600 "$ca_key_path"
180
+ openssl req -x509 -new -sha256 -days 3650 -key "$ca_key_path" \
181
+ -subj "/CN=localnet-tls-ca" -out "$ca_certificate_path" >/dev/null 2>&1
182
+ fi
183
+ if [[ ! -e "$server_key_path" || ! -e "$server_certificate_path" ]]; then
184
+ rm -f "$server_key_path" "$server_certificate_path"
185
+ local csr_path="$runtime_dir/server.csr"
186
+ local extensions_path="$runtime_dir/server-extensions.cnf"
187
+ openssl genrsa -out "$server_key_path" 2048 >/dev/null 2>&1
188
+ chmod 600 "$server_key_path"
189
+ openssl req -new -sha256 -key "$server_key_path" -subj "/CN=localhost" -out "$csr_path" >/dev/null 2>&1
190
+ cat > "$extensions_path" <<'EOF'
191
+ [server_ext]
192
+ basicConstraints = CA:FALSE
193
+ keyUsage = digitalSignature, keyEncipherment
194
+ extendedKeyUsage = serverAuth
195
+ subjectAltName = DNS:localhost, DNS:canton, IP:127.0.0.1
196
+ EOF
197
+ openssl x509 -req -sha256 -days 3650 -in "$csr_path" \
198
+ -CA "$ca_certificate_path" -CAkey "$ca_key_path" -CAcreateserial \
199
+ -out "$server_certificate_path" -extfile "$extensions_path" -extensions server_ext >/dev/null 2>&1
200
+ rm -f "$csr_path" "$extensions_path" "$ca_certificate_path.srl"
201
+ fi
202
+ if ! openssl x509 -in "$server_certificate_path" -noout >/dev/null 2>&1 \
203
+ || ! openssl pkey -in "$server_key_path" -noout >/dev/null 2>&1 \
204
+ || ! openssl x509 -in "$ca_certificate_path" -noout >/dev/null 2>&1; then
205
+ echo "Generated TLS material is invalid." >&2
206
+ return 1
207
+ fi
208
+ fi
209
+
210
+ : > "$tls_fragment_file"
211
+ local participant profile
212
+ for participant in app-provider app-user sv; do
213
+ case "$participant" in
214
+ app-provider) profile="$APP_PROVIDER_PROFILE" ;;
215
+ app-user) profile="$APP_USER_PROFILE" ;;
216
+ sv) profile="$SV_PROFILE" ;;
217
+ esac
218
+ if [[ "$profile" != "off" ]]; then
219
+ cat >> "$tls_fragment_file" <<EOF
220
+ canton.participants.${participant}.ledger-api.tls {
221
+ cert-chain-file = "/app/localnet-tls/server.crt"
222
+ private-key-file = "/app/localnet-tls/server.key"
223
+ trust-collection-file = "/app/localnet-tls/ca.crt"
224
+ }
225
+ canton.participants.${participant}.admin-api.tls {
226
+ cert-chain-file = "/app/localnet-tls/server.crt"
227
+ private-key-file = "/app/localnet-tls/server.key"
228
+ trust-collection-file = "/app/localnet-tls/ca.crt"
229
+ }
230
+ EOF
231
+ fi
232
+ done
233
+ cat > "$canton_config_file" <<'EOF'
234
+ include file("/app/base-app.conf")
235
+ include file("/app/localnet-tls.conf")
236
+ EOF
237
+ cat > "$compose_file" <<EOF
238
+ services:
239
+ canton:
240
+ volumes:
241
+ - "${LOCALNET_DIR}/conf/canton/app.conf:/app/base-app.conf:ro"
242
+ - "$canton_config_file:/app/app.conf:ro"
243
+ - "$tls_fragment_file:/app/localnet-tls.conf:ro"
244
+ - "$server_certificate_path:/app/localnet-tls/server.crt:ro"
245
+ - "$server_key_path:/app/localnet-tls/server.key:ro"
246
+ - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
247
+ EOF
248
+ export LOCALNET_TLS_COMPOSE_FILE="$compose_file"
249
+ export LOCALNET_TLS_FRAGMENT_FILE="$tls_fragment_file"
250
+ export LOCALNET_TLS_CA_CERT_PATH="$ca_certificate_path"
251
+ }
252
+
101
253
  prepare_es256_runtime_files() {
102
254
  local runtime_dir
103
255
  runtime_dir="$(es256_runtime_dir)"
@@ -150,6 +302,8 @@ prepare_es256_runtime_files() {
150
302
  cat > "$canton_config_file" <<EOF
151
303
  include file("/app/base-app.conf")
152
304
 
305
+ $(if [[ "$(resolve_tls_enabled)" == "1" ]]; then printf 'include file("/app/localnet-tls.conf")\n'; fi)
306
+
153
307
  EOF
154
308
  local participant profile
155
309
  for participant in app-provider app-user sv; do
@@ -195,6 +349,13 @@ append_es256_args() {
195
349
  fi
196
350
  }
197
351
 
352
+ append_tls_args() {
353
+ local -n compose_args_ref="$1"
354
+ if [[ "$(resolve_tls_enabled)" == "1" ]]; then
355
+ compose_args_ref+=( -f "$LOCALNET_TLS_COMPOSE_FILE" )
356
+ fi
357
+ }
358
+
198
359
  resolve_docker_compose_cmd() {
199
360
  if (( ${#DOCKER_COMPOSE_CMD[@]} > 0 )); then
200
361
  return 0
@@ -414,6 +575,25 @@ canton.participants.extra-${index}.ledger-api.auth-services = [
414
575
  }
415
576
  ]
416
577
 
578
+ EOF
579
+ done
580
+ fi
581
+
582
+ if [[ "$(resolve_tls_enabled)" == "1" ]]; then
583
+ local index
584
+ for ((index = 1; index <= count; index++)); do
585
+ cat >> "$canton_config_file" <<EOF
586
+ canton.participants.extra-${index}.ledger-api.tls {
587
+ cert-chain-file = "/app/localnet-tls/server.crt"
588
+ private-key-file = "/app/localnet-tls/server.key"
589
+ trust-collection-file = "/app/localnet-tls/ca.crt"
590
+ }
591
+ canton.participants.extra-${index}.admin-api.tls {
592
+ cert-chain-file = "/app/localnet-tls/server.crt"
593
+ private-key-file = "/app/localnet-tls/server.key"
594
+ trust-collection-file = "/app/localnet-tls/ca.crt"
595
+ }
596
+
417
597
  EOF
418
598
  done
419
599
  fi
@@ -795,6 +975,7 @@ grant_localnet_validator_read_rights() {
795
975
 
796
976
  start_ledger_stack() {
797
977
  local auth_mode="${1:-shared-secret}"
978
+ export AUTH_MODE="$auth_mode"
798
979
  local modules_dir="$QUICKSTART_DIR/docker/modules"
799
980
  local localnet_dir="$modules_dir/localnet"
800
981
  local splice_version
@@ -808,6 +989,7 @@ start_ledger_stack() {
808
989
  export SV_PROFILE=on
809
990
  export PQS_SV_PROFILE=on
810
991
  load_localnet_common_env "$localnet_dir/env/common.env"
992
+ prepare_tls_runtime_files
811
993
  prepare_es256_runtime_files
812
994
  local extra_participants
813
995
  extra_participants="$(resolve_extra_participants)"
@@ -841,6 +1023,7 @@ start_ledger_stack() {
841
1023
  fi
842
1024
 
843
1025
  append_extra_participant_args "$extra_participants" "$auth_mode" compose_args
1026
+ append_tls_args compose_args
844
1027
  append_es256_args compose_args
845
1028
  docker_compose "${compose_args[@]}" down -v --remove-orphans
846
1029
  mapfile -t startup_services < <(prerequisite_services "$auth_mode")
@@ -873,12 +1056,16 @@ if [[ ! -f .env.local ]]; then
873
1056
  make setup
874
1057
  fi
875
1058
 
876
- if [[ "$(resolve_es256_enabled)" != "1" ]] && make_target_exists start-local-ledger; then
1059
+ if [[ "$(resolve_es256_enabled)" != "1" && "$(resolve_tls_enabled)" != "1" ]] && make_target_exists start-local-ledger; then
877
1060
  make start-local-ledger
878
1061
  else
879
1062
  auth_mode="$(read_env_value AUTH_MODE || true)"
880
1063
  auth_mode="${auth_mode:-shared-secret}"
881
- echo "start-local-ledger target not found. Starting ledger-only compose stack directly."
1064
+ if [[ "$(resolve_es256_enabled)" == "1" || "$(resolve_tls_enabled)" == "1" ]]; then
1065
+ echo "Generated localnet transport overlay enabled. Starting ledger-only compose stack directly."
1066
+ else
1067
+ echo "start-local-ledger target not found. Starting ledger-only compose stack directly."
1068
+ fi
882
1069
  start_ledger_stack "$auth_mode"
883
1070
  if [[ "$(resolve_es256_enabled)" == "1" ]]; then
884
1071
  echo "ES256 bearer token written to $LOCALNET_ES256_TOKEN_PATH"
@@ -92,6 +92,19 @@ es256_runtime_dir() {
92
92
  printf '%s\n' "${START_LOCAL_ES256_RUNTIME_DIR:-$REPO_ROOT/.generated/localnet-es256}"
93
93
  }
94
94
 
95
+ resolve_tls_enabled() {
96
+ local value="${LOCALNET_TLS:-0}"
97
+ if [[ "$value" != "0" && "$value" != "1" ]]; then
98
+ echo "LOCALNET_TLS must be 0 or 1." >&2
99
+ return 1
100
+ fi
101
+ printf '%s\n' "$value"
102
+ }
103
+
104
+ tls_runtime_dir() {
105
+ printf '%s\n' "${START_LOCAL_TLS_RUNTIME_DIR:-$REPO_ROOT/.generated/localnet-tls}"
106
+ }
107
+
95
108
  append_es256_args() {
96
109
  local -n compose_args_ref="$1"
97
110
  if [[ "$(resolve_es256_enabled)" == "1" ]]; then
@@ -102,6 +115,16 @@ append_es256_args() {
102
115
  fi
103
116
  }
104
117
 
118
+ append_tls_args() {
119
+ local -n compose_args_ref="$1"
120
+ if [[ "$(resolve_tls_enabled)" == "1" ]]; then
121
+ local compose_file="$(tls_runtime_dir)/compose-localnet.yaml"
122
+ if [[ -f "$compose_file" ]]; then
123
+ compose_args_ref+=( -f "$compose_file" )
124
+ fi
125
+ fi
126
+ }
127
+
105
128
  resolve_docker_compose_cmd() {
106
129
  if (( ${#DOCKER_COMPOSE_CMD[@]} > 0 )); then
107
130
  return 0
@@ -141,6 +164,7 @@ append_existing_extra_participant_args() {
141
164
 
142
165
  stop_ledger_stack() {
143
166
  local auth_mode="${1:-shared-secret}"
167
+ export AUTH_MODE="$auth_mode"
144
168
  local modules_dir="$QUICKSTART_DIR/docker/modules"
145
169
  local localnet_dir="$modules_dir/localnet"
146
170
  local splice_version
@@ -177,6 +201,7 @@ stop_ledger_stack() {
177
201
  fi
178
202
 
179
203
  append_existing_extra_participant_args compose_args
204
+ append_tls_args compose_args
180
205
  append_es256_args compose_args
181
206
  docker_compose "${compose_args[@]}" down -v --remove-orphans
182
207
  }
@@ -190,7 +215,7 @@ if [[ ! -f .env.local ]]; then
190
215
  exit 0
191
216
  fi
192
217
 
193
- if [[ "$(resolve_es256_enabled)" != "1" ]] && make_target_exists stop-local-ledger; then
218
+ if [[ "$(resolve_es256_enabled)" != "1" && "$(resolve_tls_enabled)" != "1" ]] && make_target_exists stop-local-ledger; then
194
219
  make stop-local-ledger
195
220
  else
196
221
  auth_mode="$(read_env_value AUTH_MODE || true)"
@@ -32,12 +32,17 @@ run_case() {
32
32
  local cn_quickstart_dir_override="${6:-}"
33
33
  local repo_root_env_contents="${7:-}"
34
34
  local es256_enabled="${8:-0}"
35
+ local tls_enabled="${9:-0}"
36
+ local tls_cert_chain_path="${10:-}"
37
+ local tls_private_key_path="${11:-}"
38
+ local tls_ca_cert_path="${12:-}"
39
+ local expected_status="${13:-0}"
35
40
  local quickstart_dir="$tmpdir/quickstart"
36
41
  local quickstart_root_dir="$tmpdir/cn-quickstart"
37
42
  local stubbin="$tmpdir/bin"
38
43
  local generated_dir="$tmpdir/generated"
39
44
 
40
- rm -rf "$quickstart_dir" "$quickstart_root_dir" "$stubbin" "$generated_dir"
45
+ rm -rf "$quickstart_dir" "$quickstart_root_dir" "$stubbin" "$generated_dir" "$tmpdir/tls"
41
46
  mkdir -p \
42
47
  "$quickstart_root_dir" \
43
48
  "$quickstart_dir/docker/modules/localnet/env" \
@@ -84,6 +89,7 @@ printf 'stub env APP_PROVIDER_PROFILE=%s\n' "${APP_PROVIDER_PROFILE:-}"
84
89
  printf 'stub env APP_USER_PROFILE=%s\n' "${APP_USER_PROFILE:-}"
85
90
  printf 'stub env SV_PROFILE=%s\n' "${SV_PROFILE:-}"
86
91
  printf 'stub env PQS_SV_PROFILE=%s\n' "${PQS_SV_PROFILE:-}"
92
+ printf 'stub env AUTH_MODE=%s\n' "${AUTH_MODE:-}"
87
93
  if [[ "${1:-}" == "inspect" ]]; then
88
94
  printf 'healthy\n'
89
95
  exit 0
@@ -145,6 +151,7 @@ EOF
145
151
  chmod +x "$stubbin/docker-compose"
146
152
 
147
153
  local output
154
+ local status=0
148
155
  output="$(
149
156
  if [[ -n "$repo_root_env_contents" ]]; then
150
157
  printf '%s\n' "$repo_root_env_contents" > "$REPO_ENV_FILE"
@@ -158,20 +165,39 @@ EOF
158
165
  TERM="${TERM:-dumb}" \
159
166
  START_LOCAL_GENERATED_DIR="$generated_dir" \
160
167
  START_LOCAL_ES256_RUNTIME_DIR="$tmpdir/es256" \
168
+ START_LOCAL_TLS_RUNTIME_DIR="$tmpdir/tls" \
161
169
  LOCALNET_ES256_JWT="$es256_enabled" \
170
+ LOCALNET_TLS="$tls_enabled" \
171
+ LOCALNET_TLS_CERT_CHAIN_PATH="$tls_cert_chain_path" \
172
+ LOCALNET_TLS_PRIVATE_KEY_PATH="$tls_private_key_path" \
173
+ LOCALNET_TLS_CA_CERT_PATH="$tls_ca_cert_path" \
162
174
  EXTRA_PARTICIPANTS="$extra_participants" \
163
175
  PATH="$stubbin:$PATH" \
164
- bash ./start-local.sh
176
+ bash ./start-local.sh 2>&1
165
177
  else
166
178
  CN_QUICKSTART_DIR="${cn_quickstart_dir_override:-$quickstart_dir}" \
167
179
  START_LOCAL_GENERATED_DIR="$generated_dir" \
168
180
  START_LOCAL_ES256_RUNTIME_DIR="$tmpdir/es256" \
181
+ START_LOCAL_TLS_RUNTIME_DIR="$tmpdir/tls" \
169
182
  LOCALNET_ES256_JWT="$es256_enabled" \
183
+ LOCALNET_TLS="$tls_enabled" \
184
+ LOCALNET_TLS_CERT_CHAIN_PATH="$tls_cert_chain_path" \
185
+ LOCALNET_TLS_PRIVATE_KEY_PATH="$tls_private_key_path" \
186
+ LOCALNET_TLS_CA_CERT_PATH="$tls_ca_cert_path" \
170
187
  EXTRA_PARTICIPANTS="$extra_participants" \
171
188
  PATH="$stubbin:$PATH" \
172
- bash ./start-local.sh
189
+ bash ./start-local.sh 2>&1
173
190
  fi
174
- )"
191
+ )" || status=$?
192
+
193
+ if [[ "$expected_status" == "0" && "$status" != "0" ]]; then
194
+ printf 'expected successful start-local case, got status %s and output:\n%s\n' "$status" "$output" >&2
195
+ exit 1
196
+ fi
197
+ if [[ "$expected_status" != "0" && "$status" == "0" ]]; then
198
+ printf 'expected failing start-local case, got success and output:\n%s\n' "$output" >&2
199
+ exit 1
200
+ fi
175
201
 
176
202
  if ! grep -Fxq "$expected_output" <<<"$output"; then
177
203
  printf 'expected output line %q, got:\n%s\n' "$expected_output" "$output" >&2
@@ -190,6 +216,28 @@ assert_file_contains() {
190
216
  fi
191
217
  }
192
218
 
219
+ assert_file_contains_text() {
220
+ local path="$1"
221
+ local expected="$2"
222
+
223
+ if ! grep -Fq "$expected" "$path"; then
224
+ printf 'expected file %s to contain text %q, got:\n' "$path" "$expected" >&2
225
+ cat "$path" >&2
226
+ exit 1
227
+ fi
228
+ }
229
+
230
+ assert_file_mode() {
231
+ local path="$1"
232
+ local expected="$2"
233
+ local actual
234
+ actual="$(stat -c '%a' "$path")"
235
+ if [[ "$actual" != "$expected" ]]; then
236
+ printf 'expected file %s mode %s, got %s\n' "$path" "$expected" "$actual" >&2
237
+ exit 1
238
+ fi
239
+ }
240
+
193
241
  run_repo_root_env_case() {
194
242
  local quickstart_dir="$tmpdir/quickstart"
195
243
  local quickstart_root_dir="$tmpdir/custom-quickstart-root"
@@ -247,6 +295,7 @@ printf 'stub env APP_PROVIDER_PROFILE=%s\n' "${APP_PROVIDER_PROFILE:-}"
247
295
  printf 'stub env APP_USER_PROFILE=%s\n' "${APP_USER_PROFILE:-}"
248
296
  printf 'stub env SV_PROFILE=%s\n' "${SV_PROFILE:-}"
249
297
  printf 'stub env PQS_SV_PROFILE=%s\n' "${PQS_SV_PROFILE:-}"
298
+ printf 'stub env AUTH_MODE=%s\n' "${AUTH_MODE:-}"
250
299
  if [[ "${1:-}" == "inspect" ]]; then
251
300
  printf 'healthy\n'
252
301
  exit 0
@@ -284,6 +333,7 @@ run_case $'.PHONY: start\nstart:\n' 'stub env IMAGE_TAG=0.6.5'
284
333
  run_case $'.PHONY: start\nstart:\n' 'stub env APP_USER_PROFILE=off'
285
334
  run_case $'.PHONY: start\nstart:\n' 'stub env SV_PROFILE=on'
286
335
  run_case $'.PHONY: start\nstart:\n' 'stub env PQS_SV_PROFILE=on'
336
+ run_case $'.PHONY: start\nstart:\n' 'stub env AUTH_MODE=oauth2' oauth2
287
337
  run_case $'.PHONY: start\nstart:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv down -v --remove-orphans'
288
338
  run_case $'.PHONY: start\nstart:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv up -d --no-recreate postgres canton'
289
339
  run_case $'.PHONY: start\nstart:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv -f '"$tmpdir"'/quickstart/docker/modules/keycloak/compose.yaml --env-file '"$tmpdir"'/quickstart/docker/modules/keycloak/compose.env --profile keycloak down -v --remove-orphans' oauth2
@@ -351,4 +401,58 @@ assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf"
351
401
  assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf" ' validator-party-hint = "${EXTRA_VALIDATOR_1_PARTY_HINT}"'
352
402
  assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf" ' domains.global.buy-extra-traffic {'
353
403
  assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf" 'canton.sv-apps.sv.expected-validator-onboardings += { secret = "${EXTRA_VALIDATOR_3_ONBOARDING_SECRET}" }'
404
+ run_case $'.PHONY: start-local-ledger\nstart-local-ledger:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv -f '"$tmpdir"'/tls/compose-localnet.yaml down -v --remove-orphans' shared-secret default 0 '' '' 0 1
405
+ assert_file_contains_text "$tmpdir/tls/canton-tls.conf" 'canton.participants.app-provider.ledger-api.tls {'
406
+ assert_file_contains_text "$tmpdir/tls/canton-tls.conf" 'canton.participants.app-provider.admin-api.tls {'
407
+ assert_file_contains_text "$tmpdir/tls/canton-tls.conf" 'canton.participants.sv.admin-api.tls {'
408
+ if grep -Fq 'canton.participants.app-user.ledger-api.tls {' "$tmpdir/tls/canton-tls.conf" \
409
+ || grep -Fq 'canton.participants.app-user.admin-api.tls {' "$tmpdir/tls/canton-tls.conf"; then
410
+ echo "unexpected TLS configuration for inactive app-user participant" >&2
411
+ exit 1
412
+ fi
413
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" '/app/localnet-tls/server.key:ro'
414
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" '/app/localnet-tls/ca.crt:ro'
415
+ assert_file_mode "$tmpdir/tls/server.key" 600
416
+ assert_file_mode "$tmpdir/tls/ca.key" 600
417
+ run_case $'.PHONY: start\nstart:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv -f '"$tmpdir"'/generated/compose-extra-participants.yaml --env-file '"$tmpdir"'/generated/extra-participants.env -f '"$tmpdir"'/tls/compose-localnet.yaml down -v --remove-orphans' shared-secret default 2 '' '' 0 1
418
+ assert_file_contains_text "$tmpdir/generated/additional-config.extra-participants.conf" 'canton.participants.extra-1.ledger-api.tls {'
419
+ assert_file_contains_text "$tmpdir/generated/additional-config.extra-participants.conf" 'canton.participants.extra-2.admin-api.tls {'
420
+
421
+ mkdir -p "$tmpdir/supplied"
422
+ openssl genrsa -out "$tmpdir/supplied/ca.key" 2048 >/dev/null 2>&1
423
+ openssl req -x509 -new -sha256 -days 3650 \
424
+ -key "$tmpdir/supplied/ca.key" \
425
+ -subj '/CN=supplied-localnet-ca' \
426
+ -addext 'basicConstraints=critical,CA:TRUE' \
427
+ -addext 'keyUsage=critical,keyCertSign,cRLSign' \
428
+ -out "$tmpdir/supplied/ca.crt" >/dev/null 2>&1
429
+ openssl genrsa -out "$tmpdir/supplied/server.key" 2048 >/dev/null 2>&1
430
+ openssl req -new -sha256 -key "$tmpdir/supplied/server.key" \
431
+ -subj '/CN=localhost' -out "$tmpdir/supplied/server.csr" >/dev/null 2>&1
432
+ printf '%s\n' \
433
+ '[server_ext]' \
434
+ 'basicConstraints = CA:FALSE' \
435
+ 'keyUsage = digitalSignature, keyEncipherment' \
436
+ 'extendedKeyUsage = serverAuth' \
437
+ 'subjectAltName = DNS:localhost, DNS:canton, IP:127.0.0.1' \
438
+ > "$tmpdir/supplied/server-extensions.cnf"
439
+ openssl x509 -req -sha256 -days 3650 \
440
+ -in "$tmpdir/supplied/server.csr" \
441
+ -CA "$tmpdir/supplied/ca.crt" -CAkey "$tmpdir/supplied/ca.key" -CAcreateserial \
442
+ -out "$tmpdir/supplied/server.crt" \
443
+ -extfile "$tmpdir/supplied/server-extensions.cnf" -extensions server_ext >/dev/null 2>&1
444
+ run_case $'.PHONY: start\nstart:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv -f '"$tmpdir"'/tls/compose-localnet.yaml down -v --remove-orphans' shared-secret default 0 '' '' 0 1 "$tmpdir/supplied/server.crt" "$tmpdir/supplied/server.key" "$tmpdir/supplied/ca.crt"
445
+ if [[ -e "$tmpdir/tls/server.key" || -e "$tmpdir/tls/ca.key" ]]; then
446
+ echo 'supplied TLS mode unexpectedly generated private material' >&2
447
+ exit 1
448
+ fi
449
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" "$tmpdir/supplied/server.key:/app/localnet-tls/server.key:ro"
450
+ openssl genrsa -out "$tmpdir/supplied/mismatch.key" 2048 >/dev/null 2>&1
451
+ run_case $'.PHONY: start\nstart:\n' 'Supplied TLS private key does not match the server certificate.' shared-secret default 0 '' '' 0 1 "$tmpdir/supplied/server.crt" "$tmpdir/supplied/mismatch.key" "$tmpdir/supplied/ca.crt" 1
452
+ run_case $'.PHONY: start\nstart:\n' 'LOCALNET_TLS_CERT_CHAIN_PATH, LOCALNET_TLS_PRIVATE_KEY_PATH, and LOCALNET_TLS_CA_CERT_PATH must be set together.' shared-secret default 0 '' '' 0 1 "$tmpdir/supplied/server.crt" '' '' 1
453
+ run_case $'.PHONY: start\nstart:\n' 'LOCALNET_TLS must be 0 or 1.' shared-secret default 0 '' '' 0 invalid '' '' '' 1
454
+ run_case $'.PHONY: start\nstart:\n' 'ES256 bearer token written to '"$tmpdir"'/es256/ledger-api-user.token' shared-secret default 0 '' '' 1 1
455
+ assert_file_contains_text "$tmpdir/es256/canton-es256.conf" 'include file("/app/localnet-tls.conf")'
456
+ assert_file_contains_text "$tmpdir/es256/compose-es256.yaml" '/app/es256-certificate.pem:ro'
457
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" '/app/localnet-tls/server.crt:ro'
354
458
  run_repo_root_env_case
@@ -14,6 +14,7 @@ run_case() {
14
14
  local expected_output="$2"
15
15
  local auth_mode="${3:-shared-secret}"
16
16
  local docker_mode="${4:-compose-v2}"
17
+ local tls_enabled="${5:-0}"
17
18
  local quickstart_dir="$tmpdir/quickstart"
18
19
  local stubbin="$tmpdir/bin"
19
20
  local generated_dir="$tmpdir/generated-stop"
@@ -26,6 +27,10 @@ run_case() {
26
27
  "$quickstart_dir/docker/modules/keycloak" \
27
28
  "$stubbin" \
28
29
  "$generated_dir"
30
+ if [[ "$tls_enabled" == "1" ]]; then
31
+ mkdir -p "$tmpdir/tls"
32
+ printf 'services:\n' > "$tmpdir/tls/compose-localnet.yaml"
33
+ fi
29
34
 
30
35
  printf '%s\n' "$makefile_targets" > "$quickstart_dir/Makefile"
31
36
  printf 'DOCKER_NETWORK=quickstart\nSPLICE_VERSION=0.6.5\n' > "$quickstart_dir/.env"
@@ -64,6 +69,7 @@ printf 'stub env APP_PROVIDER_PROFILE=%s\n' "${APP_PROVIDER_PROFILE:-}"
64
69
  printf 'stub env APP_USER_PROFILE=%s\n' "${APP_USER_PROFILE:-}"
65
70
  printf 'stub env SV_PROFILE=%s\n' "${SV_PROFILE:-}"
66
71
  printf 'stub env PQS_SV_PROFILE=%s\n' "${PQS_SV_PROFILE:-}"
72
+ printf 'stub env AUTH_MODE=%s\n' "${AUTH_MODE:-}"
67
73
  if [[ "${1:-}" == "compose" && "${2:-}" == "version" ]]; then
68
74
  if [[ "$docker_mode" == "compose-v2" ]]; then
69
75
  exit 0
@@ -95,6 +101,8 @@ EOF
95
101
  cd "$SCRIPT_DIR"
96
102
  CN_QUICKSTART_DIR="$quickstart_dir" \
97
103
  START_LOCAL_GENERATED_DIR="$generated_dir" \
104
+ START_LOCAL_TLS_RUNTIME_DIR="$tmpdir/tls" \
105
+ LOCALNET_TLS="$tls_enabled" \
98
106
  PATH="$stubbin:$PATH" \
99
107
  bash ./stop-local.sh
100
108
  )"
@@ -108,5 +116,6 @@ EOF
108
116
  run_case $'.PHONY: stop-local-ledger\nstop-local-ledger:\n' 'stub make stop-local-ledger'
109
117
  run_case $'.PHONY: stop\nstop:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv down -v --remove-orphans'
110
118
  run_case $'.PHONY: stop\nstop:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv -f '"$tmpdir"'/quickstart/docker/modules/keycloak/compose.yaml --env-file '"$tmpdir"'/quickstart/docker/modules/keycloak/compose.env --profile keycloak down -v --remove-orphans' oauth2
119
+ run_case $'.PHONY: stop\nstop:\n' 'stub env AUTH_MODE=oauth2' oauth2
111
120
  run_case $'.PHONY: stop\nstop:\n' 'stub docker-compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv down -v --remove-orphans' shared-secret compose-v1
112
-
121
+ run_case $'.PHONY: stop-local-ledger\nstop-local-ledger:\n' 'stub docker compose -f compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/localnet/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/splice-onboarding/compose.yaml -f '"$tmpdir"'/quickstart/docker/modules/pqs/compose.yaml --env-file .env --env-file .env.local --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/compose.env --env-file '"$tmpdir"'/quickstart/docker/modules/localnet/env/common.env --env-file '"$tmpdir"'/quickstart/docker/modules/pqs/compose.env --profile app-provider --profile pqs-app-provider --profile pqs-sv -f '"$tmpdir"'/tls/compose-localnet.yaml down -v --remove-orphans' shared-secret compose-v2 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distrohelena/canton-typescript-sdk",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",