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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ SERVICE_AUTH_SECRET= # 32 random bytes, base64 encoded. Used for secure JWT midd
CHALLENGE_TTL=900 #15m
SESSION_TTL=21600 #6h

# OPENTELEMETRY (opt-in, requires Jaeger or OTLP-compatible collector)
# OTEL_DENO=true
# OTEL_SERVICE_NAME=provider-platform
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

# MEMPOOL CONFIGURATION
MEMPOOL_SLOT_CAPACITY=100
MEMPOOL_EXPENSIVE_OP_WEIGHT=10
Expand Down
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"@oak/oak": "jsr:@oak/oak@^17.1.4",
"@olli/kvdex": "jsr:@olli/kvdex@^3.1.4",
"zod": "npm:zod@3.24.2",
"chalk": "npm:chalk@^5.3.0"
"chalk": "npm:chalk@^5.3.0",
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0"
},
"deploy": {
"project": "159af174-b645-4039-be9c-493de4886f26",
Expand Down
12 changes: 10 additions & 2 deletions deno.lock

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

86 changes: 50 additions & 36 deletions src/core/service/auth/challenge/create/create-challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,59 @@ import { assertOrThrow } from "@/utils/error/assert-or-throw.ts";
import { isDefined } from "@/utils/type-guards/is-defined.ts";
import * as E from "@/core/service/auth/challenge/create/error.ts";
import { logAndThrow } from "@/utils/error/log-and-throw.ts";
import { withSpan } from "@/core/tracing.ts";

export const P_CreateChallenge = ProcessEngine.create(
async (input: GetChallengeInput): Promise<ChallengeData> => {
const { ctx, query } = input;
const clientAccount = query.account;

assertOrThrow(isDefined(clientAccount), new E.MISSING_CLIENT_ACCOUNT());

try {
const { tx, nonce, minTime, maxTime } =
getChallengeTransaction(clientAccount);

const xdr = tx.toXDR();
const txHash = tx.hash().toString("hex");

const dateCreated = new Date(minTime * 1000);
const expiresAt = new Date(maxTime * 1000);

const { clientIp, userAgent, requestId } = extractRequestMetadata(ctx);

const output: ChallengeData = {
ctx,
challengeData: {
txHash: txHash,
clientAccount: clientAccount,
xdr,
nonce,
dateCreated: dateCreated,
requestId,
clientIp,
userAgent,
expiresAt,
},
};

return await output;
} catch (error) {
logAndThrow(new E.FAILED_TO_CREATE_CHALLENGE(error));
}
return withSpan("P_CreateChallenge", async (span) => {
const { ctx, query } = input;
const clientAccount = query.account;

span.addEvent("validating_client_account", { "client.account": clientAccount ?? "undefined" });
assertOrThrow(isDefined(clientAccount), new E.MISSING_CLIENT_ACCOUNT());

try {
span.addEvent("building_challenge_transaction");
const { tx, nonce, minTime, maxTime } =
getChallengeTransaction(clientAccount);

const xdr = tx.toXDR();
const txHash = tx.hash().toString("hex");

const dateCreated = new Date(minTime * 1000);
const expiresAt = new Date(maxTime * 1000);

const { clientIp, userAgent, requestId } = extractRequestMetadata(ctx);

span.addEvent("challenge_created", {
"challenge.txHash": txHash,
"challenge.clientAccount": clientAccount,
"challenge.requestId": requestId,
});

const output: ChallengeData = {
ctx,
challengeData: {
txHash: txHash,
clientAccount: clientAccount,
xdr,
nonce,
dateCreated: dateCreated,
requestId,
clientIp,
userAgent,
expiresAt,
},
};

return await output;
} catch (error) {
span.addEvent("challenge_creation_failed", {
"error.message": error instanceof Error ? error.message : String(error),
});
logAndThrow(new E.FAILED_TO_CREATE_CHALLENGE(error));
}
});
},
{
name: "CreateChallengeProcessEngine",
Expand Down
34 changes: 19 additions & 15 deletions src/core/service/auth/challenge/create/generate-challenge-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,35 @@ import generateJwt from "@/core/service/auth/generate-jwt.ts";
import * as E from "@/core/service/auth/challenge/create/error.ts";
import { assertOrThrow } from "@/utils/error/assert-or-throw.ts";
import { isDefined } from "@/utils/type-guards/is-defined.ts";
import { withSpan } from "@/core/tracing.ts";

export const P_GenerateChallengeJWT = ProcessEngine.create(
async (
input: PostChallengeInput,
_metadataHelper?: MetadataHelper
): Promise<PostChallengeWithJWT> => {
// Assume the input was already validated by an earlier process.
const { signedChallenge } = input.body;
const tx = new Transaction(
signedChallenge,
NETWORK_CONFIG.networkPassphrase
);
return withSpan("P_GenerateChallengeJWT", async (span) => {
const { signedChallenge } = input.body;
const tx = new Transaction(
signedChallenge,
NETWORK_CONFIG.networkPassphrase
);

const key = tx.hash().toString("hex");
const key = tx.hash().toString("hex");

const clientAccount = tx.operations[0].source;
assertOrThrow(isDefined(clientAccount), new E.MISSING_CLIENT_ACCOUNT());
const clientAccount = tx.operations[0].source;
assertOrThrow(isDefined(clientAccount), new E.MISSING_CLIENT_ACCOUNT());

const jwt = await generateJwt(clientAccount, key);
span.addEvent("generating_jwt", { "client.account": clientAccount });
const jwt = await generateJwt(clientAccount, key);
span.addEvent("jwt_generated");

return {
ctx: input.ctx,
body: input.body,
jwt,
};
return {
ctx: input.ctx,
body: input.body,
jwt,
};
});
},
{
name: "GenerateChallengeJWT",
Expand Down
69 changes: 40 additions & 29 deletions src/core/service/auth/challenge/store/create-challenge-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,57 @@ import {
import type { ChallengeData } from "@/core/service/auth/challenge/types.ts";
import { logAndThrow } from "@/utils/error/log-and-throw.ts";
import * as E from "@/core/service/auth/challenge/store/error.ts";
import { withSpan } from "@/core/tracing.ts";

const challengeRepository = new ChallengeRepository(drizzleClient);
const userRepository = new UserRepository(drizzleClient);
const accountRepository = new AccountRepository(drizzleClient);

export const P_CreateChallengeDB = ProcessEngine.create(
async (input: ChallengeData) => {
const { challengeData } = input;
try {
let account = await accountRepository.findById(
challengeData.clientAccount
);
return withSpan("P_CreateChallengeDB", async (span) => {
const { challengeData } = input;
try {
span.addEvent("looking_up_account", { "client.account": challengeData.clientAccount });
let account = await accountRepository.findById(
challengeData.clientAccount
);

let user: NewUser | undefined;
if (!account) {
user = await userRepository.create({
id: crypto.randomUUID(),
status: UserStatus.UNVERIFIED,
} as NewUser);
let user: NewUser | undefined;
if (!account) {
span.addEvent("creating_new_user_and_account");
user = await userRepository.create({
id: crypto.randomUUID(),
status: UserStatus.UNVERIFIED,
} as NewUser);

account = await accountRepository.create({
id: challengeData.clientAccount,
type: "USER",
userId: user.id,
} as NewAccount);
}
account = await accountRepository.create({
id: challengeData.clientAccount,
type: "USER",
userId: user.id,
} as NewAccount);
} else {
span.addEvent("account_exists");
}

await challengeRepository.create({
id: crypto.randomUUID(),
accountId: account.id,
status: ChallengeStatus.UNVERIFIED,
ttl: challengeData.expiresAt,
txHash: challengeData.txHash,
txXDR: challengeData.xdr,
} as NewChallenge);
span.addEvent("persisting_challenge", { "challenge.txHash": challengeData.txHash });
await challengeRepository.create({
id: crypto.randomUUID(),
accountId: account.id,
status: ChallengeStatus.UNVERIFIED,
ttl: challengeData.expiresAt,
txHash: challengeData.txHash,
txXDR: challengeData.xdr,
} as NewChallenge);

return await input;
} catch (error) {
logAndThrow(new E.FAILED_TO_STORE_CHALLENGE_IN_DATABASE(error));
}
return await input;
} catch (error) {
span.addEvent("db_store_failed", {
"error.message": error instanceof Error ? error.message : String(error),
});
logAndThrow(new E.FAILED_TO_STORE_CHALLENGE_IN_DATABASE(error));
}
});
},
{
name: "CreateChallengeDB",
Expand Down
47 changes: 28 additions & 19 deletions src/core/service/auth/challenge/store/create-challenge-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,40 @@ import { logAndThrow } from "@/utils/error/log-and-throw.ts";
import * as E from "@/core/service/auth/challenge/store/error.ts";
import { isDefined } from "@/utils/type-guards/is-defined.ts";
import { assertOrThrow } from "@/utils/error/assert-or-throw.ts";
import { withSpan } from "@/core/tracing.ts";

export const P_CreateChallengeMemory = ProcessEngine.create(
async (input: ChallengeData) => {
const { challengeData } = input;
try {
const existingSession = await sessionManager.getSession(
challengeData.txHash
);
return withSpan("P_CreateChallengeMemory", async (span) => {
const { challengeData } = input;
try {
span.addEvent("checking_existing_session", { "challenge.txHash": challengeData.txHash });
const existingSession = await sessionManager.getSession(
challengeData.txHash
);

assertOrThrow(
!isDefined(existingSession),
new E.SESSION_ALREADY_EXISTS(challengeData.txHash)
);
assertOrThrow(
!isDefined(existingSession),
new E.SESSION_ALREADY_EXISTS(challengeData.txHash)
);

await sessionManager.addSession(
challengeData.txHash,
challengeData.clientAccount,
challengeData.requestId,
challengeData.expiresAt
);
span.addEvent("caching_session");
await sessionManager.addSession(
challengeData.txHash,
challengeData.clientAccount,
challengeData.requestId,
challengeData.expiresAt
);

return await input;
} catch (error) {
logAndThrow(new E.FAILED_TO_CACHE_CHALLENGE_IN_LIVE_SESSIONS(error));
}
span.addEvent("session_cached");
return await input;
} catch (error) {
span.addEvent("memory_cache_failed", {
"error.message": error instanceof Error ? error.message : String(error),
});
logAndThrow(new E.FAILED_TO_CACHE_CHALLENGE_IN_LIVE_SESSIONS(error));
}
});
},
{
name: "CreateChallengeMemory",
Expand Down
Loading
Loading