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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "@moonlight-protocol/provider-platform",
"version": "0.7.8",
"version": "0.7.9",
"license": "MIT",
"exports": "./src/main.ts",
"tasks": {
"serve": "deno run --allow-all --unstable-kv src/main.ts",
"test": "deno test --allow-all --no-check --ignore=src/http/v1/pay/tests src/ && deno test --allow-all --no-check --config src/http/v1/pay/tests/deno.json src/http/v1/pay/tests/",
"test": "deno test --allow-all --no-check --ignore=src/http/v1/pay/tests,src/http/v1/entities/tests src/ && deno test --allow-all --no-check --config src/http/v1/pay/tests/deno.json src/http/v1/pay/tests/ && deno test --allow-all --no-check --config src/http/v1/entities/tests/deno.json src/http/v1/entities/tests/",
"test:integration": "deno test --allow-all --no-check --config tests/deno.json tests/",
"test:unit": "deno test --allow-all --no-check --ignore=src/http/v1/pay/tests src/",
"test:unit": "deno test --allow-all --no-check --ignore=src/http/v1/pay/tests,src/http/v1/entities/tests src/",
"test:pay": "deno test --allow-all --no-check --config src/http/v1/pay/tests/deno.json src/http/v1/pay/tests/",
"test:entities": "deno test --allow-all --no-check --config src/http/v1/entities/tests/deno.json src/http/v1/entities/tests/",
"db:generate": "deno run --env -A --node-modules-dir npm:drizzle-kit generate",
"db:migrate": "deno run --env -A --node-modules-dir npm:drizzle-kit migrate",
"db:push": "deno run --env -A --node-modules-dir npm:drizzle-kit push",
Expand Down Expand Up @@ -47,7 +48,8 @@
},
"test": {
"exclude": [
"src/http/v1/pay/tests/"
"src/http/v1/pay/tests/",
"src/http/v1/entities/tests/"
]
},
"deploy": {
Expand Down
17 changes: 17 additions & 0 deletions src/core/service/auth/challenge/store/attach-entity-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ export const P_AttachEntityStatus = (deps: { log: Logger }) =>
accountPubkey,
);
const entityStatus = approval?.status ?? EntityStatus.UNVERIFIED;

// Record the interaction so unauthorized pubkeys (no row, or an
// existing UNVERIFIED row) become visible to the operator. Skip the
// write for APPROVED/PENDING/BLOCKED entities re-connecting — the
// upsert would no-op anyway, but there is no reason to touch their row.
// Best-effort: a write failure must never break the auth response.
if (!approval || approval.status === EntityStatus.UNVERIFIED) {
try {
await ppEntityApprovalRepository.recordInteraction(
ppPublicKey,
accountPubkey,
);
} catch (err) {
log.error(err, "failed to record entity interaction (connect)");
}
}

span.addEvent("entity_status_attached", {
"entity.status": entityStatus,
"pp.has_kyc_url": String(kycSubmissionUrl !== null),
Expand Down
27 changes: 27 additions & 0 deletions src/core/service/bundle/add-bundle.process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,23 @@ function createSlotBundle(
};
}

/**
* Best-effort record that a known pubkey hit the bundle-submit 403 gate, so the
* unauthorized submitter becomes visible to the operator. Never throws — the
* 403 rejection must still fire regardless of whether this write succeeds.
*/
async function recordRejectedSubmitter(
ppPublicKey: string,
accountPubkey: string,
log: Logger,
): Promise<void> {
try {
await ppApprovalRepository.recordInteraction(ppPublicKey, accountPubkey);
} catch (err) {
log.error(err, "failed to record rejected submitter interaction");
}
}

// ========== MAIN PROCESS ==========

export const P_AddOperationsBundle = (deps: { log: Logger }) =>
Expand Down Expand Up @@ -352,6 +369,11 @@ export const P_AddOperationsBundle = (deps: { log: Logger }) =>
);
if (!submitterAccount) {
log.event("submitter has no account; reject");
await recordRejectedSubmitter(
ppPublicKey,
userSession.accountId,
log,
);
throw new E.SUBMITTER_NOT_APPROVED(userSession.accountId);
}
const approval = await ppApprovalRepository.findByPpAndAccount(
Expand All @@ -360,6 +382,11 @@ export const P_AddOperationsBundle = (deps: { log: Logger }) =>
);
if (!approval || approval.status !== EntityStatus.APPROVED) {
log.event("submitter entity not approved for this PP");
await recordRejectedSubmitter(
ppPublicKey,
userSession.accountId,
log,
);
throw new E.SUBMITTER_NOT_APPROVED(userSession.accountId);
}
// Identity (name / jurisdictions) still lives on the global entity
Expand Down
50 changes: 50 additions & 0 deletions src/http/v1/entities/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { type Context, Status } from "@oak/oak";
import { drizzleClient } from "@/persistence/drizzle/config.ts";
import { PpEntityApprovalRepository } from "@/persistence/drizzle/repository/pp-entity-approval.repository.ts";
import type { PaymentProvider } from "@/persistence/drizzle/entity/pp.entity.ts";
import type { Logger } from "@/utils/logger/index.ts";

