@distrohelena/canton-typescript-sdk 0.1.17 → 0.1.18

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,167 @@ 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
+ cat > "$tls_fragment_file" <<'EOF'
211
+ canton.participants.app-provider.ledger-api.tls {
212
+ cert-chain-file = "/app/localnet-tls/server.crt"
213
+ private-key-file = "/app/localnet-tls/server.key"
214
+ trust-collection-file = "/app/localnet-tls/ca.crt"
215
+ }
216
+ canton.participants.app-provider.admin-api.tls {
217
+ cert-chain-file = "/app/localnet-tls/server.crt"
218
+ private-key-file = "/app/localnet-tls/server.key"
219
+ trust-collection-file = "/app/localnet-tls/ca.crt"
220
+ }
221
+ canton.participants.app-user.ledger-api.tls {
222
+ cert-chain-file = "/app/localnet-tls/server.crt"
223
+ private-key-file = "/app/localnet-tls/server.key"
224
+ trust-collection-file = "/app/localnet-tls/ca.crt"
225
+ }
226
+ canton.participants.app-user.admin-api.tls {
227
+ cert-chain-file = "/app/localnet-tls/server.crt"
228
+ private-key-file = "/app/localnet-tls/server.key"
229
+ trust-collection-file = "/app/localnet-tls/ca.crt"
230
+ }
231
+ canton.participants.sv.ledger-api.tls {
232
+ cert-chain-file = "/app/localnet-tls/server.crt"
233
+ private-key-file = "/app/localnet-tls/server.key"
234
+ trust-collection-file = "/app/localnet-tls/ca.crt"
235
+ }
236
+ canton.participants.sv.admin-api.tls {
237
+ cert-chain-file = "/app/localnet-tls/server.crt"
238
+ private-key-file = "/app/localnet-tls/server.key"
239
+ trust-collection-file = "/app/localnet-tls/ca.crt"
240
+ }
241
+ EOF
242
+ cat > "$canton_config_file" <<'EOF'
243
+ include file("/app/base-app.conf")
244
+ include file("/app/localnet-tls.conf")
245
+ EOF
246
+ cat > "$compose_file" <<EOF
247
+ services:
248
+ canton:
249
+ volumes:
250
+ - "${LOCALNET_DIR}/conf/canton/app.conf:/app/base-app.conf:ro"
251
+ - "$canton_config_file:/app/app.conf:ro"
252
+ - "$tls_fragment_file:/app/localnet-tls.conf:ro"
253
+ - "$server_certificate_path:/app/localnet-tls/server.crt:ro"
254
+ - "$server_key_path:/app/localnet-tls/server.key:ro"
255
+ - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
256
+ EOF
257
+ export LOCALNET_TLS_COMPOSE_FILE="$compose_file"
258
+ export LOCALNET_TLS_FRAGMENT_FILE="$tls_fragment_file"
259
+ export LOCALNET_TLS_CA_CERT_PATH="$ca_certificate_path"
260
+ }
261
+
101
262
  prepare_es256_runtime_files() {
102
263
  local runtime_dir
103
264
  runtime_dir="$(es256_runtime_dir)"
@@ -150,6 +311,8 @@ prepare_es256_runtime_files() {
150
311
  cat > "$canton_config_file" <<EOF
151
312
  include file("/app/base-app.conf")
152
313
 
314
+ $(if [[ "$(resolve_tls_enabled)" == "1" ]]; then printf 'include file("/app/localnet-tls.conf")\n'; fi)
315
+
153
316
  EOF
154
317
  local participant profile
155
318
  for participant in app-provider app-user sv; do
@@ -195,6 +358,13 @@ append_es256_args() {
195
358
  fi
196
359
  }
197
360
 
361
+ append_tls_args() {
362
+ local -n compose_args_ref="$1"
363
+ if [[ "$(resolve_tls_enabled)" == "1" ]]; then
364
+ compose_args_ref+=( -f "$LOCALNET_TLS_COMPOSE_FILE" )
365
+ fi
366
+ }
367
+
198
368
  resolve_docker_compose_cmd() {
199
369
  if (( ${#DOCKER_COMPOSE_CMD[@]} > 0 )); then
200
370
  return 0
@@ -414,6 +584,25 @@ canton.participants.extra-${index}.ledger-api.auth-services = [
414
584
  }
415
585
  ]
416
586
 
587
+ EOF
588
+ done
589
+ fi
590
+
591
+ if [[ "$(resolve_tls_enabled)" == "1" ]]; then
592
+ local index
593
+ for ((index = 1; index <= count; index++)); do
594
+ cat >> "$canton_config_file" <<EOF
595
+ canton.participants.extra-${index}.ledger-api.tls {
596
+ cert-chain-file = "/app/localnet-tls/server.crt"
597
+ private-key-file = "/app/localnet-tls/server.key"
598
+ trust-collection-file = "/app/localnet-tls/ca.crt"
599
+ }
600
+ canton.participants.extra-${index}.admin-api.tls {
601
+ cert-chain-file = "/app/localnet-tls/server.crt"
602
+ private-key-file = "/app/localnet-tls/server.key"
603
+ trust-collection-file = "/app/localnet-tls/ca.crt"
604
+ }
605
+
417
606
  EOF
418
607
  done
419
608
  fi
@@ -808,6 +997,7 @@ start_ledger_stack() {
808
997
  export SV_PROFILE=on
809
998
  export PQS_SV_PROFILE=on
810
999
  load_localnet_common_env "$localnet_dir/env/common.env"
1000
+ prepare_tls_runtime_files
811
1001
  prepare_es256_runtime_files
812
1002
  local extra_participants
813
1003
  extra_participants="$(resolve_extra_participants)"
@@ -841,6 +1031,7 @@ start_ledger_stack() {
841
1031
  fi
842
1032
 
843
1033
  append_extra_participant_args "$extra_participants" "$auth_mode" compose_args
1034
+ append_tls_args compose_args
844
1035
  append_es256_args compose_args
845
1036
  docker_compose "${compose_args[@]}" down -v --remove-orphans
846
1037
  mapfile -t startup_services < <(prerequisite_services "$auth_mode")
@@ -873,12 +1064,16 @@ if [[ ! -f .env.local ]]; then
873
1064
  make setup
874
1065
  fi
875
1066
 
876
- if [[ "$(resolve_es256_enabled)" != "1" ]] && make_target_exists start-local-ledger; then
1067
+ if [[ "$(resolve_es256_enabled)" != "1" && "$(resolve_tls_enabled)" != "1" ]] && make_target_exists start-local-ledger; then
877
1068
  make start-local-ledger
878
1069
  else
879
1070
  auth_mode="$(read_env_value AUTH_MODE || true)"
880
1071
  auth_mode="${auth_mode:-shared-secret}"
881
- echo "start-local-ledger target not found. Starting ledger-only compose stack directly."
1072
+ if [[ "$(resolve_es256_enabled)" == "1" || "$(resolve_tls_enabled)" == "1" ]]; then
1073
+ echo "Generated localnet transport overlay enabled. Starting ledger-only compose stack directly."
1074
+ else
1075
+ echo "start-local-ledger target not found. Starting ledger-only compose stack directly."
1076
+ fi
882
1077
  start_ledger_stack "$auth_mode"
883
1078
  if [[ "$(resolve_es256_enabled)" == "1" ]]; then
884
1079
  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
@@ -177,6 +200,7 @@ stop_ledger_stack() {
177
200
  fi
178
201
 
179
202
  append_existing_extra_participant_args compose_args
203
+ append_tls_args compose_args
180
204
  append_es256_args compose_args
181
205
  docker_compose "${compose_args[@]}" down -v --remove-orphans
182
206
  }
@@ -190,7 +214,7 @@ if [[ ! -f .env.local ]]; then
190
214
  exit 0
191
215
  fi
192
216
 
193
- if [[ "$(resolve_es256_enabled)" != "1" ]] && make_target_exists stop-local-ledger; then
217
+ if [[ "$(resolve_es256_enabled)" != "1" && "$(resolve_tls_enabled)" != "1" ]] && make_target_exists stop-local-ledger; then
194
218
  make stop-local-ledger
195
219
  else
196
220
  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" \
@@ -145,6 +150,7 @@ EOF
145
150
  chmod +x "$stubbin/docker-compose"
146
151
 
147
152
  local output
153
+ local status=0
148
154
  output="$(
149
155
  if [[ -n "$repo_root_env_contents" ]]; then
150
156
  printf '%s\n' "$repo_root_env_contents" > "$REPO_ENV_FILE"
@@ -158,20 +164,39 @@ EOF
158
164
  TERM="${TERM:-dumb}" \
159
165
  START_LOCAL_GENERATED_DIR="$generated_dir" \
160
166
  START_LOCAL_ES256_RUNTIME_DIR="$tmpdir/es256" \
167
+ START_LOCAL_TLS_RUNTIME_DIR="$tmpdir/tls" \
161
168
  LOCALNET_ES256_JWT="$es256_enabled" \
169
+ LOCALNET_TLS="$tls_enabled" \
170
+ LOCALNET_TLS_CERT_CHAIN_PATH="$tls_cert_chain_path" \
171
+ LOCALNET_TLS_PRIVATE_KEY_PATH="$tls_private_key_path" \
172
+ LOCALNET_TLS_CA_CERT_PATH="$tls_ca_cert_path" \
162
173
  EXTRA_PARTICIPANTS="$extra_participants" \
163
174
  PATH="$stubbin:$PATH" \
164
- bash ./start-local.sh
175
+ bash ./start-local.sh 2>&1
165
176
  else
166
177
  CN_QUICKSTART_DIR="${cn_quickstart_dir_override:-$quickstart_dir}" \
167
178
  START_LOCAL_GENERATED_DIR="$generated_dir" \
168
179
  START_LOCAL_ES256_RUNTIME_DIR="$tmpdir/es256" \
180
+ START_LOCAL_TLS_RUNTIME_DIR="$tmpdir/tls" \
169
181
  LOCALNET_ES256_JWT="$es256_enabled" \
182
+ LOCALNET_TLS="$tls_enabled" \
183
+ LOCALNET_TLS_CERT_CHAIN_PATH="$tls_cert_chain_path" \
184
+ LOCALNET_TLS_PRIVATE_KEY_PATH="$tls_private_key_path" \
185
+ LOCALNET_TLS_CA_CERT_PATH="$tls_ca_cert_path" \
170
186
  EXTRA_PARTICIPANTS="$extra_participants" \
171
187
  PATH="$stubbin:$PATH" \
172
- bash ./start-local.sh
188
+ bash ./start-local.sh 2>&1
173
189
  fi
174
- )"
190
+ )" || status=$?
191
+
192
+ if [[ "$expected_status" == "0" && "$status" != "0" ]]; then
193
+ printf 'expected successful start-local case, got status %s and output:\n%s\n' "$status" "$output" >&2
194
+ exit 1
195
+ fi
196
+ if [[ "$expected_status" != "0" && "$status" == "0" ]]; then
197
+ printf 'expected failing start-local case, got success and output:\n%s\n' "$output" >&2
198
+ exit 1
199
+ fi
175
200
 
176
201
  if ! grep -Fxq "$expected_output" <<<"$output"; then
177
202
  printf 'expected output line %q, got:\n%s\n' "$expected_output" "$output" >&2
@@ -190,6 +215,28 @@ assert_file_contains() {
190
215
  fi
191
216
  }
192
217
 
218
+ assert_file_contains_text() {
219
+ local path="$1"
220
+ local expected="$2"
221
+
222
+ if ! grep -Fq "$expected" "$path"; then
223
+ printf 'expected file %s to contain text %q, got:\n' "$path" "$expected" >&2
224
+ cat "$path" >&2
225
+ exit 1
226
+ fi
227
+ }
228
+
229
+ assert_file_mode() {
230
+ local path="$1"
231
+ local expected="$2"
232
+ local actual
233
+ actual="$(stat -c '%a' "$path")"
234
+ if [[ "$actual" != "$expected" ]]; then
235
+ printf 'expected file %s mode %s, got %s\n' "$path" "$expected" "$actual" >&2
236
+ exit 1
237
+ fi
238
+ }
239
+
193
240
  run_repo_root_env_case() {
194
241
  local quickstart_dir="$tmpdir/quickstart"
195
242
  local quickstart_root_dir="$tmpdir/custom-quickstart-root"
@@ -351,4 +398,54 @@ assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf"
351
398
  assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf" ' validator-party-hint = "${EXTRA_VALIDATOR_1_PARTY_HINT}"'
352
399
  assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf" ' domains.global.buy-extra-traffic {'
353
400
  assert_file_contains "$tmpdir/generated/additional-config.extra-validators.conf" 'canton.sv-apps.sv.expected-validator-onboardings += { secret = "${EXTRA_VALIDATOR_3_ONBOARDING_SECRET}" }'
401
+ 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
402
+ assert_file_contains_text "$tmpdir/tls/canton-tls.conf" 'canton.participants.app-provider.ledger-api.tls {'
403
+ assert_file_contains_text "$tmpdir/tls/canton-tls.conf" 'canton.participants.app-provider.admin-api.tls {'
404
+ assert_file_contains_text "$tmpdir/tls/canton-tls.conf" 'canton.participants.app-user.ledger-api.tls {'
405
+ assert_file_contains_text "$tmpdir/tls/canton-tls.conf" 'canton.participants.sv.admin-api.tls {'
406
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" '/app/localnet-tls/server.key:ro'
407
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" '/app/localnet-tls/ca.crt:ro'
408
+ assert_file_mode "$tmpdir/tls/server.key" 600
409
+ assert_file_mode "$tmpdir/tls/ca.key" 600
410
+ 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
411
+ assert_file_contains_text "$tmpdir/generated/additional-config.extra-participants.conf" 'canton.participants.extra-1.ledger-api.tls {'
412
+ assert_file_contains_text "$tmpdir/generated/additional-config.extra-participants.conf" 'canton.participants.extra-2.admin-api.tls {'
413
+
414
+ mkdir -p "$tmpdir/supplied"
415
+ openssl genrsa -out "$tmpdir/supplied/ca.key" 2048 >/dev/null 2>&1
416
+ openssl req -x509 -new -sha256 -days 3650 \
417
+ -key "$tmpdir/supplied/ca.key" \
418
+ -subj '/CN=supplied-localnet-ca' \
419
+ -addext 'basicConstraints=critical,CA:TRUE' \
420
+ -addext 'keyUsage=critical,keyCertSign,cRLSign' \
421
+ -out "$tmpdir/supplied/ca.crt" >/dev/null 2>&1
422
+ openssl genrsa -out "$tmpdir/supplied/server.key" 2048 >/dev/null 2>&1
423
+ openssl req -new -sha256 -key "$tmpdir/supplied/server.key" \
424
+ -subj '/CN=localhost' -out "$tmpdir/supplied/server.csr" >/dev/null 2>&1
425
+ printf '%s\n' \
426
+ '[server_ext]' \
427
+ 'basicConstraints = CA:FALSE' \
428
+ 'keyUsage = digitalSignature, keyEncipherment' \
429
+ 'extendedKeyUsage = serverAuth' \
430
+ 'subjectAltName = DNS:localhost, DNS:canton, IP:127.0.0.1' \
431
+ > "$tmpdir/supplied/server-extensions.cnf"
432
+ openssl x509 -req -sha256 -days 3650 \
433
+ -in "$tmpdir/supplied/server.csr" \
434
+ -CA "$tmpdir/supplied/ca.crt" -CAkey "$tmpdir/supplied/ca.key" -CAcreateserial \
435
+ -out "$tmpdir/supplied/server.crt" \
436
+ -extfile "$tmpdir/supplied/server-extensions.cnf" -extensions server_ext >/dev/null 2>&1
437
+ 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"
438
+ if [[ -e "$tmpdir/tls/server.key" || -e "$tmpdir/tls/ca.key" ]]; then
439
+ echo 'supplied TLS mode unexpectedly generated private material' >&2
440
+ exit 1
441
+ fi
442
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" "$tmpdir/supplied/server.key:/app/localnet-tls/server.key:ro"
443
+ openssl genrsa -out "$tmpdir/supplied/mismatch.key" 2048 >/dev/null 2>&1
444
+ 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
445
+ 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
446
+ run_case $'.PHONY: start\nstart:\n' 'LOCALNET_TLS must be 0 or 1.' shared-secret default 0 '' '' 0 invalid '' '' '' 1
447
+ run_case $'.PHONY: start\nstart:\n' 'ES256 bearer token written to '"$tmpdir"'/es256/ledger-api-user.token' shared-secret default 0 '' '' 1 1
448
+ assert_file_contains_text "$tmpdir/es256/canton-es256.conf" 'include file("/app/localnet-tls.conf")'
449
+ assert_file_contains_text "$tmpdir/es256/compose-es256.yaml" '/app/es256-certificate.pem:ro'
450
+ assert_file_contains_text "$tmpdir/tls/compose-localnet.yaml" '/app/localnet-tls/server.crt:ro'
354
451
  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"
@@ -95,6 +100,8 @@ EOF
95
100
  cd "$SCRIPT_DIR"
96
101
  CN_QUICKSTART_DIR="$quickstart_dir" \
97
102
  START_LOCAL_GENERATED_DIR="$generated_dir" \
103
+ START_LOCAL_TLS_RUNTIME_DIR="$tmpdir/tls" \
104
+ LOCALNET_TLS="$tls_enabled" \
98
105
  PATH="$stubbin:$PATH" \
99
106
  bash ./stop-local.sh
100
107
  )"
@@ -109,4 +116,4 @@ run_case $'.PHONY: stop-local-ledger\nstop-local-ledger:\n' 'stub make stop-loca
109
116
  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
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 -f '"$tmpdir"'/quickstart/docker/modules/keycloak/compose.yaml --env-file '"$tmpdir"'/quickstart/docker/modules/keycloak/compose.env --profile keycloak down -v --remove-orphans' oauth2
111
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 down -v --remove-orphans' shared-secret compose-v1
112
-
119
+ 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.18",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",