-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathproof-of-concept.tex
581 lines (497 loc) · 30 KB
/
proof-of-concept.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
\chapter{Proof of Concept}
\label{ch:proof-of-concept}
To evaluate the suitability of the languages in the short list, a proof of concept was made that covers some typical use cases of TrustBuilder's extensions.
Assuming the target functionality defined in the requirements analysis, extensions will made up of units called Actions, which can each handle a specific task. Actions can take inputs and produce outputs, and be tied together into a workflow. The way these actions are composed into workflows will be the responsibility of the encompassing framework, and is out of scope for this thesis.
The proof of concept for each language will consist of the following:
\begin{itemize}
\item An Action that retrieves user information from a REST API. The GitHub API is used here as an example.
\item An Action that retrieves user information from a database. A local SQLite database is used here as an example.
\item An Action that verifies a TOTP code, as an example of a typical cybersecurity task.
\item An Action that passes data to the workflow that encompasses it.
\item An automatically generated CycloneDX SBOM for the project that implements the above functionality.
\end{itemize}
The code examples in the following sections of this chapter are snippets, and often omit lines such as imports and type definitions for the sake of brevity and readability. The full codebases of the proofs of concept can be retrieved at the following URLs:
\begin{itemize}
\item TypeScript: \url{https://github.com/aikovdp/thesis-poc-ts}
\item Java: \url{https://github.com/aikovdp/thesis-poc-java}
\end{itemize}
\section{Actions}
The conceptual ``Actions'' detailed above, units of code accepting input and output, can be represented as functions in both TypeScript and Java.
The Java implementation in listing~\ref{lst:action-java} takes the form of a generic interface with a single \texttt{execute} method. The interface accepts type parameters \texttt{I} and \texttt{O}, representing the input and output types of the action. The \texttt{execute} method accepts an input of type \texttt{I} and a \texttt{WorkflowContext} object, and produces an output of type \texttt{O}.
\begin{listing}[H]
\begin{minted}{java}
public interface Action<I, O> {
O execute(I input, WorkflowContext context);
}
\end{minted}
\caption{Action interface in Java}
\label{lst:action-java}
\end{listing}
The TypeScript implementation in listing~\ref{lst:action-ts} is similar, but the function can be defined as a type itself, without an encompassing interface. It is also a generic type, accepting \texttt{Input} and \texttt{Output} type parameters. There is no underlying technical reason why the Java type parameter names are a single capital letter (\texttt{I}, \texttt{O}) while those in TypeScript are PascalCased (\texttt{Input}, \texttt{Output}). This is simply following stylistic conventions for those languages.
\begin{listing}[H]
\begin{minted}{typescript}
export type Action<Input, Output> =
(input: Input, context: WorkflowContext) => Promise<Output>
\end{minted}
\caption{Action type in TypeScript}
\label{lst:action-ts}
\end{listing}
The Action construct can be elegantly declared in both languages, each in their own idiomatic way.
\pagebreak
\section{REST API Call Action}
\subsection{TypeScript}
\begin{listing}[h]
\begin{minted}[fontsize=\footnotesize]{typescript}
export const getGitHubUserAction: Action<GetGitHubUserActionInput, GithubUser> =
async (input) => {
const res = await fetch(`https://api.github.com/users/${input.username}`);
return res.json();
};
\end{minted}
\caption{REST API call in TypeScript}
\end{listing}
Calling a REST API in TypeScript is straightforward. Through the Fetch API, these API calls can be performed with little complexity in very few lines of code. Its \texttt{json()} method also allows you to easily parse the returned JSON response.
JSON serialization and deserialization is also part of the language specification, making JavaScript and TypeScript even better suited for consuming JSON REST APIs \autocite{EcmaInternational2024}. This is to be expected, considering JavaScript's roots in web browsers. However, in this proof of concept, no validation is performed on the returned object. TypeScript simply casts the parsed response's type to the \texttt{GithubUser} type, without any additional checks or warnings. It is the programmer's responsibility to be aware of this, and validate the response's content accordingly.
\subsection{Java}
As of Java 11, the Java platform provides a built-in HTTP Client API, making it easy to call REST APIs without an external library. Compared to the TypeScript implementation of this call, the Java one is significantly more verbose, requiring a try-with-resources statement to handle automatic closure of the client, and handling two checked exceptions. Although this makes the code more robust and determinate, it does make the language less appealing for use in small snippets like Actions. Without checked exceptions, the programmer wouldn't be required to decide on what to do with them, and could let the encompassing framework handle them instead.
Additionally, JSON (de)serialization is not part of the Java Platform APIs, requiring a third party library to cover this functionality. In this proof of concept, Google's Gson was chosen. This once again requires additional code and introduces some supply chain risk, but validates the JSON contents when deserializing it into an object of the specified class, unlike TypeScript's unsafe cast.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public class GetGitHubUserAction implements Action<Input, GitHubUser> {
private static final URI GITHUB_USERS_URI = URI.create("https://api.github.com/users/");
private final Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
@Override
public GitHubUser execute(Input input, WorkflowContext context) {
try (var client = HttpClient.newHttpClient()) {
HttpRequest request = HttpRequest.newBuilder()
.uri(GITHUB_USERS_URI.resolve(input.username))
.build();
String response = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
return gson.fromJson(response, GitHubUser.class);
} catch (IOException e) {
throw new ActionException("Error occurred trying to reach GitHub API", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ActionException("Thread was interrupted while trying to reach GitHub API", e);
}
}
}
\end{minted}
\caption{REST API call in Java}
\end{listing}
\subsection{Summary}
While TypeScript provides a quick way to call REST services and covers most functionality out of the box, it drops the ball in terms of safety by default. A happy flow can be written in no time, but the programmer is responsible for taking the necessary safety measures for when something goes wrong, if these scenarios are not handled by the encompassing framework. What Java lacks in terseness and JSON functionality, it makes up for in safety. It is type safe every step of the way, and requires the programmer to handle exceptions, but as a result is significantly more verbose than TypeScript.
\pagebreak
\section{Database Query Action}
% TODO
Apart from REST APIs, workflows also often query databases directly. The Actions on display in this section query a user by their from a SQLite database created with the SQL statements in listing~\ref{lst:sqlite}.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{sql}
CREATE TABLE users
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
given_name TEXT,
family_name TEXT,
email TEXT NOT NULL
);
CREATE UNIQUE INDEX users_email_uindex
ON users (email);
CREATE UNIQUE INDEX users_username_uindex
ON users (username);
INSERT INTO users (id, username, given_name, family_name, email)
VALUES (1, 'jsmith', 'John', 'Smith', '[email protected]');
INSERT INTO users (id, username, given_name, family_name, email)
VALUES (2, 'jdoe', 'Jane', 'Doe', '[email protected]');
\end{minted}
\caption{\label{lst:sqlite}DDL and seed for SQLite database}
\end{listing}
\subsection{Java}
The Java implementation of this Action in listing~\ref{lst:db-java} uses the JDBI library, a lightweight wrapper around JDBC, which in turn is Java's built-in database connectivity API. It provides simple access to a database via a JDBC URL, and the ability to map a query result to an object. This implementation is quite straightforward.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public class GetDatabaseUserAction implements Action<Input, DatabaseUser> {
private final Jdbi jdbi;
public GetDatabaseUserAction(String url) {
this.jdbi = Jdbi.create(url);
}
@Override
public DatabaseUser execute(Input input, WorkflowContext context) {
return jdbi.withHandle(handle ->
handle.createQuery("SELECT id, given_name, family_name, username, email FROM users WHERE username = :username")
.bind("username", input.username)
.map((rs, ctx) -> new DatabaseUser(
rs.getInt("id"),
rs.getString("given_name"),
rs.getString("family_name"),
rs.getString("username"),
rs.getString("email")
)
).one()
);
}
public record Input(
String username
) {}
public record DatabaseUser(
int id,
String givenName,
String familyName,
String username,
String email
) { }
}
\end{minted}
\caption{\label{lst:db-java}Database query action in Java}
\end{listing}
\subsection{TypeScript}
TypeScript has no equivalent for JDBC, so it has to rely entirely on external libraries. This implementation (listing~\ref{lst:db-ts}) uses Drizzle, a lightweight TypeScript object-relational mapper (ORM). The ORM requires defining the database schema in the code, but is then able to provide a type-safe interface through which the database can be queried, along with automatic object mapping, and detailed code completion hints. This is a prime example implicit documentation as mentioned in the literature review as one of the benefits of statically typed languages.
It should also be noted that even despite this extra ORM functionality in the TypeScript implementation, it still contains less lines of code than the Java implementation.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
const users = sqliteTable('users', {
id: integer('id').primaryKey(),
givenName: text("given_name"),
familyName: text("family_name"),
username: text("username").notNull(),
email: text("email").notNull()
})
const db = drizzle(new Database('database.db'), {schema: {users: users}})
export const getDbUser: Action<GetDbUserActionInput, DbUser | undefined> =
async (input) => {
return await db.query.users.findFirst({
where: eq(users.username, input.username)
})
}
interface GetDbUserActionInput {
username: string
}
interface DbUser {
id: number,
givenName?: string | null,
familyName?: string | null,
username: string,
email: string
}
\end{minted}
\caption{\label{lst:db-ts}Database query action in TypeScript}
\end{listing}
\subsection{Summary}
The database querying capabilities of Java and TypeScript cannot be properly compared based on these two pieces of code alone, but both languages are able to query databases with ease. Java has its JDBC API, which standardizes how Java applications communicate with databases, upon which other libraries can rely. Although TypeScript doesn't have this standard API, other libraries like Drizzle still provide lean ways to query a databases, with full ORM functionality providing type-safe querying interfaces, automatic object mapping and useful code completion.
\pagebreak
\section{TOTP Verification Action}
Verifying a TOTP code is a prime example of typical cybersecurity functionality commonly implemented through workflows at TrustBuilder. This makes it an excellent candidate to test a language's suitability for such tasks. As mentioned in the literature review, implementing the TOTP algorithm depends on a couple of additional technologies: Unix time, HMAC, Base32, and bitwise operations. Both implementations here abstract away functionality in three distinct units: the verification action depends on the TOTP generator, which in turn depends on the HOTP generator.
\subsection{TypeScript}
Through the use of the Web Cryptography API, an HMAC-SHA1 hash can easily be generated from the provided secret and counter value. However, converting the counter value to its required 8-byte binary representation requires additional measures to be taken due to a lack of specific number types in JavaScript. The \texttt{Number} value needs to be converted to a \texttt{BigInt}, to then be set as an unsigned 64-bit \texttt{BigInt} on the \texttt{DataView} encompassing the \texttt{ArrayBuffer}. This can be seen in listing~\ref{lst:hotp-ts}. Dynamic truncation of the HMAC-SHA1 hash was done using a DataView object, making the bitwise operations relatively trivial.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
import { Action } from "../action.js";
import * as totp from "../lib/totp.js"
export const verifyTotp: Action<VerifyTotpInput, boolean> = async (input) => {
return parseInt(input.code) === await totp.generate(input.key);
}
interface VerifyTotpInput {
key: string,
code: string
}
\end{minted}
\caption{TOTP verification Action in TypeScript}
\end{listing}
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
import * as hotp from "./hotp.js";
export const TIME_STEP_SECONDS = 30
export async function generate(key: string) {
const counter = Math.floor(Date.now() / 1000 / TIME_STEP_SECONDS);
return hotp.generate(key, counter);
}
\end{minted}
\caption{TOTP generator in TypeScript}
\end{listing}
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
import * as base32 from "./base32.js";
export async function generate(key: string, counter: number) {
const buffer = new ArrayBuffer(8);
new DataView(buffer).setBigUint64(0, BigInt(counter));
const hash = await hmacSha(base32.decode(key), buffer);
return truncate(hash) % 10 ** 6;
}
async function hmacSha(
keyBytes: ArrayBuffer,
dataBytes: ArrayBuffer
): Promise<ArrayBuffer> {
const key = await crypto.subtle.importKey(
"raw",
keyBytes,
{
name: "HMAC",
hash: "SHA-1",
},
false,
["sign"]
);
return crypto.subtle.sign("HMAC", key, dataBytes);
}
function truncate(hash: ArrayBuffer): number {
const view = new DataView(hash);
const offset = view.getUint8(view.byteLength - 1) & 0xf;
return view.getInt32(offset) & 0x7fffffff;
}
\end{minted}
\caption{HOTP generator in TypeScript}
\label{lst:hotp-ts}
\end{listing}
The Base32 private key also needs to be decoded, but no such functionality is provided by the language or Web APIs, and no reputable platform-agnostic libraries could be found that provide this functionality. As a result, this required an implementation from scratch.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
export function decode(base32: string) {
base32 = base32.replace(/=+$/, "");
let currentValue = 0;
let bits = 0;
let index = 0;
const array = new Uint8Array((base32.length * 5) / 8);
base32
.split("")
.map((char) => alphabet.indexOf(char))
.forEach((value) => {
if (value === -1) throw new Error("Invalid character in base32 string");
currentValue = (currentValue << 5) | value;
bits += 5;
if (bits >= 8) {
array[index++] = currentValue >>> (bits - 8);
bits -= 8;
}
});
return array.buffer;
}
\end{minted}
\caption{Base32 decoder in TypeScript}
\end{listing}
\subsection{Java}
The Java Cryptography Architecture, encompassing the \texttt{java.security} and \texttt{javax.crypto} packages, provides API for handling the cryptographic key and generating the HMAC-SHA1 hash. Base32 decoding is not bundled with the Java platform either though, also requiring a separate library or custom implementation. With Java having a longer history of being used as a server-side language, its ecosystem of cryptography libraries is able to fill this need easily. This proof of concept uses the Bouncy Castle library to decode the Base32-encoded secret key.
To perform the bitwise operations necessary for the binary representation of the counter value and the dynamic truncation of the HMAC-SHA1 hash, the ByteBuffer class of the Java NIO API is used. The counter conversion is easier here however, as the 8-byte \texttt{long} value can be directly placed into the ByteBuffer. Thanks to Java's more specific numeric types, a host of accidental bugs is avoided here by the type system itself.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public class VerifyTOTPAction implements Action<Input, Boolean> {
@Override
public Boolean execute(Input input, WorkflowContext context) {
return input.totp() == TOTP.generate(input.key());
}
public record Input(
String key,
int totp
) {}
}
\end{minted}
\caption{TOTP verification Action in Java}
\end{listing}
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public final class TOTP {
public static final int TIME_STEP_SECONDS = 30;
private TOTP() {}
public static int generate(String key) {
long counter = ZonedDateTime.now().toEpochSecond() / TIME_STEP_SECONDS;
return HOTP.generate(key, counter);
}
}
\end{minted}
\caption{TOTP generator in Java}
\end{listing}
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public final class HOTP {
private HOTP() {}
public static int generate(String key, long counter) {
byte[] hash = hmacSha(key, longToByteArray(counter));
return truncate(hash) % (int) Math.pow(10, 6);
}
public static byte[] hmacSha(String keyString, byte[] bytes) {
Key key = new SecretKeySpec(Base32.decode(keyString), "RAW");
Mac hmacSha1;
try {
hmacSha1 = Mac.getInstance("HmacSHA1");
hmacSha1.init(key);
return hmacSha1.doFinal(bytes);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new ActionException(e);
}
}
private static byte[] longToByteArray(long value) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.putLong(value);
return buffer.array().clone();
}
private static int truncate(byte[] hash) {
int offset = hash[hash.length - 1] & 0xf;
ByteBuffer buffer = ByteBuffer.wrap(hash);
// Get 4 bytes at offset index, and discard the most significant one
return buffer.getInt(offset) & 0x7FFFFFFF;
}
}
\end{minted}
\caption{HOTP generator in Java}
\end{listing}
\subsection{Summary}
With the Web Cryptography API and Java's Cryptography Architecture, both languages are at a similar level of maturity for core cryptographic operations. The surrounding lower-level operations are not, however. Java's more precise numeric types make these easier, and its more mature ecosystem offers reputable cryptography libraries TypeScript's cannot provide.
\pagebreak
\section{Workflow Context}
Actions like these would be composed together in a ``workflow'', executing the Actions in order and passing data between them. A workflow can also have its own output, which might have to conform to a certain schema depending on where the workflow is being triggered from. Although the design of the encompassing workflow framework is outside the scope of this thesis, passing data from an action into the workflow context could look vastly different in TypeScript than it does in Java.
This proof of concept implements it according to how this would typically be handled in projects written in each language. The goal of the Action here is to set an ``attribute'' value on the output of the workflow, according to a pre-defined schema.
\subsection{TypeScript}
The necessary data schemas take the form of interface declarations in TypeScript. Listing~\ref{lst:ctx-ts} declares the structure of the \texttt{WorkflowContext} object as containing an \texttt{output} object. This \texttt{output} object can contain any properties, but any value in the \texttt{attributes} field must contain an object that satisfies the \texttt{Attributes} interface.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
export interface WorkflowContext {
output: WorkflowOutput;
}
export interface WorkflowOutput {
attributes?: Attributes;
[property: string]: any;
}
export interface Attributes {
[category: string]: {
[name: string]: string[];
};
}
\end{minted}
\caption{\label{lst:ctx-ts}WorkflowContext and Attributes interfaces in TypeScript}
\end{listing}
The action which sets a given attribute value on the workflow output can do so directly through the context object. Modern JavaScript features allow you to do this concisely through object destructuring, spread syntax (\texttt{...}), and optional chaining. Despite being concise, this may be considered hard to read with this level of nesting.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
export const setAttributeAction: Action<SetAttributeInput, void> =
async (input, context) => {
const { attributes } = context.output
context.output.attributes = {
...attributes,
[input.category]: {
...(attributes?.[input.category]),
[input.name]: input.value
}
}
}
interface SetAttributeInput {
category: string,
name: string,
value: string[]
}
\end{minted}
\caption{''Set attribute'' Action in TypeScript}
\end{listing}
\subsection{Java}
The Java implementation is more structured, but lengthier. To allow developers the freedom to pass any object to the workflow output, the generic \texttt{OutputKey} class was made (listing~\ref{lst:outputkey}). The \texttt{WorkflowOutput} class (listing~\ref{lst:wf-output-java}) delegates to a HashMap, in which the \texttt{path} property of the \texttt{OutputKey} is used as the key. The \texttt{OutputKey}'s type parameter serves as a type guarantee for the data present in the output at that key. By defining \texttt{OutputKey}s, the workflow output can hold any data type, while still containing the expected types at each field.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public record OutputKey<T>(String path) {
public static final OutputKey<Attributes> ATTRIBUTES = new OutputKey<>("attributes");
}
\end{minted}
\caption{\label{lst:outputkey}The \texttt{OutputKey} class in Java}
\end{listing}
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public class WorkflowOutput {
private final Map<String, Object> output = new HashMap<>();
public <T> void put(OutputKey<T> key, T value) {
output.put(key.path(), value);
}
@SuppressWarnings("unchecked")
public <T> T get(OutputKey<T> key) {
return (T) output.get(key.path());
}
@SuppressWarnings("unchecked")
public <T> T computeIfAbsent(OutputKey<T> key, Function<String, ? extends T> mappingFunction) {
return (T) output.computeIfAbsent(key.path(), mappingFunction);
}
}
\end{minted}
\caption{\label{lst:wf-output-java}The \texttt{WorkflowOutput} class in Java}
\end{listing}
The Attributes class wraps a nested HashMap containing lists of String values. The Action then uses this class to set the attribute value in a strongly typed manner.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public class Attributes {
private final Map<String, Map<String, List<String>>> attributes = new HashMap<>();
public void setAttribute(String category, String name, List<String> values) {
attributes.computeIfAbsent(category, (key) -> new HashMap<>())
.put(name, values);
}
public List<String> getAttribute(String category, String name) {
Map<String, List<String>> categoryMap = attributes.get(category);
if (categoryMap == null) return null;
return categoryMap.get(name);
}
}
\end{minted}
\caption{Attributes class in Java}
\end{listing}
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public class SetAttributeAction implements Action<Input, Void> {
@Override
public Void execute(Input input, WorkflowContext context) {
Attributes attributes = context.getOutput().computeIfAbsent(OutputKey.ATTRIBUTES, (k) -> new Attributes());
attributes.setAttribute(input.category(), input.name(), input.values());
return null;
}
}
\end{minted}
\caption{''Set attribute'' Action in Java}
\end{listing}
\subsection{Summary}
Both languages are a suitable fit for the presumed framework, though the implementation of it would vary wildly. TypeScript is clearly able to handle loosely structured data more easily though, likely because of its structural type system and its roots in the dynamically typed JavaScript. The idiomatic implementation of this in TypeScript is also drastically more concise.
\pagebreak
\section{Testing}
Tests were written in both languages for all Actions mentioned above. The Actions concept is extremely well suited to unit tests, as they are specific units of code producing a specific output for a given input. Unit tests were written using Vitest for TypeScript and JUnit for Java.
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{typescript}
test('returns octocat user', async () => {
const user = await getGitHubUserAction({username: "octocat"}, createContext())
expect(user.login, "octocat")
expect(user.name, "The Octocat")
expect(user.company, "@github")
})
\end{minted}
\caption{GitHub API Action test in TypeScript}
\end{listing}
\begin{listing}[H]
\begin{minted}[fontsize=\footnotesize]{java}
public class GetGitHubUserActionTest {
@Test
public void testGitHubUserAction() {
GetGitHubUserAction action = new GetGitHubUserAction();
GitHubUser user = action.execute(new GetGitHubUserAction.Input("octocat"), new WorkflowContext());
assertEquals("octocat", user.login());
assertEquals("The Octocat", user.name());
assertEquals("@github", user.company());
}
}
\end{minted}
\caption{GitHub API Action test in Java}
\end{listing}
\section{SBOM Generation}
If customers will be allowed to write their own Actions, they should be required to include an SBOM as discussed in the literature review. Generating a CycloneDX SBOM is trivial for both TypeScript and Java.
In the TypeScript project, for which the pnpm package manager was used, the first-party CycloneDX Generator can be installed through pnpm itself. It can then generate an SBOM for the project with a single command:
\mint{pwsh}|pnpm cdxgen -t pnpm -o bom.json|
In the Java project, made with the Gradle Build Tool, the official CycloneDX Gradle plugin can be applied in the build script as shown in listing~\ref{lst:gradle-plugins}. The SBOM can then be generated simply by running the \texttt{cyclonedxBom} task.
\begin{listing}[H]
\begin{minted}[highlightlines={3}]{kotlin}
plugins {
`java-library`
id("org.cyclonedx.bom") version "1.8.2"
}
\end{minted}
\caption{\label{lst:gradle-plugins}Plugin configuration in Gradle build script}
\end{listing}
Neither language is better suited to this than the other, and this task will likely also be trivial in most other popular languages that use a package manager.
\section{Other Findings}
During the process of creating the proofs of concept, Java's ecosystem felt more mature and reliable than TypeScript’s. Where Java is designed through the singular OpenJDK project, JavaScript (and by extension, TypeScript) is standardized through the ECMAScript Language Specification, with certain other pieces of functionality standardized by WHATWG, which are not always implemented by all runtimes.
Java also has a specific target runtime, the JVM, whereas JavaScript/TypeScript may run in the browser, Node, Deno, Bun, or various serverless runtimes, each with different feature sets. When creating the proof of concept, certain choices needed to be made to remain runtime-agnostic, as using a specific library may limit the project to only being able to run in runtimes supported by that library.
When it comes to libraries, Java also shows more singular industry standards \autocite{JetBrains2023}. Maven and Gradle are by far the most used build tools, unit tests are written with JUnit and Mockito, and database access happens through JDBC. This made it easy to pick the right libraries for the job. TypeScript on the other hand has different popular package managers (npm, yarn, pnpm), popular testing frameworks (Jest, Vitest, Mocha), and bundlers (Webpack, Vite) \autocite{Greif2024}. Its ecosystem has many tools covering the same functionality, with rapidly evolving usage statistics and no clear industry standard.
These findings are not strictly negative for TypeScript. They can be considered signs that the JavaScript/TypeScript ecosystem is thriving, with many authors creating tools to do things the way they believe is best, without settling for the most popular option at the time. It is more prone to trends however, and technology choices made now may become outdated sooner in the TypeScript ecosystem than they do in the Java ecosystem. Considering that one of the main problems of the current workflow system is that it is outdated and no longer following industry standards, Java may be a safer bet.
On the other hand, implementing Actions in TypeScript was consistently experienced as being more lean: each TypeScript implementation is considerably less verbose than its JavaScript counterpart. Part of this is because of Java's syntax, but also because it takes additional safety measures. Checked exceptions require the programmer to think about how the code should behave in case things go wrong, and need to be handled by the programmer. This can be seen as an advantage—ensuring more runtime safety— or as a disadvantage for requiring the programmer to deal with something the encompassing framework could handle. Lines of code are certainly not an indicator of software quality, but it can influence the development experience, and should not be taken for granted when this developer experience is customer-facing.