let approvalRepo = new PpEntityApprovalRepository(drizzleClient);

/** Test-only seam to inject a repo backed by the PGlite test DB. */
export function setEntitiesRepoForTests(
repo: PpEntityApprovalRepository,
): void {
approvalRepo = repo;
}

/**
* GET /api/v1/providers/:ppPublicKey/entities
*
* Operator view of every entity that has interacted with this PP: the ones
* approved via KYC self-serve plus the unauthorized pubkeys recorded at the
* SEP-10 connect + bundle-submit-403 gates. Ownership is enforced upstream by
* requirePpOwnership (PP comes from ctx.state.pp). Newest interaction first.
*/
export function handleGetEntities(
deps: { log: Logger },
): (ctx: Context) => Promise<void> {
const log = deps.log.scope("getEntities");

return async (ctx) => {
log.info("getEntities");
const pp = ctx.state.pp as PaymentProvider;

log.event("listing entity interactions for PP");
const rows = await approvalRepo.listByPp(pp.publicKey);
log.debug("entityCount", rows.length);

ctx.response.status = Status.OK;
ctx.response.body = {
data: rows.map((row) => ({
pubkey: row.accountPubkey,
status: row.status,
name: row.name,
jurisdictions: row.jurisdictions,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
})),
};
log.event("entities response assembled");
};
}
45 changes: 45 additions & 0 deletions src/http/v1/entities/tests/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"compilerOptions": {
"strict": true
},
"nodeModulesDir": "auto",
"imports": {
"@/": "../../../../../src/",

"@/persistence/drizzle/config.ts": "./pglite_db.ts",

"@electric-sql/pglite": "npm:@electric-sql/pglite@^0.2.17",

"@colibri/core": "jsr:@colibri/core@^0.20.2",
"@stellar/stellar-sdk": "npm:@stellar/stellar-sdk@^15.0.1",
"@stellar/stellar-sdk/contract": "npm:@stellar/stellar-sdk@^15.0.1/contract",
"@drizzle-team/brocli": "npm:@drizzle-team/brocli@^0.11.0",
"@fifo/convee": "jsr:@fifo/convee@^0.10.0",
"@moonlight/moonlight-sdk": "jsr:@moonlight/moonlight-sdk@^0.8.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/dotenv": "jsr:@std/dotenv@^0.225.6",
"@zaubrik/djwt": "jsr:@zaubrik/djwt@^3.0.2",
"drizzle-kit": "npm:drizzle-kit@^0.31.6",
"drizzle-orm": "npm:drizzle-orm@^0.44.7",
"drizzle-orm/pg-core": "npm:drizzle-orm@^0.44.7/pg-core",
"drizzle-orm/pglite": "npm:drizzle-orm@^0.44.7/pglite",
"drizzle-orm/postgres-js": "npm:drizzle-orm@^0.44.7/postgres-js",
"postgres": "npm:postgres@^3.4.7",
"@noble/curves": "jsr:@noble/curves@^1.8.0",
"@noble/curves/p256": "jsr:@noble/curves@^1.8.0/p256",
"@noble/curves/abstract/modular": "jsr:@noble/curves@^1.8.0/abstract/modular",
"@noble/hashes": "jsr:@noble/hashes@^1.6.1",
"@noble/hashes/sha256": "jsr:@noble/hashes@^1.6.1/sha256",
"@noble/hashes/hkdf": "jsr:@noble/hashes@^1.6.1/hkdf",
"tslib": "npm:tslib@2.5.0",
"stellar-sdk": "npm:@stellar/stellar-sdk@^15.0.1",
"stellar-sdk/rpc": "npm:@stellar/stellar-sdk@^15.0.1/rpc",
"buffer": "npm:buffer@^6.0.3",
"asn1js": "npm:asn1js@3.0.5",
"@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",
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0"
}
}
Loading
Loading