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')}