From 3bf6bcb6b580715ea71aaf66f52b1cf62723271c Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:38:30 +0100
Subject: [PATCH 01/23] test: require OBP-API calls on v5.1.0 or newer across
the app stack
---
.../src/lib/server/obp/obpApiVersion.test.ts | 76 +++++++++++++++++++
1 file changed, 76 insertions(+)
create mode 100644 packages/shared/src/lib/server/obp/obpApiVersion.test.ts
diff --git a/packages/shared/src/lib/server/obp/obpApiVersion.test.ts b/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
new file mode 100644
index 00000000..cbab5681
--- /dev/null
+++ b/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
@@ -0,0 +1,76 @@
+import { describe, expect, it } from 'vitest';
+import { readdirSync, readFileSync, statSync } from 'node:fs';
+import { join, relative, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+// Every OBP-API call in the app stack uses v5.1.0 or newer, so a deployment can disable
+// the older API versions. Calls that have no equivalent in v5.1.0 are listed below with
+// the reason; anything else below v5.1.0 fails this test.
+
+const REPO_ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)), '../../../../../..');
+const SOURCE_ROOTS = ['apps', 'packages'];
+const SKIP_DIRS = new Set(['node_modules', '.svelte-kit', 'build', 'dist', 'test-results', 'docs']);
+const EXTENSIONS = ['.ts', '.js', '.mjs', '.cjs', '.svelte'];
+const OLD_VERSION = /\/obp\/(v(?:[1-4])\.\d+\.\d+|v5\.0\.0)\//g;
+
+/** file (repo-relative) + path fragment -> why it is still below v5.1.0 */
+const NO_V510_EQUIVALENT: Record = {
+ 'apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts|/obp/v3.0.0/users/username/':
+ 'GET users/username/USERNAME is excluded from v5.1.0; v5.1.0 needs a provider: users/provider/PROVIDER/username/USERNAME',
+ 'apps/cohort/scripts/seed-demo-data.mjs|/obp/v3.0.0/users/username/':
+ 'GET users/username/USERNAME is excluded from v5.1.0; v5.1.0 needs a provider: users/provider/PROVIDER/username/USERNAME',
+ 'apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts|/obp/v4.0.0/banks/${space()}/customers?':
+ 'v4.0.0 filters customers by attribute; v5.1.0 serves getCustomersAtOneBank without attribute filtering',
+ 'apps/cohort/scripts/seed-demo-data.mjs|/obp/v4.0.0/banks/${space}/customers?':
+ 'v4.0.0 filters customers by attribute; v5.1.0 serves getCustomersAtOneBank without attribute filtering'
+};
+
+function sourceFiles(dir: string): string[] {
+ const out: string[] = [];
+ for (const name of readdirSync(dir)) {
+ if (SKIP_DIRS.has(name) || name.startsWith('.')) continue;
+ const path = join(dir, name);
+ if (statSync(path).isDirectory()) out.push(...sourceFiles(path));
+ else if (EXTENSIONS.some((ext) => name.endsWith(ext)) && !/\.(test|spec)\.[a-z]+$/.test(name))
+ out.push(path);
+ }
+ return out;
+}
+
+function callsBelowV510(): string[] {
+ const found: string[] = [];
+ for (const root of SOURCE_ROOTS) {
+ for (const file of sourceFiles(join(REPO_ROOT, root))) {
+ const rel = relative(REPO_ROOT, file);
+ readFileSync(file, 'utf8')
+ .split('\n')
+ .forEach((line, i) => {
+ for (const match of line.matchAll(OLD_VERSION)) {
+ const allowed = Object.keys(NO_V510_EQUIVALENT).some((key) => {
+ const [keyFile, fragment] = key.split('|');
+ return keyFile === rel && line.includes(fragment);
+ });
+ if (!allowed) found.push(`${rel}:${i + 1} ${match[0]}`);
+ }
+ });
+ }
+ }
+ return found;
+}
+
+describe('OBP-API version used by the app stack', () => {
+ it('finds the app sources', () => {
+ expect(sourceFiles(join(REPO_ROOT, 'apps')).length).toBeGreaterThan(100);
+ });
+
+ it('calls OBP-API on v5.1.0 or newer', () => {
+ expect(callsBelowV510()).toEqual([]);
+ });
+
+ it('keeps the list of calls without a v5.1.0 equivalent current', () => {
+ for (const key of Object.keys(NO_V510_EQUIVALENT)) {
+ const [file, fragment] = key.split('|');
+ expect(readFileSync(join(REPO_ROOT, file), 'utf8'), key).toContain(fragment);
+ }
+ });
+});
From 4ace28f8a5f3aedf7900c06e12e7bb7f0981255a Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:08 +0100
Subject: [PATCH 02/23] refactor(api-manager): call method routing endpoints on
OBP-API v5.1.0
---
apps/api-manager/src/lib/server/activeConnector.ts | 2 +-
.../routes/backend/integration/method-routings/+server.ts | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/apps/api-manager/src/lib/server/activeConnector.ts b/apps/api-manager/src/lib/server/activeConnector.ts
index 71b3be55..d6da88c6 100644
--- a/apps/api-manager/src/lib/server/activeConnector.ts
+++ b/apps/api-manager/src/lib/server/activeConnector.ts
@@ -32,7 +32,7 @@ const logger = createLogger("ActiveConnector");
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes: the prop only changes on a restart, routings can change
const CONFIG_PROPS_PATH = "/obp/v6.0.0/management/config-props";
-const METHOD_ROUTINGS_PATH = "/obp/v3.1.0/management/method_routings";
+const METHOD_ROUTINGS_PATH = "/obp/v5.1.0/management/method_routings";
/** The connector star falls back to when no Method Routing matches a method. */
export const STAR_FALLBACK_CONNECTOR = "mapped";
diff --git a/apps/api-manager/src/routes/backend/integration/method-routings/+server.ts b/apps/api-manager/src/routes/backend/integration/method-routings/+server.ts
index 11af74c7..66f7521f 100644
--- a/apps/api-manager/src/routes/backend/integration/method-routings/+server.ts
+++ b/apps/api-manager/src/routes/backend/integration/method-routings/+server.ts
@@ -49,7 +49,7 @@ export const GET: RequestHandler = async ({ locals, url }) => {
try {
const queryString = url.searchParams.toString();
- const endpoint = `/obp/v3.1.0/management/method_routings${queryString ? `?${queryString}` : ""}`;
+ const endpoint = `/obp/v5.1.0/management/method_routings${queryString ? `?${queryString}` : ""}`;
logger.info(`Request: ${endpoint}`);
let response;
@@ -183,7 +183,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
logger.info("Transformed body for OBP:", obpBody);
- const endpoint = `/obp/v3.1.0/management/method_routings`;
+ const endpoint = `/obp/v5.1.0/management/method_routings`;
logger.info(`Request: POST ${endpoint}`);
const response = await obp_requests.post(endpoint, obpBody, accessToken);
@@ -248,7 +248,7 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
logger.info("Transformed body for OBP:", obpBody);
- const endpoint = `/obp/v3.1.0/management/method_routings/${methodRoutingId}`;
+ const endpoint = `/obp/v5.1.0/management/method_routings/${methodRoutingId}`;
logger.info(`Request: PUT ${endpoint}`);
const response = await obp_requests.put(endpoint, obpBody, accessToken);
@@ -284,7 +284,7 @@ export const DELETE: RequestHandler = async ({ locals, url }) => {
return json({ message: "method_routing_id is required", code: 400 }, { status: 400 });
}
try {
- await obp_requests.delete(`/obp/v3.1.0/management/method_routings/${encodeURIComponent(methodRoutingId)}`, accessToken);
+ await obp_requests.delete(`/obp/v5.1.0/management/method_routings/${encodeURIComponent(methodRoutingId)}`, accessToken);
logger.info(`Deleted method routing ${methodRoutingId}`);
return json({ deleted: methodRoutingId });
} catch (err) {
From 9094ca92fa361bb6810480d659d609ce18389fd8 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:08 +0100
Subject: [PATCH 03/23] refactor(api-manager): call endpoint mapping endpoints
on OBP-API v5.1.0
---
.../src/lib/components/DynamicEndpointOperationsTable.svelte | 4 ++--
.../dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts | 2 +-
.../(protected)/dynamic-endpoints/system/[id]/+page.server.ts | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte b/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte
index 6ee538c1..8b1a8590 100644
--- a/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte
+++ b/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte
@@ -62,8 +62,8 @@
// Endpoint-mapping CRUD base: bank-level when bankId is set, otherwise system.
const mappingsBase = $derived(
bankId
- ? `/proxy/obp/v4.0.0/management/banks/${encodeURIComponent(bankId)}/endpoint-mappings`
- : `/proxy/obp/v4.0.0/management/endpoint-mappings`,
+ ? `/proxy/obp/v5.1.0/management/banks/${encodeURIComponent(bankId)}/endpoint-mappings`
+ : `/proxy/obp/v5.1.0/management/endpoint-mappings`,
);
// Dynamic endpoints are served under /obp/dynamic-endpoint/ (no API version, no /banks/ segment
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts
index 0319b59c..692d5e13 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts
@@ -78,7 +78,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
}> = [];
try {
const resp = await obp_requests.get(
- `/obp/v4.0.0/management/banks/${bank_id}/endpoint-mappings`,
+ `/obp/v5.1.0/management/banks/${bank_id}/endpoint-mappings`,
accessToken,
);
mappings = resp?.["endpoint-mappings"] || [];
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts
index 9fbb4bbd..c2616853 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts
@@ -76,7 +76,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
}> = [];
try {
const resp = await obp_requests.get(
- `/obp/v4.0.0/management/endpoint-mappings`,
+ `/obp/v5.1.0/management/endpoint-mappings`,
accessToken,
);
mappings = resp?.["endpoint-mappings"] || [];
From f61e0dfa5df81c03553bbc70b3b93fa865c15832 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:08 +0100
Subject: [PATCH 04/23] refactor(api-manager): call JSON schema validation
endpoints on OBP-API v5.1.0
---
.../src/lib/components/DynamicEndpointOperationsTable.svelte | 4 ++--
apps/api-manager/src/lib/services/jsonSchemaValidations.ts | 2 +-
.../dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts | 2 +-
.../(protected)/dynamic-endpoints/system/[id]/+page.server.ts | 2 +-
.../(protected)/json-schema-validations/+page.server.ts | 2 +-
.../json-schema-validations/[operation_id]/+page.server.ts | 2 +-
6 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte b/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte
index 8b1a8590..06ee6896 100644
--- a/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte
+++ b/apps/api-manager/src/lib/components/DynamicEndpointOperationsTable.svelte
@@ -445,7 +445,7 @@
try {
const response = await fetch(
- `/proxy/obp/v4.0.0/management/json-schema-validations/${encodeURIComponent(op.operationId)}`,
+ `/proxy/obp/v5.1.0/management/json-schema-validations/${encodeURIComponent(op.operationId)}`,
{
method,
headers: { "Content-Type": "application/json" },
@@ -479,7 +479,7 @@
try {
const response = await fetch(
- `/proxy/obp/v4.0.0/management/json-schema-validations/${encodeURIComponent(op.operationId)}`,
+ `/proxy/obp/v5.1.0/management/json-schema-validations/${encodeURIComponent(op.operationId)}`,
{ method: "DELETE", credentials: "include" },
);
diff --git a/apps/api-manager/src/lib/services/jsonSchemaValidations.ts b/apps/api-manager/src/lib/services/jsonSchemaValidations.ts
index fb95807a..81287d48 100644
--- a/apps/api-manager/src/lib/services/jsonSchemaValidations.ts
+++ b/apps/api-manager/src/lib/services/jsonSchemaValidations.ts
@@ -39,7 +39,7 @@ export interface OperationChoice {
summary: string;
}
-const BASE = "/proxy/obp/v4.0.0/management/json-schema-validations";
+const BASE = "/proxy/obp/v5.1.0/management/json-schema-validations";
export const EXAMPLE_SCHEMA = `{
"$schema": "http://json-schema.org/draft-07/schema",
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts
index 692d5e13..abf9925f 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/bank/[bank_id]/[id]/+page.server.ts
@@ -60,7 +60,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
let validations: Array<{ operation_id: string; json_schema: any }> = [];
try {
const resp = await obp_requests.get(
- `/obp/v4.0.0/management/json-schema-validations`,
+ `/obp/v5.1.0/management/json-schema-validations`,
accessToken,
);
validations = resp?.json_schema_validations || [];
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts
index c2616853..5d25448d 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/dynamic-endpoints/system/[id]/+page.server.ts
@@ -56,7 +56,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
let validations: Array<{ operation_id: string; json_schema: any }> = [];
try {
const resp = await obp_requests.get(
- `/obp/v4.0.0/management/json-schema-validations`,
+ `/obp/v5.1.0/management/json-schema-validations`,
accessToken,
);
validations = resp?.json_schema_validations || [];
diff --git a/apps/api-manager/src/routes/(protected)/json-schema-validations/+page.server.ts b/apps/api-manager/src/routes/(protected)/json-schema-validations/+page.server.ts
index 51451236..84dd2362 100644
--- a/apps/api-manager/src/routes/(protected)/json-schema-validations/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/json-schema-validations/+page.server.ts
@@ -34,7 +34,7 @@ export const load: PageServerLoad = async ({ locals }) => {
let validations: JsonSchemaValidation[] = [];
let loadError: string | null = null;
try {
- const resp = await obp_requests.get("/obp/v4.0.0/management/json-schema-validations", token);
+ const resp = await obp_requests.get("/obp/v5.1.0/management/json-schema-validations", token);
validations = resp?.json_schema_validations ?? [];
} catch (e) {
logger.error("Could not load JSON Schema Validations:", e);
diff --git a/apps/api-manager/src/routes/(protected)/json-schema-validations/[operation_id]/+page.server.ts b/apps/api-manager/src/routes/(protected)/json-schema-validations/[operation_id]/+page.server.ts
index 7fa81fe5..0fc4bbb2 100644
--- a/apps/api-manager/src/routes/(protected)/json-schema-validations/[operation_id]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/json-schema-validations/[operation_id]/+page.server.ts
@@ -31,7 +31,7 @@ export const load: PageServerLoad = async ({ locals, params }) => {
const operationId = params.operation_id;
let validation: any;
try {
- validation = await obp_requests.get(`/obp/v4.0.0/management/json-schema-validations/${encodeURIComponent(operationId)}`, token);
+ validation = await obp_requests.get(`/obp/v5.1.0/management/json-schema-validations/${encodeURIComponent(operationId)}`, token);
} catch (e) {
throw error(404, `No JSON Schema Validation for ${operationId}: ${e instanceof Error ? e.message : String(e)}`);
}
From 5c044ad1e00607989fb1550f86e47675f8ab60ba Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:08 +0100
Subject: [PATCH 05/23] refactor(api-manager): call dynamic resource doc
endpoints on OBP-API v5.1.0
---
.../src/lib/components/DynamicResourceDocForm.svelte | 2 +-
.../(protected)/dynamic-resource-docs/system/+page.server.ts | 2 +-
.../(protected)/dynamic-resource-docs/system/+page.svelte | 2 +-
.../dynamic-resource-docs/system/[id]/+page.server.ts | 2 +-
.../dynamic-resource-docs/system/[id]/+page.svelte | 4 ++--
.../dynamic-resource-docs/system/create/+page.svelte | 2 +-
6 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/apps/api-manager/src/lib/components/DynamicResourceDocForm.svelte b/apps/api-manager/src/lib/components/DynamicResourceDocForm.svelte
index 587c5d2d..3e3f2f42 100644
--- a/apps/api-manager/src/lib/components/DynamicResourceDocForm.svelte
+++ b/apps/api-manager/src/lib/components/DynamicResourceDocForm.svelte
@@ -548,7 +548,7 @@
submitError = null;
try {
const response = await fetch(
- "/proxy/obp/v4.0.0/management/dynamic-resource-docs/endpoint-code",
+ "/proxy/obp/v5.1.0/management/dynamic-resource-docs/endpoint-code",
{
method: "POST",
headers: { "Content-Type": "application/json" },
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.server.ts b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.server.ts
index 163f8987..5092af3c 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.server.ts
@@ -39,7 +39,7 @@ export const load: PageServerLoad = async ({ locals }) => {
try {
const resp = await obp_requests.get(
- `/obp/v4.0.0/management/dynamic-resource-docs`,
+ `/obp/v5.1.0/management/dynamic-resource-docs`,
accessToken,
);
const docs = resp?.["dynamic-resource-docs"] || [];
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.svelte b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.svelte
index 77701826..21617c1c 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/+page.svelte
@@ -69,7 +69,7 @@
successMessage = null;
try {
const response = await fetch(
- `/proxy/obp/v4.0.0/management/dynamic-resource-docs/${encodeURIComponent(id)}`,
+ `/proxy/obp/v5.1.0/management/dynamic-resource-docs/${encodeURIComponent(id)}`,
{ method: "DELETE", credentials: "include" },
);
if (!response.ok) {
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.server.ts b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.server.ts
index ecc42a90..049b8252 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.server.ts
@@ -103,7 +103,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
try {
const doc = await obp_requests.get(
- `/obp/v4.0.0/management/dynamic-resource-docs/${encodeURIComponent(id)}`,
+ `/obp/v5.1.0/management/dynamic-resource-docs/${encodeURIComponent(id)}`,
accessToken,
);
if (!doc) {
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.svelte b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.svelte
index 79ac9ce2..30bc46f3 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/[id]/+page.svelte
@@ -67,7 +67,7 @@
if (!docId) throw new Error("Missing doc id");
successMessage = null;
const response = await fetch(
- `/proxy/obp/v4.0.0/management/dynamic-resource-docs/${encodeURIComponent(docId)}`,
+ `/proxy/obp/v5.1.0/management/dynamic-resource-docs/${encodeURIComponent(docId)}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
@@ -140,7 +140,7 @@
isDeleting = true;
try {
const response = await fetch(
- `/proxy/obp/v4.0.0/management/dynamic-resource-docs/${encodeURIComponent(docId)}`,
+ `/proxy/obp/v5.1.0/management/dynamic-resource-docs/${encodeURIComponent(docId)}`,
{ method: "DELETE", credentials: "include" },
);
if (!response.ok) {
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/create/+page.svelte b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/create/+page.svelte
index 9ac517ca..83fc44b8 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/create/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/dynamic-resource-docs/system/create/+page.svelte
@@ -58,7 +58,7 @@
async function handleSubmit(values: DynamicResourceDocFormValues) {
const response = await fetch(
- `/proxy/obp/v4.0.0/management/dynamic-resource-docs`,
+ `/proxy/obp/v5.1.0/management/dynamic-resource-docs`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
From 780e25bbb383f89d897a994dd10aa822d95e8861 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:08 +0100
Subject: [PATCH 06/23] refactor(api-manager): call product and product
attribute endpoints on OBP-API v5.1.0
---
.../[bank_id]/[account_id]/[view_id]/+page.svelte | 2 +-
.../financial/[bank_id]/[product_code]/+page.svelte | 2 +-
.../financial/[bank_id]/[product_code]/edit/+page.svelte | 8 ++++----
.../(protected)/products/financial/create/+page.svelte | 4 ++--
4 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/+page.svelte b/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/+page.svelte
index 7d0e964e..92f8c63e 100644
--- a/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/+page.svelte
@@ -437,7 +437,7 @@
try {
const res = await trackedFetch(
- `/proxy/obp/v3.1.0/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/products/${encodeURIComponent(productCode)}/attribute`,
+ `/proxy/obp/v5.1.0/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/products/${encodeURIComponent(productCode)}/attribute`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
diff --git a/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/+page.svelte b/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/+page.svelte
index 7de6925e..5c1f0818 100644
--- a/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/+page.svelte
@@ -67,7 +67,7 @@
try {
const [productResponse, tagsResponse] = await Promise.all([
trackedFetch(
- `/proxy/obp/v4.0.0/banks/${bankId}/products/${encodeURIComponent(productCode)}`,
+ `/proxy/obp/v5.1.0/banks/${bankId}/products/${encodeURIComponent(productCode)}`,
),
trackedFetch(
`/proxy/obp/v6.0.0/banks/${bankId}/products/${encodeURIComponent(productCode)}/tags`,
diff --git a/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/edit/+page.svelte b/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/edit/+page.svelte
index 5bb6f78d..ece69d2c 100644
--- a/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/edit/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/products/financial/[bank_id]/[product_code]/edit/+page.svelte
@@ -79,7 +79,7 @@
try {
const [productResponse, tagsResponse] = await Promise.all([
trackedFetch(
- `/proxy/obp/v4.0.0/banks/${bankId}/products/${encodeURIComponent(productCode)}`,
+ `/proxy/obp/v5.1.0/banks/${bankId}/products/${encodeURIComponent(productCode)}`,
),
trackedFetch(
`/proxy/obp/v6.0.0/banks/${bankId}/products/${encodeURIComponent(productCode)}/tags`,
@@ -157,7 +157,7 @@
isSubmitting = true;
try {
const response = await trackedFetch(
- `/proxy/obp/v5.0.0/banks/${bankId}/products/${encodeURIComponent(productCode)}`,
+ `/proxy/obp/v5.1.0/banks/${bankId}/products/${encodeURIComponent(productCode)}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
@@ -195,7 +195,7 @@
let attrResponse: Response;
if (attr.product_attribute_id) {
attrResponse = await trackedFetch(
- `/proxy/obp/v4.0.0/banks/${bankId}/products/${encodeURIComponent(productCode)}/attributes/${attr.product_attribute_id}`,
+ `/proxy/obp/v5.1.0/banks/${bankId}/products/${encodeURIComponent(productCode)}/attributes/${attr.product_attribute_id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
@@ -209,7 +209,7 @@
);
} else {
attrResponse = await trackedFetch(
- `/proxy/obp/v4.0.0/banks/${bankId}/products/${encodeURIComponent(productCode)}/attribute`,
+ `/proxy/obp/v5.1.0/banks/${bankId}/products/${encodeURIComponent(productCode)}/attribute`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
diff --git a/apps/api-manager/src/routes/(protected)/products/financial/create/+page.svelte b/apps/api-manager/src/routes/(protected)/products/financial/create/+page.svelte
index 84da707b..f6cc4812 100644
--- a/apps/api-manager/src/routes/(protected)/products/financial/create/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/products/financial/create/+page.svelte
@@ -54,7 +54,7 @@
async function putProduct(bankId: string, code: string) {
const response = await trackedFetch(
- `/proxy/obp/v5.0.0/banks/${bankId}/products/${encodeURIComponent(code)}`,
+ `/proxy/obp/v5.1.0/banks/${bankId}/products/${encodeURIComponent(code)}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
@@ -99,7 +99,7 @@
async function postAttribute(bankId: string, code: string, attr: CustomAttribute) {
const response = await trackedFetch(
- `/proxy/obp/v4.0.0/banks/${bankId}/products/${encodeURIComponent(code)}/attribute`,
+ `/proxy/obp/v5.1.0/banks/${bankId}/products/${encodeURIComponent(code)}/attribute`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
From af3f78d9e2ad43fa6e7d6426f0ee05914cc792ab Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 07/23] refactor: call customer, customer link and
account-minimal endpoints on OBP-API v5.1.0
---
.../customers/[bank_id]/[customer_id]/+page.svelte | 2 +-
.../src/routes/(protected)/customers/graph/+page.svelte | 2 +-
.../src/routes/(protected)/mandates/+page.svelte | 2 +-
.../[account_id]/customer-account-links/+server.ts | 2 +-
.../[customer_id]/customer-account-links/+server.ts | 2 +-
apps/cohort/scripts/seed-demo-data.mjs | 4 ++--
apps/cohort/src/lib/server/customers.ts | 2 +-
apps/cohort/src/lib/server/participants.ts | 2 +-
.../(protected)/staff/organisations/+page.server.ts | 8 ++++----
9 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/apps/api-manager/src/routes/(protected)/customers/[bank_id]/[customer_id]/+page.svelte b/apps/api-manager/src/routes/(protected)/customers/[bank_id]/[customer_id]/+page.svelte
index be0dcfd5..36608766 100644
--- a/apps/api-manager/src/routes/(protected)/customers/[bank_id]/[customer_id]/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/customers/[bank_id]/[customer_id]/+page.svelte
@@ -148,7 +148,7 @@
try {
const res = await trackedFetch(
- `/proxy/obp/v4.0.0/banks/${encodeURIComponent(bankId)}/customers/${encodeURIComponent(customerId)}/attribute`,
+ `/proxy/obp/v5.1.0/banks/${encodeURIComponent(bankId)}/customers/${encodeURIComponent(customerId)}/attribute`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
diff --git a/apps/api-manager/src/routes/(protected)/customers/graph/+page.svelte b/apps/api-manager/src/routes/(protected)/customers/graph/+page.svelte
index 83de22f9..aeb6df1d 100644
--- a/apps/api-manager/src/routes/(protected)/customers/graph/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/customers/graph/+page.svelte
@@ -146,7 +146,7 @@
try {
const res = await trackedFetch(
- `/proxy/obp/v4.0.0/banks/${encodeURIComponent(bankId)}/user_customer_links/customers/${encodeURIComponent(customerId)}`,
+ `/proxy/obp/v5.1.0/banks/${encodeURIComponent(bankId)}/user_customer_links/customers/${encodeURIComponent(customerId)}`,
);
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
diff --git a/apps/api-manager/src/routes/(protected)/mandates/+page.svelte b/apps/api-manager/src/routes/(protected)/mandates/+page.svelte
index 897e80f2..574915ea 100644
--- a/apps/api-manager/src/routes/(protected)/mandates/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/mandates/+page.svelte
@@ -201,7 +201,7 @@
try {
// First get accounts for this customer
const accountsRes = await trackedFetch(
- `/proxy/obp/v4.0.0/customers/${encodeURIComponent(customerId)}/accounts-minimal`
+ `/proxy/obp/v5.1.0/customers/${encodeURIComponent(customerId)}/accounts-minimal`
);
if (!accountsRes.ok) {
const data = await accountsRes.json().catch(() => ({}));
diff --git a/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/accounts/[account_id]/customer-account-links/+server.ts b/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/accounts/[account_id]/customer-account-links/+server.ts
index 11a5780f..fa2c6ac0 100644
--- a/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/accounts/[account_id]/customer-account-links/+server.ts
+++ b/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/accounts/[account_id]/customer-account-links/+server.ts
@@ -48,7 +48,7 @@ export const GET: RequestHandler = async ({ locals, params }) => {
try {
logger.info(`Fetching customer account links for account: ${account_id} at bank: ${bank_id}`);
- const endpoint = `/obp/v5.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/customer-account-links`;
+ const endpoint = `/obp/v5.1.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/customer-account-links`;
const response = await obp_requests.get(endpoint, accessToken);
const links = response.links || [];
diff --git a/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/customers/[customer_id]/customer-account-links/+server.ts b/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/customers/[customer_id]/customer-account-links/+server.ts
index ab6165e0..5a046016 100644
--- a/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/customers/[customer_id]/customer-account-links/+server.ts
+++ b/apps/api-manager/src/routes/backend/obp/banks/[bank_id]/customers/[customer_id]/customer-account-links/+server.ts
@@ -48,7 +48,7 @@ export const GET: RequestHandler = async ({ locals, params }) => {
try {
logger.info(`Fetching customer account links for customer: ${customer_id} at bank: ${bank_id}`);
- const endpoint = `/obp/v5.0.0/banks/${encodeURIComponent(bank_id)}/customers/${encodeURIComponent(customer_id)}/customer-account-links`;
+ const endpoint = `/obp/v5.1.0/banks/${encodeURIComponent(bank_id)}/customers/${encodeURIComponent(customer_id)}/customer-account-links`;
const response = await obp_requests.get(endpoint, accessToken);
const links = response.links || [];
diff --git a/apps/cohort/scripts/seed-demo-data.mjs b/apps/cohort/scripts/seed-demo-data.mjs
index 0722c9cc..9f6d39ea 100644
--- a/apps/cohort/scripts/seed-demo-data.mjs
+++ b/apps/cohort/scripts/seed-demo-data.mjs
@@ -353,7 +353,7 @@ for (const o of data.organisations ?? []) {
});
const customerId = encodeURIComponent(String(customer?.customer_id ?? ''));
for (const [name, value] of Object.entries({ organisation_kind: o.kind, name_ar: o.name_ar, website: o.website })) {
- if (value) await obp('POST', `/obp/v4.0.0/banks/${space}/customers/${customerId}/attribute`, staff.token, { name, type: 'STRING', value });
+ if (value) await obp('POST', `/obp/v5.1.0/banks/${space}/customers/${customerId}/attribute`, staff.token, { name, type: 'STRING', value });
}
organisations.push(customer);
count('organisations', true);
@@ -484,7 +484,7 @@ for (const s of data.startups ?? []) {
customerId = customers[p.company.legal_name] = String(customer?.customer_id ?? '');
count('companies', true);
if (s.owner === 'startup') {
- await obp('POST', `/obp/v4.0.0/banks/${space}/user_customer_links`, staff.token, { user_id: owner.user_id, customer_id: customerId }).catch((err) =>
+ await obp('POST', `/obp/v5.1.0/banks/${space}/user_customer_links`, staff.token, { user_id: owner.user_id, customer_id: customerId }).catch((err) =>
errors.push(`link ${p.company.legal_name}: ${err.message}`)
);
}
diff --git a/apps/cohort/src/lib/server/customers.ts b/apps/cohort/src/lib/server/customers.ts
index a462f7c6..334d9bd2 100644
--- a/apps/cohort/src/lib/server/customers.ts
+++ b/apps/cohort/src/lib/server/customers.ts
@@ -67,7 +67,7 @@ export async function createCustomerForUser(
);
const customerId = String(created?.customer_id ?? '');
try {
- await obpRequests.post(`/obp/v4.0.0/banks/${space()}/user_customer_links`, { user_id: userId, customer_id: customerId }, token);
+ await obpRequests.post(`/obp/v5.1.0/banks/${space()}/user_customer_links`, { user_id: userId, customer_id: customerId }, token);
} catch (err) {
// The Customer exists but belongs to nobody yet; link it by hand (Create User Customer Link) to reuse it.
logger.error(`Customer ${customerId} was created but not linked to User ${userId}:`, err);
diff --git a/apps/cohort/src/lib/server/participants.ts b/apps/cohort/src/lib/server/participants.ts
index 665c83b1..67585638 100644
--- a/apps/cohort/src/lib/server/participants.ts
+++ b/apps/cohort/src/lib/server/participants.ts
@@ -140,7 +140,7 @@ export async function registerCompany(
token
);
const customerId = String(created?.customer_id ?? '');
- await obpRequests.post(`/obp/v4.0.0/banks/${space}/user_customer_links`, { user_id: userId, customer_id: customerId }, token);
+ await obpRequests.post(`/obp/v5.1.0/banks/${space}/user_customer_links`, { user_id: userId, customer_id: customerId }, token);
// A PUT replaces the record: send its fields (not its own id field) with the customer_id added.
const body: CohortRecord = Object.fromEntries(
Object.entries({ ...found.record, customer_id: customerId }).filter(
diff --git a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
index 7dcbdd86..5b639c02 100644
--- a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
+++ b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
@@ -53,7 +53,7 @@ interface Member {
/** The Users linked to a Customer, with their usernames where the staff member may look Users up. */
async function membersOf(customerId: string, token: string): Promise {
- const response = await obpRequests.get(`/obp/v4.0.0/banks/${space()}/user_customer_links/customers/${encodeURIComponent(customerId)}`, token);
+ const response = await obpRequests.get(`/obp/v5.1.0/banks/${space()}/user_customer_links/customers/${encodeURIComponent(customerId)}`, token);
const links: Record[] = response?.user_customer_links ?? [];
return Promise.all(
links.map(async (l) => {
@@ -123,7 +123,7 @@ export const actions: Actions = {
const customerId = encodeURIComponent(String(customer?.customer_id ?? ''));
const attributes = { [ORGANISATION_ATTRIBUTES.kind]: kind, [ORGANISATION_ATTRIBUTES.name_ar]: str(form, 'name_ar'), [ORGANISATION_ATTRIBUTES.website]: str(form, 'website') };
for (const [name, value] of Object.entries(attributes).filter(([, v]) => v !== '')) {
- await obpRequests.post(`/obp/v4.0.0/banks/${space()}/customers/${customerId}/attribute`, { name, type: 'STRING', value }, token);
+ await obpRequests.post(`/obp/v5.1.0/banks/${space()}/customers/${customerId}/attribute`, { name, type: 'STRING', value }, token);
}
},
{ created: true }
@@ -144,12 +144,12 @@ export const actions: Actions = {
return fail(400, { form: 'member', error: `member_not_found`, detail: messageOf(err) });
}
if (!userId) return fail(400, { form: 'member', error: 'member_not_found' });
- return write('member', () => obpRequests.post(`/obp/v4.0.0/banks/${space()}/user_customer_links`, { user_id: userId, customer_id: customerId }, token), { added: username });
+ return write('member', () => obpRequests.post(`/obp/v5.1.0/banks/${space()}/user_customer_links`, { user_id: userId, customer_id: customerId }, token), { added: username });
},
removeMember: async ({ locals, request }) => {
const linkId = str(await request.formData(), 'link_id');
if (!linkId) return fail(400, { form: 'member', error: 'member_missing' });
- return write('member', () => obpRequests.delete(`/obp/v4.0.0/banks/${space()}/user_customer_links/${encodeURIComponent(linkId)}`, userToken(locals)!), { removed: true });
+ return write('member', () => obpRequests.delete(`/obp/v5.1.0/banks/${space()}/user_customer_links/${encodeURIComponent(linkId)}`, userToken(locals)!), { removed: true });
},
requestRole: async ({ locals, request }) => requestRole(await request.formData(), userToken(locals)!)
};
From 3e989d9c7fb1b02d5ff03585a6c45fd7c722f3aa Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 08/23] refactor: call user lookup endpoints on OBP-API v5.1.0
---
.../src/lib/components/UserSearchPickerWidget.svelte | 2 +-
.../src/routes/(protected)/staff/organisations/+page.server.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/api-manager/src/lib/components/UserSearchPickerWidget.svelte b/apps/api-manager/src/lib/components/UserSearchPickerWidget.svelte
index 2a7b43d5..f068ca73 100644
--- a/apps/api-manager/src/lib/components/UserSearchPickerWidget.svelte
+++ b/apps/api-manager/src/lib/components/UserSearchPickerWidget.svelte
@@ -123,7 +123,7 @@
try {
let url: string;
if (type === "email") {
- url = `/proxy/obp/v4.0.0/users/email/${encodeURIComponent(trimmed)}/terminator`;
+ url = `/proxy/obp/v5.1.0/users/email/${encodeURIComponent(trimmed)}/terminator`;
} else if (type === "userid") {
url = `/proxy/obp/v6.0.0/users/user-id/${encodeURIComponent(trimmed)}`;
} else {
diff --git a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
index 5b639c02..2e39243a 100644
--- a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
+++ b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
@@ -58,7 +58,7 @@ async function membersOf(customerId: string, token: string): Promise {
return Promise.all(
links.map(async (l) => {
const userId = String(l.user_id ?? '');
- const user = await obpRequests.get(`/obp/v3.0.0/users/user_id/${encodeURIComponent(userId)}`, token).catch(() => null);
+ const user = await obpRequests.get(`/obp/v5.1.0/users/user_id/${encodeURIComponent(userId)}`, token).catch(() => null);
return { link_id: String(l.user_customer_link_id ?? ''), user_id: userId, username: String(user?.username ?? '') };
})
);
From 1425d78bde8928990ba5c390e6228e05942c78e3 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 09/23] refactor: call user invitation endpoints on OBP-API
v5.1.0
---
.../src/routes/(protected)/user-invitations/+page.svelte | 2 +-
.../src/routes/backend/user-invitations/create/+server.ts | 2 +-
apps/portal/src/routes/user-invitation/+page.server.ts | 8 ++++----
.../portal/src/routes/user-invitation/page.server.test.ts | 4 ++--
4 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/apps/api-manager/src/routes/(protected)/user-invitations/+page.svelte b/apps/api-manager/src/routes/(protected)/user-invitations/+page.svelte
index ab1191ce..795013c1 100644
--- a/apps/api-manager/src/routes/(protected)/user-invitations/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/user-invitations/+page.svelte
@@ -62,7 +62,7 @@
try {
const response = await trackedFetch(
- `/proxy/obp/v4.0.0/banks/${encodeURIComponent(selectedBankId)}/user-invitations`,
+ `/proxy/obp/v5.1.0/banks/${encodeURIComponent(selectedBankId)}/user-invitations`,
);
if (!response.ok) {
diff --git a/apps/api-manager/src/routes/backend/user-invitations/create/+server.ts b/apps/api-manager/src/routes/backend/user-invitations/create/+server.ts
index dc87bbf4..b7b370e7 100644
--- a/apps/api-manager/src/routes/backend/user-invitations/create/+server.ts
+++ b/apps/api-manager/src/routes/backend/user-invitations/create/+server.ts
@@ -52,7 +52,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
}
logger.info("=== CREATE USER INVITATION ===");
- const endpoint = `/obp/v4.0.0/banks/${bank_id}/user-invitation`;
+ const endpoint = `/obp/v5.1.0/banks/${bank_id}/user-invitation`;
logger.info(`Request: POST ${endpoint}`);
logger.info(`Data:`, { first_name, last_name, email, company, country, purpose });
diff --git a/apps/portal/src/routes/user-invitation/+page.server.ts b/apps/portal/src/routes/user-invitation/+page.server.ts
index c2f6d2ac..7e7d4faf 100644
--- a/apps/portal/src/routes/user-invitation/+page.server.ts
+++ b/apps/portal/src/routes/user-invitation/+page.server.ts
@@ -73,13 +73,13 @@ export const actions = {
try {
// Call OBP API to validate the invitation
- // POST /obp/v4.0.0/banks/{BANK_ID}/user-invitations
+ // POST /obp/v5.1.0/banks/{BANK_ID}/user-invitations
const requestBody: OBPUserInvitationValidateRequestBody = {
secret_key: parseInt(secretKey)
};
const response = await obp_requests.post(
- `/obp/v4.0.0/banks/${effectiveBankId}/user-invitations`,
+ `/obp/v5.1.0/banks/${effectiveBankId}/user-invitations`,
requestBody
) as OBPUserInvitation;
@@ -204,7 +204,7 @@ export const actions = {
try {
// Option B: Create user account directly via REST API
- // POST /obp/v4.0.0/banks/{BANK_ID}/user-invitation
+ // POST /obp/v5.1.0/banks/{BANK_ID}/user-invitation
const requestBody: OBPUserInvitationAcceptRequestBody = {
secret_key: parseInt(secretKey),
username: username.trim(),
@@ -223,7 +223,7 @@ export const actions = {
});
const response = await obp_requests.post(
- `/obp/v4.0.0/banks/${effectiveBankId}/user-invitation`,
+ `/obp/v5.1.0/banks/${effectiveBankId}/user-invitation`,
requestBody
);
diff --git a/apps/portal/src/routes/user-invitation/page.server.test.ts b/apps/portal/src/routes/user-invitation/page.server.test.ts
index fa577418..48b4f933 100644
--- a/apps/portal/src/routes/user-invitation/page.server.test.ts
+++ b/apps/portal/src/routes/user-invitation/page.server.test.ts
@@ -65,7 +65,7 @@ describe('user-invitation accept action', () => {
actions.accept({ request: createMockRequest(form) } as never);
// The regression this file exists for. OBP-API serves
- // /obp/v4.0.0/banks/{BANK_ID}/user-invitation as POST only -- there is no PUT on that
+ // /obp/v5.1.0/banks/{BANK_ID}/user-invitation as POST only -- there is no PUT on that
// path at any version, so the previous `obp_requests.put` could only ever 404 and
// accepting an invitation could never complete. Asserting the verb, not just the URL,
// is what makes this test able to fail if it regresses.
@@ -80,7 +80,7 @@ describe('user-invitation accept action', () => {
expect(obp_requests.put).not.toHaveBeenCalled();
expect(obp_requests.post).toHaveBeenCalledWith(
- '/obp/v4.0.0/banks/gh.29.uk/user-invitation',
+ '/obp/v5.1.0/banks/gh.29.uk/user-invitation',
expect.objectContaining({
secret_key: 12345,
username: 'inviteduser',
From 4ced29a6ea6a8295dcf865f215283515ea985013 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 10/23] refactor: call consent challenge and consent request
endpoints on OBP-API v5.1.0
---
.../routes/backend/app-studio/consent/challenge/+server.ts | 2 +-
.../(protected)/confirm-vrp-consent-request/+page.server.ts | 4 ++--
.../routes/(protected)/confirm-vrp-consent/+page.server.ts | 2 +-
.../(protected)/obp-consent-request-sca/+page.server.ts | 2 +-
.../routes/(protected)/obp-consent-request/+page.server.ts | 4 ++--
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/apps/api-manager/src/routes/backend/app-studio/consent/challenge/+server.ts b/apps/api-manager/src/routes/backend/app-studio/consent/challenge/+server.ts
index 668cb0e0..761106a5 100644
--- a/apps/api-manager/src/routes/backend/app-studio/consent/challenge/+server.ts
+++ b/apps/api-manager/src/routes/backend/app-studio/consent/challenge/+server.ts
@@ -52,7 +52,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
try {
const result = await obp_requests.post(
- `/obp/v3.1.0/banks/${encodeURIComponent(bankId)}/consents/${encodeURIComponent(consentId)}/challenge`,
+ `/obp/v5.1.0/banks/${encodeURIComponent(bankId)}/consents/${encodeURIComponent(consentId)}/challenge`,
{ answer },
token,
);
diff --git a/apps/portal/src/routes/(protected)/confirm-vrp-consent-request/+page.server.ts b/apps/portal/src/routes/(protected)/confirm-vrp-consent-request/+page.server.ts
index 6493e9c9..0c8a0c98 100644
--- a/apps/portal/src/routes/(protected)/confirm-vrp-consent-request/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/confirm-vrp-consent-request/+page.server.ts
@@ -50,7 +50,7 @@ export async function load(event: RequestEvent) {
try {
const consentRequest = await obp_requests.get(
- `/obp/v5.0.0/consumer/consent-requests/${consentRequestId}`,
+ `/obp/v5.1.0/consumer/consent-requests/${consentRequestId}`,
token
);
@@ -95,7 +95,7 @@ export const actions = {
try {
const response = await obp_requests.post(
- `/obp/v5.0.0/consumer/consent-requests/${consentRequestId}/IMPLICIT/consents`,
+ `/obp/v5.1.0/consumer/consent-requests/${consentRequestId}/IMPLICIT/consents`,
{},
token
);
diff --git a/apps/portal/src/routes/(protected)/confirm-vrp-consent/+page.server.ts b/apps/portal/src/routes/(protected)/confirm-vrp-consent/+page.server.ts
index e6848a1a..8f61efa6 100644
--- a/apps/portal/src/routes/(protected)/confirm-vrp-consent/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/confirm-vrp-consent/+page.server.ts
@@ -60,7 +60,7 @@ export const actions = {
try {
// Step 1: Submit OTP challenge
const challengeResponse = await obp_requests.post(
- `/obp/v3.1.0/banks/${defaultBankId}/consents/${consentId}/challenge`,
+ `/obp/v5.1.0/banks/${defaultBankId}/consents/${consentId}/challenge`,
{ answer: otp },
token
);
diff --git a/apps/portal/src/routes/(protected)/obp-consent-request-sca/+page.server.ts b/apps/portal/src/routes/(protected)/obp-consent-request-sca/+page.server.ts
index 6f173458..6f189b44 100644
--- a/apps/portal/src/routes/(protected)/obp-consent-request-sca/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/obp-consent-request-sca/+page.server.ts
@@ -67,7 +67,7 @@ export const actions = {
try {
const response = await obp_requests.post(
- `/obp/v3.1.0/banks/${bankId}/consents/${consentId}/challenge`,
+ `/obp/v5.1.0/banks/${bankId}/consents/${consentId}/challenge`,
{ answer: otp },
token
);
diff --git a/apps/portal/src/routes/(protected)/obp-consent-request/+page.server.ts b/apps/portal/src/routes/(protected)/obp-consent-request/+page.server.ts
index 462e699d..df5f3b24 100644
--- a/apps/portal/src/routes/(protected)/obp-consent-request/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/obp-consent-request/+page.server.ts
@@ -63,7 +63,7 @@ export async function load(event: RequestEvent) {
try {
const consentRequest = await obp_requests.get(
- `/obp/v5.0.0/consumer/consent-requests/${consentRequestId}`,
+ `/obp/v5.1.0/consumer/consent-requests/${consentRequestId}`,
token
);
@@ -156,7 +156,7 @@ export const actions = {
try {
const response = await obp_requests.post(
- `/obp/v5.0.0/consumer/consent-requests/${consentRequestId}/IMPLICIT/consents`,
+ `/obp/v5.1.0/consumer/consent-requests/${consentRequestId}/IMPLICIT/consents`,
{},
token
);
From 7ab4d8a921ef9c6ea832176eb49867a42081557b Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 11/23] refactor(portal): call account, view, counterparty and
transaction challenge endpoints on OBP-API v5.1.0
---
.../[view_id]/counterparties/[counterparty_id]/+page.svelte | 2 +-
apps/portal/src/routes/(protected)/otp/+page.server.ts | 2 +-
.../src/routes/(protected)/user/accounts/+page.server.ts | 2 +-
.../user/accounts/[bank_id]/[account_id]/+page.server.ts | 2 +-
.../accounts/[bank_id]/[account_id]/[view_id]/+page.server.ts | 4 ++--
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.svelte b/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.svelte
index 28b07503..f7cb0a60 100644
--- a/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/account-access/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.svelte
@@ -92,7 +92,7 @@
error = null;
try {
const res = await trackedFetch(
- `/proxy/obp/v4.0.0/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/${encodeURIComponent(viewId)}/counterparties/${encodeURIComponent(counterpartyId)}`
+ `/proxy/obp/v5.1.0/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/${encodeURIComponent(viewId)}/counterparties/${encodeURIComponent(counterpartyId)}`
);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
diff --git a/apps/portal/src/routes/(protected)/otp/+page.server.ts b/apps/portal/src/routes/(protected)/otp/+page.server.ts
index ed7f88b1..39dabc6b 100644
--- a/apps/portal/src/routes/(protected)/otp/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/otp/+page.server.ts
@@ -123,7 +123,7 @@ export const actions = {
const transactionRequestId = formData.get('transactionRequestId') as string;
await obp_requests.post(
- `/obp/v4.0.0/banks/${bankId}/accounts/${accountId}/${viewId}/transaction-request-types/${transactionRequestType}/transaction-requests/${transactionRequestId}/challenge`,
+ `/obp/v5.1.0/banks/${bankId}/accounts/${accountId}/${viewId}/transaction-request-types/${transactionRequestType}/transaction-requests/${transactionRequestId}/challenge`,
{ id, answer: otp },
token
);
diff --git a/apps/portal/src/routes/(protected)/user/accounts/+page.server.ts b/apps/portal/src/routes/(protected)/user/accounts/+page.server.ts
index 28b10e15..0ff0b729 100644
--- a/apps/portal/src/routes/(protected)/user/accounts/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/user/accounts/+page.server.ts
@@ -61,7 +61,7 @@ export async function load(event: RequestEvent) {
let fetchError: string | null = null;
try {
- const accountsResponse = await obp_requests.get('/obp/v3.0.0/my/accounts', accessToken);
+ const accountsResponse = await obp_requests.get('/obp/v5.1.0/my/accounts', accessToken);
accounts = accountsResponse.accounts ?? [];
} catch (e) {
logger.error('Failed to fetch user accounts:', e);
diff --git a/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/+page.server.ts b/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/+page.server.ts
index 368aee50..334b64d9 100644
--- a/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/+page.server.ts
@@ -44,7 +44,7 @@ export async function load(event: RequestEvent) {
error(400, { message: 'bank_id and account_id are required.' });
}
- const viewsUrl = `/obp/v5.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/views`;
+ const viewsUrl = `/obp/v5.1.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/views`;
let defaultView: string | null = null;
try {
diff --git a/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/+page.server.ts b/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/+page.server.ts
index 8abc9548..e10fd7cb 100644
--- a/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/+page.server.ts
@@ -91,8 +91,8 @@ export async function load(event: RequestEvent) {
const accountUrl = `/obp/v7.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/${encodeURIComponent(view_id)}/account`;
const transactionsUrl = `/obp/v6.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/${encodeURIComponent(view_id)}/transactions?limit=10&sort_direction=DESC`;
- const viewsUrl = `/obp/v5.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/views`;
- const myAccountsUrl = '/obp/v3.0.0/my/accounts';
+ const viewsUrl = `/obp/v5.1.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(account_id)}/views`;
+ const myAccountsUrl = '/obp/v5.1.0/my/accounts';
const [accountResult, transactionsResult, viewsResult, myAccountsResult] =
await Promise.allSettled([
From 984e61f119ab58298a79044f391eb861974fd5ce Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 12/23] refactor(portal): call user auth context update
endpoints on OBP-API v5.1.0
---
.../add-user-auth-context-update-request/+page.server.ts | 2 +-
.../confirm-user-auth-context-update-request/+page.server.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/portal/src/routes/(protected)/add-user-auth-context-update-request/+page.server.ts b/apps/portal/src/routes/(protected)/add-user-auth-context-update-request/+page.server.ts
index 541bdacf..24ed4ae3 100644
--- a/apps/portal/src/routes/(protected)/add-user-auth-context-update-request/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/add-user-auth-context-update-request/+page.server.ts
@@ -63,7 +63,7 @@ export const actions = {
try {
const response = await obp_requests.post(
- `/obp/v5.0.0/banks/${bankId}/users/current/auth-context-updates/${scaMethod}`,
+ `/obp/v5.1.0/banks/${bankId}/users/current/auth-context-updates/${scaMethod}`,
{ key, value },
token
);
diff --git a/apps/portal/src/routes/(protected)/confirm-user-auth-context-update-request/+page.server.ts b/apps/portal/src/routes/(protected)/confirm-user-auth-context-update-request/+page.server.ts
index d0772b26..a1132385 100644
--- a/apps/portal/src/routes/(protected)/confirm-user-auth-context-update-request/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/confirm-user-auth-context-update-request/+page.server.ts
@@ -59,7 +59,7 @@ export const actions = {
try {
const response = await obp_requests.post(
- `/obp/v5.0.0/banks/${bankId}/users/current/auth-context-updates/${authContextUpdateId}/challenge`,
+ `/obp/v5.1.0/banks/${bankId}/users/current/auth-context-updates/${authContextUpdateId}/challenge`,
{ answer },
token
);
From a17126bb918f3aed7dac9819b1aad3258251429d Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 13/23] refactor: call the role list endpoint on OBP-API v5.1.0
---
apps/portal/src/routes/backend/obp/roles/+server.ts | 2 +-
packages/shared/src/lib/server/opey/consentHandler.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/portal/src/routes/backend/obp/roles/+server.ts b/apps/portal/src/routes/backend/obp/roles/+server.ts
index fa20f99e..636c3542 100644
--- a/apps/portal/src/routes/backend/obp/roles/+server.ts
+++ b/apps/portal/src/routes/backend/obp/roles/+server.ts
@@ -32,7 +32,7 @@ export const GET: RequestHandler = async ({ locals }) => {
try {
const accessToken = session.data.oauth?.access_token;
- const response = await obp_requests.get("/obp/v2.1.0/roles", accessToken);
+ const response = await obp_requests.get("/obp/v5.1.0/roles", accessToken);
return json(response);
} catch (err: unknown) {
logger.error("Error fetching roles:", err);
diff --git a/packages/shared/src/lib/server/opey/consentHandler.ts b/packages/shared/src/lib/server/opey/consentHandler.ts
index 10f216ac..14f21e16 100644
--- a/packages/shared/src/lib/server/opey/consentHandler.ts
+++ b/packages/shared/src/lib/server/opey/consentHandler.ts
@@ -159,7 +159,7 @@ export function createOpeyConsentHandler(config: OpeyConsentHandlerConfig): { PO
// Role catalogue: which roles are bank-scoped. Also a sanity check on stored pairs;
// an inconsistency is logged, never blocked on.
- const availableRolesResp = await obpRequests.get('/obp/v2.1.0/roles', accessToken);
+ const availableRolesResp = await obpRequests.get('/obp/v5.1.0/roles', accessToken);
const requiresBankIdByRole = new Map(
(availableRolesResp.roles || []).map((r: any) => [r.role, r.requires_bank_id])
);
From d92f4c05d6ce847a1c5b13784399465662c6421e Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:09 +0100
Subject: [PATCH 14/23] refactor: call message docs, glossary and certs
endpoints on OBP-API v5.1.0
---
apps/api-manager/src/lib/server/glossaryCache.ts | 4 ++--
.../[bank_id]/[dynamic_entity_id]/[webhook_id]/+page.svelte | 2 +-
.../(protected)/message-docs/[connector]/+page.server.ts | 4 ++--
.../message-docs/[connector]/[process]/+page.server.ts | 2 +-
packages/shared/src/lib/server/explorer/messageDocsCache.ts | 2 +-
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/apps/api-manager/src/lib/server/glossaryCache.ts b/apps/api-manager/src/lib/server/glossaryCache.ts
index 6c4504b1..3980b278 100644
--- a/apps/api-manager/src/lib/server/glossaryCache.ts
+++ b/apps/api-manager/src/lib/server/glossaryCache.ts
@@ -21,14 +21,14 @@ import { env } from "$env/dynamic/private";
const logger = createLogger("GlossaryCache");
-/** One item of GET /obp/v4.0.0/api/glossary. */
+/** One item of GET /obp/v5.1.0/api/glossary. */
export interface GlossaryItem {
title: string;
description: { markdown: string; html: string };
}
const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes: the glossary only changes on an OBP-API deploy
-const GLOSSARY_PATH = "/obp/v4.0.0/api/glossary";
+const GLOSSARY_PATH = "/obp/v5.1.0/api/glossary";
const DEFAULT_EXPLORER_URL = "https://apiexplorer-ii-sandbox.openbankproject.com";
let items: GlossaryItem[] = [];
diff --git a/apps/api-manager/src/routes/(protected)/dynamic-entity-webhooks/[bank_id]/[dynamic_entity_id]/[webhook_id]/+page.svelte b/apps/api-manager/src/routes/(protected)/dynamic-entity-webhooks/[bank_id]/[dynamic_entity_id]/[webhook_id]/+page.svelte
index 2b0f42bc..541a80f8 100644
--- a/apps/api-manager/src/routes/(protected)/dynamic-entity-webhooks/[bank_id]/[dynamic_entity_id]/[webhook_id]/+page.svelte
+++ b/apps/api-manager/src/routes/(protected)/dynamic-entity-webhooks/[bank_id]/[dynamic_entity_id]/[webhook_id]/+page.svelte
@@ -157,7 +157,7 @@
Signing
{#if w.signature_status === "signed"}
- Signed: each request carries OBP-Signature, verifiable with the key at /obp/v3.1.0/certs.
+ Signed: each request carries OBP-Signature, verifiable with the key at /obp/v5.1.0/certs.
{:else}
Not signed on this instance (it has no stable server key). Requests carry OBP-Signature-Status: not_configured.
{/if}
diff --git a/apps/api-manager/src/routes/(protected)/message-docs/[connector]/+page.server.ts b/apps/api-manager/src/routes/(protected)/message-docs/[connector]/+page.server.ts
index 55c24a42..6760e24b 100644
--- a/apps/api-manager/src/routes/(protected)/message-docs/[connector]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/message-docs/[connector]/+page.server.ts
@@ -34,9 +34,9 @@ const logger = createLogger("MessageDocsPage");
/** The endpoints behind this page, linked to their resource docs in the API Explorer. */
const ENDPOINTS = [
- { operation_id: "OBPv2.2.0-getMessageDocs", verb: "GET", path: "/obp/v2.2.0/message-docs/CONNECTOR" },
+ { operation_id: "OBPv2.2.0-getMessageDocs", verb: "GET", path: "/obp/v5.1.0/message-docs/CONNECTOR" },
{ operation_id: "OBPv6.0.0-getMessageDocsJsonSchema", verb: "GET", path: "/obp/v6.0.0/message-docs/CONNECTOR/json-schema" },
- { operation_id: "OBPv3.1.0-getMessageDocsSwagger", verb: "GET", path: "/obp/v3.1.0/message-docs/CONNECTOR/swagger2.0" },
+ { operation_id: "OBPv3.1.0-getMessageDocsSwagger", verb: "GET", path: "/obp/v5.1.0/message-docs/CONNECTOR/swagger2.0" },
];
export const load: PageServerLoad = async ({ locals, params, url }) => {
diff --git a/apps/api-manager/src/routes/(protected)/message-docs/[connector]/[process]/+page.server.ts b/apps/api-manager/src/routes/(protected)/message-docs/[connector]/[process]/+page.server.ts
index 9834e3f9..bc6ae4c1 100644
--- a/apps/api-manager/src/routes/(protected)/message-docs/[connector]/[process]/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/message-docs/[connector]/[process]/+page.server.ts
@@ -27,7 +27,7 @@ import {
import { explorerResourceDocUrl } from "$lib/server/glossaryCache";
const ENDPOINTS = [
- { operation_id: "OBPv2.2.0-getMessageDocs", verb: "GET", path: "/obp/v2.2.0/message-docs/CONNECTOR" },
+ { operation_id: "OBPv2.2.0-getMessageDocs", verb: "GET", path: "/obp/v5.1.0/message-docs/CONNECTOR" },
];
export const load: PageServerLoad = async ({ locals, params }) => {
diff --git a/packages/shared/src/lib/server/explorer/messageDocsCache.ts b/packages/shared/src/lib/server/explorer/messageDocsCache.ts
index acee02e0..c36245e8 100644
--- a/packages/shared/src/lib/server/explorer/messageDocsCache.ts
+++ b/packages/shared/src/lib/server/explorer/messageDocsCache.ts
@@ -121,7 +121,7 @@ async function fetchConnector(
const promise = (async () => {
try {
const resp = await obp.get(
- `/obp/v2.2.0/message-docs/${encodeURIComponent(connector)}`,
+ `/obp/v5.1.0/message-docs/${encodeURIComponent(connector)}`,
token
);
const docs = (resp?.message_docs ?? []) as MessageDoc[];
From c575e8e31d5c5fce40bf5513fe05558fdc3506da Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:10 +0100
Subject: [PATCH 15/23] refactor: call consumer scope and entitlement request
endpoints on OBP-API v5.1.0
---
.../src/routes/(protected)/user/entitlements/+page.server.ts | 2 +-
apps/cohort/src/lib/server/staffAccess.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/api-manager/src/routes/(protected)/user/entitlements/+page.server.ts b/apps/api-manager/src/routes/(protected)/user/entitlements/+page.server.ts
index 6d47ff7a..86f66acf 100644
--- a/apps/api-manager/src/routes/(protected)/user/entitlements/+page.server.ts
+++ b/apps/api-manager/src/routes/(protected)/user/entitlements/+page.server.ts
@@ -67,7 +67,7 @@ export async function load({ locals }) {
try {
const consumer = await obp_requests.get('/obp/v6.0.0/consumers/current', accessToken);
if (!consumer?.consumer_id) return null;
- const scopesResponse = await obp_requests.get(`/obp/v4.0.0/consumers/${consumer.consumer_id}/scopes`, accessToken);
+ const scopesResponse = await obp_requests.get(`/obp/v5.1.0/consumers/${consumer.consumer_id}/scopes`, accessToken);
return {
consumer_id: consumer.consumer_id,
app_name: consumer.app_name || '',
diff --git a/apps/cohort/src/lib/server/staffAccess.ts b/apps/cohort/src/lib/server/staffAccess.ts
index 44567406..64595a5d 100644
--- a/apps/cohort/src/lib/server/staffAccess.ts
+++ b/apps/cohort/src/lib/server/staffAccess.ts
@@ -116,7 +116,7 @@ export async function staffAccess(token: string, extra: StaffRole[] = []) {
const space = cohortSpace();
const [user, requests, manager] = await Promise.all([
obpRequests.get('/obp/v5.1.0/users/current', token),
- obpRequests.get('/obp/v3.0.0/my/entitlement-requests', token).catch(() => ({ entitlement_requests: [] })),
+ obpRequests.get('/obp/v5.1.0/my/entitlement-requests', token).catch(() => ({ entitlement_requests: [] })),
apiManagerUrl()
]);
const held: Grant[] = user?.entitlements?.list ?? [];
From 29ffaa777147c8bdc00bf8f29498005cb8cf937f Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:27 +0100
Subject: [PATCH 16/23] refactor(portal): call the counterparty detail endpoint
on OBP-API v5.1.0
---
.../[view_id]/counterparties/[counterparty_id]/+page.server.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.server.ts b/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.server.ts
index f54769a8..a84082da 100644
--- a/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.server.ts
+++ b/apps/portal/src/routes/(protected)/user/accounts/[bank_id]/[account_id]/[view_id]/counterparties/[counterparty_id]/+page.server.ts
@@ -69,7 +69,7 @@ export async function load(event: RequestEvent) {
});
}
- const url = `/obp/v4.0.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(
+ const url = `/obp/v5.1.0/banks/${encodeURIComponent(bank_id)}/accounts/${encodeURIComponent(
account_id
)}/${encodeURIComponent(view_id)}/counterparties/${encodeURIComponent(counterparty_id)}`;
From 1311b0cb600505576b6fa28f68f2ccbed0a2cb88 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Mon, 9 Nov 2026 22:15:36 +0100
Subject: [PATCH 17/23] docs: show OBP-API v5.1.0 paths in developer pages and
notes
---
apps/api-manager/dynamic_endpoint_plan_doc.md | 32 +++++++++----------
...la_initiated_Administrate_Consents_flow.md | 2 +-
...ola_initiated_Berlin_Group_Consent_flow.md | 2 +-
...Hola_initiated_OBP_Consent_Request_flow.md | 2 +-
..._initiated_UK_Open_Banking_Consent_flow.md | 2 +-
apps/portal/docs/training-page.md | 2 +-
.../access-to-accounts/+page.svelte | 8 ++---
.../developers/account-access/+page.svelte | 2 +-
.../src/routes/user-invitation/README.md | 6 ++--
9 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/apps/api-manager/dynamic_endpoint_plan_doc.md b/apps/api-manager/dynamic_endpoint_plan_doc.md
index 9e3deeae..61332621 100644
--- a/apps/api-manager/dynamic_endpoint_plan_doc.md
+++ b/apps/api-manager/dynamic_endpoint_plan_doc.md
@@ -40,32 +40,32 @@ A Dynamic Endpoint is a Swagger/OpenAPI doc that OBP ingests. Each operation in
## Relevant OBP endpoints (reference)
### Dynamic Endpoint (existing UI)
-- `POST /obp/v4.0.0/management/dynamic-endpoints` — role `CanCreateDynamicEndpoint`
-- `GET /obp/v4.0.0/management/dynamic-endpoints` — role `CanGetDynamicEndpoints`
-- `GET /obp/v4.0.0/management/dynamic-endpoints/{id}`
-- `DELETE /obp/v4.0.0/management/dynamic-endpoints/{id}` — role `CanDeleteDynamicEndpoint`
-- `PUT /obp/v4.0.0/management/dynamic-endpoints/{id}/host` — role `CanUpdateDynamicEndpoint`
+- `POST /obp/v5.1.0/management/dynamic-endpoints` — role `CanCreateDynamicEndpoint`
+- `GET /obp/v5.1.0/management/dynamic-endpoints` — role `CanGetDynamicEndpoints`
+- `GET /obp/v5.1.0/management/dynamic-endpoints/{id}`
+- `DELETE /obp/v5.1.0/management/dynamic-endpoints/{id}` — role `CanDeleteDynamicEndpoint`
+- `PUT /obp/v5.1.0/management/dynamic-endpoints/{id}/host` — role `CanUpdateDynamicEndpoint`
- Bank-level equivalents under `/management/banks/{bank_id}/dynamic-endpoints/...`
### Method Routing
-- `POST /obp/v3.1.0/management/method_routings` — role `CanCreateMethodRouting`
-- `GET /obp/v3.1.0/management/method_routings`
-- `PUT /obp/v3.1.0/management/method_routings/{id}`
-- `DELETE /obp/v3.1.0/management/method_routings/{id}`
+- `POST /obp/v5.1.0/management/method_routings` — role `CanCreateMethodRouting`
+- `GET /obp/v5.1.0/management/method_routings`
+- `PUT /obp/v5.1.0/management/method_routings/{id}`
+- `DELETE /obp/v5.1.0/management/method_routings/{id}`
- Helper: `GET /obp/v6.0.0/system/connector-method-names`
- Fields: `method_name`, `connector_name`, `is_bank_id_exact_match`, `bank_id_pattern?`, `parameters[]`
### JSON Schema Validation (per operation_id)
-- `POST /obp/v4.0.0/management/json-schema-validations/{OPERATION_ID}` — role `CanCreateJsonSchemaValidation`
-- `GET /obp/v4.0.0/management/json-schema-validations/{OPERATION_ID}`
-- `PUT /obp/v4.0.0/management/json-schema-validations/{OPERATION_ID}`
-- `DELETE /obp/v4.0.0/management/json-schema-validations/{OPERATION_ID}`
-- `GET /obp/v4.0.0/management/json-schema-validations` — list all
-- `GET /obp/v4.0.0/endpoints/json-schema-validations` — public, no auth (useful for read-only preview)
+- `POST /obp/v5.1.0/management/json-schema-validations/{OPERATION_ID}` — role `CanCreateJsonSchemaValidation`
+- `GET /obp/v5.1.0/management/json-schema-validations/{OPERATION_ID}`
+- `PUT /obp/v5.1.0/management/json-schema-validations/{OPERATION_ID}`
+- `DELETE /obp/v5.1.0/management/json-schema-validations/{OPERATION_ID}`
+- `GET /obp/v5.1.0/management/json-schema-validations` — list all
+- `GET /obp/v5.1.0/endpoints/json-schema-validations` — public, no auth (useful for read-only preview)
- Request body: a JSON Schema doc (draft-07 is the OBP example).
### Endpoint Mapping (for `host: dynamic_entity`)
-- `POST /obp/v4.0.0/management/endpoint-mappings` — role `CanCreateEndpointMapping`
+- `POST /obp/v5.1.0/management/endpoint-mappings` — role `CanCreateEndpointMapping`
- plus GET/PUT/DELETE + bank-level equivalents
### Role names (to surface on the UI)
diff --git a/apps/portal/docs/WIP/Hola_initiated_Administrate_Consents_flow.md b/apps/portal/docs/WIP/Hola_initiated_Administrate_Consents_flow.md
index 35a07773..567df506 100644
--- a/apps/portal/docs/WIP/Hola_initiated_Administrate_Consents_flow.md
+++ b/apps/portal/docs/WIP/Hola_initiated_Administrate_Consents_flow.md
@@ -24,7 +24,7 @@ Hola (TPP app) OBP API Hydra (Auth)
4. Hola /main endpoint:
- Validates id_token
- Exchanges code for tokens
- - Fetches /obp/v4.0.0/users/current
+ - Fetches /obp/v5.1.0/users/current
- Displays consents management page
```
diff --git a/apps/portal/docs/WIP/Hola_initiated_Berlin_Group_Consent_flow.md b/apps/portal/docs/WIP/Hola_initiated_Berlin_Group_Consent_flow.md
index 04557acc..1e2fb377 100644
--- a/apps/portal/docs/WIP/Hola_initiated_Berlin_Group_Consent_flow.md
+++ b/apps/portal/docs/WIP/Hola_initiated_Berlin_Group_Consent_flow.md
@@ -33,7 +33,7 @@ Hola (TPP app) OBP API Hydra (Auth)
4. Hola /main endpoint:
- Validates id_token
- Exchanges code for tokens
- - Fetches /obp/v4.0.0/users/current
+ - Fetches /obp/v5.1.0/users/current
- Fetches /berlin-group/v1.3/consents/{CONSENT_ID}
(gets frequencyPerDay, consentStatus,
validUntil, recurringIndicator)
diff --git a/apps/portal/docs/WIP/Hola_initiated_OBP_Consent_Request_flow.md b/apps/portal/docs/WIP/Hola_initiated_OBP_Consent_Request_flow.md
index 46c1cb5c..3094277b 100644
--- a/apps/portal/docs/WIP/Hola_initiated_OBP_Consent_Request_flow.md
+++ b/apps/portal/docs/WIP/Hola_initiated_OBP_Consent_Request_flow.md
@@ -35,7 +35,7 @@ Hola (TPP app) OBP API Hydra (Auth)
4. Hola /main endpoint:
- Validates id_token
- Exchanges code for tokens
- - Fetches /obp/v4.0.0/users/current
+ - Fetches /obp/v5.1.0/users/current
- Displays accounts page
```
diff --git a/apps/portal/docs/WIP/Hola_initiated_UK_Open_Banking_Consent_flow.md b/apps/portal/docs/WIP/Hola_initiated_UK_Open_Banking_Consent_flow.md
index 034d6b6a..00b0a199 100644
--- a/apps/portal/docs/WIP/Hola_initiated_UK_Open_Banking_Consent_flow.md
+++ b/apps/portal/docs/WIP/Hola_initiated_UK_Open_Banking_Consent_flow.md
@@ -31,7 +31,7 @@ Hola (TPP app) OBP API Hydra (Auth)
- Extracts consent_id from claims
- Exchanges code for tokens
(access_token, refresh_token)
- - Fetches /obp/v4.0.0/users/current
+ - Fetches /obp/v5.1.0/users/current
- Displays accounts page
```
diff --git a/apps/portal/docs/training-page.md b/apps/portal/docs/training-page.md
index ef3a88be..5d00a91e 100644
--- a/apps/portal/docs/training-page.md
+++ b/apps/portal/docs/training-page.md
@@ -43,7 +43,7 @@ The easiest way is the prefilled API Manager form:
`/dynamic-entities/system/create?template=training-progress`
(the Portal's missing-entity callout links there; the template lives in
`apps/api-manager/src/lib/data/dynamicEntityTemplates.ts`). Equivalent raw API call
-(`POST /obp/v4.0.0/management/system-dynamic-entities`):
+(`POST /obp/v5.1.0/management/system-dynamic-entities`):
```json
{
diff --git a/apps/portal/src/routes/developers/access-to-accounts/+page.svelte b/apps/portal/src/routes/developers/access-to-accounts/+page.svelte
index 25a69300..ba4bc82e 100644
--- a/apps/portal/src/routes/developers/access-to-accounts/+page.svelte
+++ b/apps/portal/src/routes/developers/access-to-accounts/+page.svelte
@@ -41,7 +41,7 @@
see in API Explorer){/if}:
-
+
Counterparties
@@ -134,7 +134,7 @@
(see in API Explorer){/if}:
-
+
You can also create counterparties, which is needed before making
@@ -150,7 +150,7 @@
(see in API Explorer){/if}:
-
+
Accessing Accounts as a Third Party
diff --git a/apps/portal/src/routes/developers/account-access/+page.svelte b/apps/portal/src/routes/developers/account-access/+page.svelte
index a29e5a20..33345650 100644
--- a/apps/portal/src/routes/developers/account-access/+page.svelte
+++ b/apps/portal/src/routes/developers/account-access/+page.svelte
@@ -94,7 +94,7 @@
(see in API Explorer){/if}:
-
+
Granting Access
diff --git a/apps/portal/src/routes/user-invitation/README.md b/apps/portal/src/routes/user-invitation/README.md
index ae605b80..c537e23f 100644
--- a/apps/portal/src/routes/user-invitation/README.md
+++ b/apps/portal/src/routes/user-invitation/README.md
@@ -48,7 +48,7 @@ This route handles the complete user invitation acceptance flow. When a user rec
### 5. Account Creation (Option B - Full Portal Flow)
- Form submits to `?/accept` action
- Server validates all required fields (username, password, password match, terms)
-- Server calls PUT /obp/v4.0.0/banks/{BANK_ID}/user-invitation with full user data
+- Server calls PUT /obp/v5.1.0/banks/{BANK_ID}/user-invitation with full user data
- Account is created directly via REST API
- Redirects to login page with success message
- User can immediately log in with their new credentials
@@ -64,7 +64,7 @@ This route handles the complete user invitation acceptance flow. When a user rec
**API Call:**
```
-POST /obp/v4.0.0/banks/{BANK_ID}/user-invitations
+POST /obp/v5.1.0/banks/{BANK_ID}/user-invitations
Body: { "secret_key": 5819479115482092878 }
```
@@ -92,7 +92,7 @@ Body: { "secret_key": 5819479115482092878 }
**API Call:**
```
-PUT /obp/v4.0.0/banks/{BANK_ID}/user-invitation
+PUT /obp/v5.1.0/banks/{BANK_ID}/user-invitation
Body: { secret_key, username, password, first_name, last_name, email, company, country }
```
From c0372a9f3a8fa125ef875698377a9dd39ebf36c7 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Tue, 10 Nov 2026 00:40:50 +0100
Subject: [PATCH 18/23] test(cohort): expect username lookups on GET
/obp/v7.0.0/users?username=
---
apps/cohort/src/lib/server/users.test.ts | 37 +++++++++++++++++++
.../src/lib/server/obp/obpApiVersion.test.ts | 4 --
2 files changed, 37 insertions(+), 4 deletions(-)
create mode 100644 apps/cohort/src/lib/server/users.test.ts
diff --git a/apps/cohort/src/lib/server/users.test.ts b/apps/cohort/src/lib/server/users.test.ts
new file mode 100644
index 00000000..7305b48c
--- /dev/null
+++ b/apps/cohort/src/lib/server/users.test.ts
@@ -0,0 +1,37 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('./obp', () => ({ obpRequests: { get: vi.fn() } }));
+
+const { obpRequests } = await import('./obp');
+const { userIdByUsername } = await import('./users');
+const get = vi.mocked(obpRequests.get);
+
+describe('userIdByUsername', () => {
+ beforeEach(() => get.mockReset());
+
+ it('looks the user up with GET /obp/v7.0.0/users?username=', async () => {
+ get.mockResolvedValue({ users: [{ user_id: 'u-1', username: 'ada lovelace', provider: 'local' }] });
+ await expect(userIdByUsername('ada lovelace', 'token-1')).resolves.toBe('u-1');
+ expect(get).toHaveBeenCalledWith('/obp/v7.0.0/users?username=ada%20lovelace', 'token-1');
+ });
+
+ it('only accepts an exact username match', async () => {
+ get.mockResolvedValue({ users: [{ user_id: 'u-2', username: 'Ada', provider: 'local' }] });
+ await expect(userIdByUsername('ada', 'token-1')).rejects.toThrow(/no user/i);
+ });
+
+ it('fails when no user has the username', async () => {
+ get.mockResolvedValue({ users: [] });
+ await expect(userIdByUsername('nobody', 'token-1')).rejects.toThrow(/no user/i);
+ });
+
+ it('fails when users of several providers share the username', async () => {
+ get.mockResolvedValue({
+ users: [
+ { user_id: 'u-3', username: 'ada', provider: 'local' },
+ { user_id: 'u-4', username: 'ada', provider: 'keycloak' }
+ ]
+ });
+ await expect(userIdByUsername('ada', 'token-1')).rejects.toThrow(/2 users/);
+ });
+});
diff --git a/packages/shared/src/lib/server/obp/obpApiVersion.test.ts b/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
index cbab5681..37257ebc 100644
--- a/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
+++ b/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
@@ -15,10 +15,6 @@ const OLD_VERSION = /\/obp\/(v(?:[1-4])\.\d+\.\d+|v5\.0\.0)\//g;
/** file (repo-relative) + path fragment -> why it is still below v5.1.0 */
const NO_V510_EQUIVALENT: Record = {
- 'apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts|/obp/v3.0.0/users/username/':
- 'GET users/username/USERNAME is excluded from v5.1.0; v5.1.0 needs a provider: users/provider/PROVIDER/username/USERNAME',
- 'apps/cohort/scripts/seed-demo-data.mjs|/obp/v3.0.0/users/username/':
- 'GET users/username/USERNAME is excluded from v5.1.0; v5.1.0 needs a provider: users/provider/PROVIDER/username/USERNAME',
'apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts|/obp/v4.0.0/banks/${space()}/customers?':
'v4.0.0 filters customers by attribute; v5.1.0 serves getCustomersAtOneBank without attribute filtering',
'apps/cohort/scripts/seed-demo-data.mjs|/obp/v4.0.0/banks/${space}/customers?':
From 47dfe14f3b1a10d474b1d1566a600ce91a61184a Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Tue, 10 Nov 2026 00:41:28 +0100
Subject: [PATCH 19/23] refactor(cohort): look users up by username on OBP-API
v7.0.0
---
apps/cohort/scripts/seed-demo-data.mjs | 6 ++--
apps/cohort/src/lib/server/users.ts | 34 +++++++++++++++++++
.../staff/organisations/+page.server.ts | 4 +--
3 files changed, 40 insertions(+), 4 deletions(-)
create mode 100644 apps/cohort/src/lib/server/users.ts
diff --git a/apps/cohort/scripts/seed-demo-data.mjs b/apps/cohort/scripts/seed-demo-data.mjs
index 9f6d39ea..cf3c0f05 100644
--- a/apps/cohort/scripts/seed-demo-data.mjs
+++ b/apps/cohort/scripts/seed-demo-data.mjs
@@ -222,8 +222,10 @@ async function loginValidated(username, password) {
return await login(username, password);
} catch (err) {
if (!superuser || !String(err?.message).includes('OBP-20073')) throw err;
- const user = await obp('GET', `/obp/v3.0.0/users/username/${encodeURIComponent(username)}`, superuser.token);
- await obp('PUT', `/obp/v5.1.0/management/users/${encodeURIComponent(user.user_id)}`, superuser.token);
+ const found = await obp('GET', `/obp/v7.0.0/users?username=${encodeURIComponent(username)}`, superuser.token);
+ const users = (found?.users ?? []).filter((u) => u?.username === username && u?.user_id);
+ if (users.length !== 1) throw new Error(`Expected one user with username ${username}, found ${users.length}`);
+ await obp('PUT', `/obp/v5.1.0/management/users/${encodeURIComponent(users[0].user_id)}`, superuser.token);
console.log(`Validated the email of ${username}`);
return login(username, password);
}
diff --git a/apps/cohort/src/lib/server/users.ts b/apps/cohort/src/lib/server/users.ts
new file mode 100644
index 00000000..5ce6af95
--- /dev/null
+++ b/apps/cohort/src/lib/server/users.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025-2026 TESOBE GmbH
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { obpRequests } from '$lib/server/obp';
+
+type UserRow = { user_id?: string; username?: string; provider?: string };
+
+/**
+ * The user_id of the User with exactly this username (needs CanGetAnyUser).
+ *
+ * GET /obp/v7.0.0/users?username= filters across all identity providers, so a username used
+ * by Users of more than one provider is refused rather than guessed.
+ */
+export async function userIdByUsername(username: string, token: string): Promise {
+ const response = await obpRequests.get(`/obp/v7.0.0/users?username=${encodeURIComponent(username)}`, token);
+ const users: UserRow[] = (response?.users ?? []).filter((u: UserRow) => u?.username === username && u?.user_id);
+ if (users.length === 0) throw new Error(`No user with username ${username}`);
+ if (users.length > 1) throw new Error(`${users.length} users have the username ${username} (providers: ${users.map((u) => u.provider).join(', ')})`);
+ return String(users[0].user_id);
+}
diff --git a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
index 2e39243a..f16b519c 100644
--- a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
+++ b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
@@ -24,6 +24,7 @@ import { idOf, listRecords, userToken } from '$lib/server/records';
import { cohortSpace } from '$lib/server/space';
import { staffAccess } from '$lib/server/staffAccess';
import { requestRole, write } from '$lib/server/staffRecords';
+import { userIdByUsername } from '$lib/server/users';
import type { Actions, PageServerLoad } from './$types';
/**
@@ -138,8 +139,7 @@ export const actions: Actions = {
if (!customerId || !username) return fail(400, { form: 'member', error: 'member_missing' });
let userId: string;
try {
- const user = await obpRequests.get(`/obp/v3.0.0/users/username/${encodeURIComponent(username)}`, token);
- userId = String(user?.user_id ?? '');
+ userId = await userIdByUsername(username, token);
} catch (err) {
return fail(400, { form: 'member', error: `member_not_found`, detail: messageOf(err) });
}
From 6d3e415134b7ffe9fef27e1e9158116b1d3b20b1 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Tue, 10 Nov 2026 01:53:41 +0100
Subject: [PATCH 20/23] style(cohort): format the username lookup helper and
its test
---
apps/cohort/src/lib/server/users.test.ts | 4 +++-
apps/cohort/src/lib/server/users.ts | 14 +++++++++++---
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/apps/cohort/src/lib/server/users.test.ts b/apps/cohort/src/lib/server/users.test.ts
index 7305b48c..50892856 100644
--- a/apps/cohort/src/lib/server/users.test.ts
+++ b/apps/cohort/src/lib/server/users.test.ts
@@ -10,7 +10,9 @@ describe('userIdByUsername', () => {
beforeEach(() => get.mockReset());
it('looks the user up with GET /obp/v7.0.0/users?username=', async () => {
- get.mockResolvedValue({ users: [{ user_id: 'u-1', username: 'ada lovelace', provider: 'local' }] });
+ get.mockResolvedValue({
+ users: [{ user_id: 'u-1', username: 'ada lovelace', provider: 'local' }]
+ });
await expect(userIdByUsername('ada lovelace', 'token-1')).resolves.toBe('u-1');
expect(get).toHaveBeenCalledWith('/obp/v7.0.0/users?username=ada%20lovelace', 'token-1');
});
diff --git a/apps/cohort/src/lib/server/users.ts b/apps/cohort/src/lib/server/users.ts
index 5ce6af95..c7d9210b 100644
--- a/apps/cohort/src/lib/server/users.ts
+++ b/apps/cohort/src/lib/server/users.ts
@@ -26,9 +26,17 @@ type UserRow = { user_id?: string; username?: string; provider?: string };
* by Users of more than one provider is refused rather than guessed.
*/
export async function userIdByUsername(username: string, token: string): Promise {
- const response = await obpRequests.get(`/obp/v7.0.0/users?username=${encodeURIComponent(username)}`, token);
- const users: UserRow[] = (response?.users ?? []).filter((u: UserRow) => u?.username === username && u?.user_id);
+ const response = await obpRequests.get(
+ `/obp/v7.0.0/users?username=${encodeURIComponent(username)}`,
+ token
+ );
+ const users: UserRow[] = (response?.users ?? []).filter(
+ (u: UserRow) => u?.username === username && u?.user_id
+ );
if (users.length === 0) throw new Error(`No user with username ${username}`);
- if (users.length > 1) throw new Error(`${users.length} users have the username ${username} (providers: ${users.map((u) => u.provider).join(', ')})`);
+ if (users.length > 1)
+ throw new Error(
+ `${users.length} users have the username ${username} (providers: ${users.map((u) => u.provider).join(', ')})`
+ );
return String(users[0].user_id);
}
From 4b66d53f50683ef97d4f20c77d073e7d1459040e Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Tue, 10 Nov 2026 02:02:18 +0100
Subject: [PATCH 21/23] test: catch old OBP-API versions built without a
leading slash, by concatenation or in a constant
---
.../src/lib/server/obp/obpApiVersion.test.ts | 22 +++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/packages/shared/src/lib/server/obp/obpApiVersion.test.ts b/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
index 37257ebc..b368a807 100644
--- a/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
+++ b/packages/shared/src/lib/server/obp/obpApiVersion.test.ts
@@ -11,7 +11,10 @@ const REPO_ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)), '../../.
const SOURCE_ROOTS = ['apps', 'packages'];
const SKIP_DIRS = new Set(['node_modules', '.svelte-kit', 'build', 'dist', 'test-results', 'docs']);
const EXTENSIONS = ['.ts', '.js', '.mjs', '.cjs', '.svelte'];
-const OLD_VERSION = /\/obp\/(v(?:[1-4])\.\d+\.\d+|v5\.0\.0)\//g;
+/** obp/vX.Y.Z in a path, with or without a leading slash, followed by anything but more of the version. */
+const OLD_VERSION = /obp\/(v(?:[1-4])\.\d+\.\d+|v5\.0\.0)(?![\d.])/g;
+/** A string literal that is only a version, as in `const API_VERSION = 'v4.0.0'`, used to build paths. */
+const OLD_VERSION_LITERAL = /['"`](v(?:[1-4])\.\d+\.\d+|v5\.0\.0)['"`]/g;
/** file (repo-relative) + path fragment -> why it is still below v5.1.0 */
const NO_V510_EQUIVALENT: Record = {
@@ -41,7 +44,10 @@ function callsBelowV510(): string[] {
readFileSync(file, 'utf8')
.split('\n')
.forEach((line, i) => {
- for (const match of line.matchAll(OLD_VERSION)) {
+ // A version-only literal in a comment documents something; it builds no path.
+ const comment = /^\s*(\/\/|\/?\*)/.test(line);
+ const literals = comment ? [] : [...line.matchAll(OLD_VERSION_LITERAL)];
+ for (const match of [...line.matchAll(OLD_VERSION), ...literals]) {
const allowed = Object.keys(NO_V510_EQUIVALENT).some((key) => {
const [keyFile, fragment] = key.split('|');
return keyFile === rel && line.includes(fragment);
@@ -63,6 +69,18 @@ describe('OBP-API version used by the app stack', () => {
expect(callsBelowV510()).toEqual([]);
});
+ it('recognises the ways a path below v5.1.0 can be written', () => {
+ const flagged = (line: string) =>
+ [...line.matchAll(OLD_VERSION), ...line.matchAll(OLD_VERSION_LITERAL)].length > 0;
+ expect(flagged("'/obp/v4.0.0' + path")).toBe(true);
+ expect(flagged('`/obp/v4.0.0?limit=1`')).toBe(true);
+ expect(flagged('`${base}obp/v3.1.0/banks`')).toBe(true);
+ expect(flagged("const API_VERSION = 'v4.0.0';")).toBe(true);
+ expect(flagged('`/obp/v5.1.0/banks`')).toBe(false);
+ expect(flagged("const API_VERSION = 'v6.0.0';")).toBe(false);
+ expect(flagged("operation_id: 'OBPv4.0.0-getBanks'")).toBe(false);
+ });
+
it('keeps the list of calls without a v5.1.0 equivalent current', () => {
for (const key of Object.keys(NO_V510_EQUIVALENT)) {
const [file, fragment] = key.split('|');
From 70d408d98b576f8ebb1b0cf1f78e76981e1eb438 Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Tue, 10 Nov 2026 02:22:30 +0100
Subject: [PATCH 22/23] test(cohort): expect a distinct error for an ambiguous
username
---
apps/cohort/src/lib/server/users.test.ts | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/apps/cohort/src/lib/server/users.test.ts b/apps/cohort/src/lib/server/users.test.ts
index 50892856..2af60d5b 100644
--- a/apps/cohort/src/lib/server/users.test.ts
+++ b/apps/cohort/src/lib/server/users.test.ts
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('./obp', () => ({ obpRequests: { get: vi.fn() } }));
const { obpRequests } = await import('./obp');
-const { userIdByUsername } = await import('./users');
+const { memberLookupError, userIdByUsername } = await import('./users');
const get = vi.mocked(obpRequests.get);
describe('userIdByUsername', () => {
@@ -37,3 +37,22 @@ describe('userIdByUsername', () => {
await expect(userIdByUsername('ada', 'token-1')).rejects.toThrow(/2 users/);
});
});
+
+describe('memberLookupError', () => {
+ beforeEach(() => get.mockReset());
+
+ it('tells an ambiguous username apart from one nobody has', async () => {
+ get.mockResolvedValue({
+ users: [
+ { user_id: 'u-3', username: 'ada', provider: 'local' },
+ { user_id: 'u-4', username: 'ada', provider: 'keycloak' }
+ ]
+ });
+ const ambiguous = await userIdByUsername('ada', 'token-1').catch((e) => e);
+ expect(memberLookupError(ambiguous)).toBe('member_ambiguous');
+
+ get.mockResolvedValue({ users: [] });
+ const missing = await userIdByUsername('nobody', 'token-1').catch((e) => e);
+ expect(memberLookupError(missing)).toBe('member_not_found');
+ });
+});
From e5ce0ea9150536eea4f5ed4b75387f10bbe02f5e Mon Sep 17 00:00:00 2001
From: Hongwei
Date: Tue, 10 Nov 2026 02:22:41 +0100
Subject: [PATCH 23/23] fix(cohort): say when a username belongs to users of
more than one provider
---
apps/cohort/src/lib/i18n/ar.ts | 1 +
apps/cohort/src/lib/i18n/en.ts | 1 +
apps/cohort/src/lib/server/users.ts | 9 ++++++++-
.../(protected)/staff/organisations/+page.server.ts | 4 ++--
.../routes/(protected)/staff/organisations/+page.svelte | 2 +-
5 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/apps/cohort/src/lib/i18n/ar.ts b/apps/cohort/src/lib/i18n/ar.ts
index d0256664..155643da 100644
--- a/apps/cohort/src/lib/i18n/ar.ts
+++ b/apps/cohort/src/lib/i18n/ar.ts
@@ -852,6 +852,7 @@ export const ar: Partial> = {
organisation_members_saved: 'تم حفظ أشخاص المؤسسة.',
member_missing: 'أدخل اسم مستخدم.',
member_not_found: 'لا يوجد مستخدم بهذا الاسم.',
+ member_ambiguous: 'هذا الاسم مستخدم لدى أكثر من مزوّد تسجيل دخول، ولا يمكن معرفة المستخدم المقصود.',
files_heading: 'المستندات',
files_none: 'لا توجد مستندات بعد.',
files_name: 'الاسم',
diff --git a/apps/cohort/src/lib/i18n/en.ts b/apps/cohort/src/lib/i18n/en.ts
index 52c4e722..bd8fd010 100644
--- a/apps/cohort/src/lib/i18n/en.ts
+++ b/apps/cohort/src/lib/i18n/en.ts
@@ -849,6 +849,7 @@ export const en = {
organisation_members_saved: 'The people of the organisation are saved.',
member_missing: 'Give a username.',
member_not_found: 'No user has that username.',
+ member_ambiguous: 'Users of more than one login provider have that username; it cannot be told which one is meant.',
files_heading: 'Documents',
files_none: 'No documents yet.',
files_name: 'Name',
diff --git a/apps/cohort/src/lib/server/users.ts b/apps/cohort/src/lib/server/users.ts
index c7d9210b..5925e2e9 100644
--- a/apps/cohort/src/lib/server/users.ts
+++ b/apps/cohort/src/lib/server/users.ts
@@ -19,6 +19,13 @@ import { obpRequests } from '$lib/server/obp';
type UserRow = { user_id?: string; username?: string; provider?: string };
+/** Users of more than one identity provider have the username: which one is meant cannot be told. */
+export class AmbiguousUsernameError extends Error {}
+
+/** The form error key for a failed lookup: an ambiguous username, or none found (or the lookup failed). */
+export const memberLookupError = (err: unknown) =>
+ err instanceof AmbiguousUsernameError ? 'member_ambiguous' : 'member_not_found';
+
/**
* The user_id of the User with exactly this username (needs CanGetAnyUser).
*
@@ -35,7 +42,7 @@ export async function userIdByUsername(username: string, token: string): Promise
);
if (users.length === 0) throw new Error(`No user with username ${username}`);
if (users.length > 1)
- throw new Error(
+ throw new AmbiguousUsernameError(
`${users.length} users have the username ${username} (providers: ${users.map((u) => u.provider).join(', ')})`
);
return String(users[0].user_id);
diff --git a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
index f16b519c..4ad8fde4 100644
--- a/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
+++ b/apps/cohort/src/routes/(protected)/staff/organisations/+page.server.ts
@@ -24,7 +24,7 @@ import { idOf, listRecords, userToken } from '$lib/server/records';
import { cohortSpace } from '$lib/server/space';
import { staffAccess } from '$lib/server/staffAccess';
import { requestRole, write } from '$lib/server/staffRecords';
-import { userIdByUsername } from '$lib/server/users';
+import { memberLookupError, userIdByUsername } from '$lib/server/users';
import type { Actions, PageServerLoad } from './$types';
/**
@@ -141,7 +141,7 @@ export const actions: Actions = {
try {
userId = await userIdByUsername(username, token);
} catch (err) {
- return fail(400, { form: 'member', error: `member_not_found`, detail: messageOf(err) });
+ return fail(400, { form: 'member', error: memberLookupError(err), detail: messageOf(err) });
}
if (!userId) return fail(400, { form: 'member', error: 'member_not_found' });
return write('member', () => obpRequests.post(`/obp/v5.1.0/banks/${space()}/user_customer_links`, { user_id: userId, customer_id: customerId }, token), { added: username });
diff --git a/apps/cohort/src/routes/(protected)/staff/organisations/+page.svelte b/apps/cohort/src/routes/(protected)/staff/organisations/+page.svelte
index 2710ee63..1da6e84f 100644
--- a/apps/cohort/src/routes/(protected)/staff/organisations/+page.svelte
+++ b/apps/cohort/src/routes/(protected)/staff/organisations/+page.svelte
@@ -9,7 +9,7 @@
const locale = $derived(data.locale as Locale);
const missing = (key: keyof typeof data.lacking) => data.lacking[key];
const creatable = CREATABLE_KINDS;
- const errorText = (e: string) => (['organisation_missing', 'member_missing', 'member_not_found'].includes(e) ? t(locale, e as MessageKey) : `${t(locale, 'staff_error')} ${e}`);
+ const errorText = (e: string) => (['organisation_missing', 'member_missing', 'member_not_found', 'member_ambiguous'].includes(e) ? t(locale, e as MessageKey) : `${t(locale, 'staff_error')} ${e}`);
{t(locale, 'organisations_heading')}