From 2875833252548cc4b007fe46901a24396ae646f8 Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Thu, 23 Jul 2026 10:44:00 +0530 Subject: [PATCH 01/12] generic-bap: wire catalogPull module for the catalog-crawler plugin Adds a catalogPull handler alongside bapTxnReceiver/bapTxnCaller on the onix-bap container, serving /catalog/pull as a DS-internal trigger that fetches and verifies a provider node's manifest -> index -> catalog chain. --- generic-devkit/config/generic-bap.yaml | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/generic-devkit/config/generic-bap.yaml b/generic-devkit/config/generic-bap.yaml index 0ac5c75..03ade76 100644 --- a/generic-devkit/config/generic-bap.yaml +++ b/generic-devkit/config/generic-bap.yaml @@ -159,3 +159,56 @@ modules: - validateSchema - sign - validateAckSign + + # ───────────────────────────────────────────────────────────────── + # catalog/pull — DS-internal trigger for the catalog-crawler plugin. + # Unsigned, same-operator call (not a network-facing Beckn action): + # no validateSign/addRoute/signAck. Given a bppUri, it fetches and + # verifies that provider's manifest -> index -> catalog chain and + # returns the result synchronously. + # Listens at: http://localhost:8081/catalog/pull + # See beckn-onix's pkg/plugin/implementation/catalogcrawler/README.md + # ───────────────────────────────────────────────────────────────── + - name: catalogPull + path: /catalog/pull + handler: + type: catalogPull + role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s + plugins: + # registry+cache exist only to satisfy keyManager's constructor + # (KeyManagerProvider.New always requires a RegistryLookup); the + # crawler itself never calls either -- it resolves the provider + # node's domain directly from the DS-supplied bppUri. + registry: + id: dediregistry + config: + timeout: 10 + retry_max: 3 + retry_wait_min: 100ms + retry_wait_max: 500ms + cache: + id: cache + config: + addr: redis:6379 + keyManager: + id: simplekeymanager + config: + subscriberId: bap.example.com + keyId: 76EU7LZ7gfqj13dWDKR1Uitnim11mCoxWBPdzLxUpAMBPVdANKgyFM + signingPrivateKey: TTQMAEy0xJcoRXRNofZAwZq1c3VH6j98UUaHP7budQQ= + signingPublicKey: g/3swjI93IhZ0SScrVZapeLjU+W0AeiSid3LViYZJFo= + encrPrivateKey: TTQMAEy0xJcoRXRNofZAwZq1c3VH6j98UUaHP7budQQ= + encrPublicKey: g/3swjI93IhZ0SScrVZapeLjU+W0AeiSid3LViYZJFo= + signer: + id: signer + crawler: + id: catalogcrawler + config: + maxArtifactSize: "10485760" + fetchTimeout: "15s" + retryMax: "2" From 10b83d80a3459bf4cd91ca5911999f1cad19253f Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Thu, 23 Jul 2026 11:54:32 +0530 Subject: [PATCH 02/12] =?UTF-8?q?postman:=20add=20catalog/pull=20request?= =?UTF-8?q?=20under=20new=20"3=20=E2=80=94=20Catalog"=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses {{networkId}} and {{bpp_uri}} collection variables, consistent with the existing Discovery/Transaction requests. Adds bap_base_url since catalog/pull is a root-level path, not nested under /bap/caller like the other requests. --- ...BAPBecknStarterKit.postman_collection.json | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json b/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json index c0ddc9f..3d10609 100644 --- a/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json +++ b/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json @@ -144,6 +144,44 @@ "response": [] } ] + }, + { + "name": "3 — Catalog", + "item": [ + { + "name": "catalog pull", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"bppUri\": \"{{bpp_uri}}\",\n \"networkId\": \"{{networkId}}\",\n \"mode\": \"full\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bap_base_url}}/catalog/pull", + "host": [ + "{{bap_base_url}}" + ], + "path": [ + "catalog", + "pull" + ] + }, + "description": "DS-internal trigger for the catalog-crawler plugin: fetches and verifies the given provider node's manifest -> index -> catalog chain and returns them. Unsigned -- same-operator call, not a signed network action. Response matches beckn.yaml's CatalogPullCallbackAction shape: {status: COMPLETED, catalogs: [...]} or {status: FAILED, error: {...}}." + }, + "response": [] + } + ] } ], "event": [ @@ -218,6 +256,11 @@ "key": "transaction_id", "value": "", "description": "Transaction ID — set once in pre-request script, shared across all calls in a flow" + }, + { + "key": "bap_base_url", + "value": "http://localhost:8081", + "description": "BAP adapter host:port, no path suffix -- used by root-level paths like /catalog/pull." } ] -} \ No newline at end of file +} From b4e70a5ec8be80b7beb3a149256f3b37dc102475 Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Thu, 23 Jul 2026 12:08:11 +0530 Subject: [PATCH 03/12] postman: rename catalog/pull's bppUri to receiverId Aligns with beckn.yaml Context.receiverId naming (the PN's DID) rather than reusing bppUri. Value is still the PN's URI for now -- real DID resolution isn't implemented, this is a field-name alignment only. --- .../postman/BAPBecknStarterKit.postman_collection.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json b/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json index 3d10609..7b0c9eb 100644 --- a/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json +++ b/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json @@ -160,7 +160,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"bppUri\": \"{{bpp_uri}}\",\n \"networkId\": \"{{networkId}}\",\n \"mode\": \"full\"\n}", + "raw": "{\n \"receiverId\": \"{{bpp_uri}}\",\n \"networkId\": \"{{networkId}}\",\n \"mode\": \"full\"\n}", "options": { "raw": { "language": "json" From 201b4d21a2c125a62e14639a41f7350d32971ba3 Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Thu, 23 Jul 2026 15:39:59 +0530 Subject: [PATCH 04/12] generic: pin onix-adapter image to 1.7.5 for catalog-crawler support Pins onix-bap/onix-bpp to fidedocker/onix-adapter:1.7.5 (built from beckn-onix's catalog-crawler branch) instead of the implicit :latest, so the team gets the catalog/pull handler without rebuilding locally. --- generic-devkit/install/docker-compose-generic.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generic-devkit/install/docker-compose-generic.yml b/generic-devkit/install/docker-compose-generic.yml index 00532c6..0f18436 100644 --- a/generic-devkit/install/docker-compose-generic.yml +++ b/generic-devkit/install/docker-compose-generic.yml @@ -47,7 +47,7 @@ services: # Receiver (inbound): http://localhost:8081/bap/receiver/ # ============================================================ onix-bap: - image: fidedocker/onix-adapter + image: fidedocker/onix-adapter:1.7.5 container_name: onix-bap platform: linux/amd64 networks: @@ -70,7 +70,7 @@ services: # Receiver (inbound): http://localhost:8082/bpp/receiver/ # ============================================================ onix-bpp: - image: fidedocker/onix-adapter + image: fidedocker/onix-adapter:1.7.5 container_name: onix-bpp platform: linux/amd64 networks: From 931e1f9e508809c9a7415cd9aaacd97367394239 Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Thu, 23 Jul 2026 15:48:32 +0530 Subject: [PATCH 05/12] generic-devkit: document catalog/pull in the README Adds a "Catalog Crawler" section covering what /catalog/pull does, how to trigger it (Postman or curl, using networkId=beckn.one/testnet), and sample COMPLETED/FAILED responses -- so this is discoverable from starter-kit itself, not just beckn-onix's own plugin README. --- generic-devkit/README.md | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/generic-devkit/README.md b/generic-devkit/README.md index 80aa8ba..48cc14e 100644 --- a/generic-devkit/README.md +++ b/generic-devkit/README.md @@ -93,6 +93,58 @@ Use the **BPP collection** to simulate BPP-initiated callbacks directly (e.g. un --- +## Catalog Crawler (`catalog/pull`) + +`onix-bap` also exposes `/catalog/pull` — a DS-internal trigger that fetches and verifies a provider node's published manifest → index → catalog chain (self-hosted, DeDi-signed catalogs), independent of the discover/select/... transaction flow above. Unlike the other endpoints, it's an unsigned, same-operator call (no `Authorization` header) — see [beckn-onix's catalogcrawler README](https://github.com/beckn/beckn-onix/blob/catalog-crawler/pkg/plugin/implementation/catalogcrawler/README.md) for the full design background. + +### Trigger it + +Use the **`catalog pull`** request under the BAP collection's **`3 — Catalog`** folder, or call it directly: + +```bash +curl -X POST http://localhost:8081/catalog/pull \ + -H "Content-Type: application/json" \ + -d '{ + "receiverId": "https://angular-absently-gab.ngrok-free.dev", + "networkId": "beckn.one/testnet", + "mode": "full" + }' +``` + +`receiverId` is the provider node's address to crawl (for now, treated as a literal domain/URI, not resolved via DID). `mode` is `full` or `incremental` (currently behave identically — incremental digest-skip isn't implemented yet). + +### Sample response + +```json +{ + "status": "COMPLETED", + "catalogs": [ + { + "id": "CAT-GENERIC-001", + "descriptor": { "name": "Generic Catalog", "shortDesc": "Daily essentials generic items" }, + "provider": { "id": "PROV-EXAMPLE-01", "descriptor": { "name": "BP Pvt Ltd" }, "availableAt": [ /* ... */ ] }, + "resources": [ /* ... items, as published by the provider ... */ ], + "offers": [ /* ... offers ... */ ], + "validity": { "startDate": "2026-01-01T00:00:00Z", "endDate": "2026-12-31T23:59:59Z" }, + "isActive": true + } + ] +} +``` + +Matches beckn.yaml's `CatalogPullCallbackAction` shape exactly — `status` is always present (`COMPLETED`/`FAILED`), `catalogs` only on success. A failed crawl (unreachable provider, bad `receiverId`) still returns `200`, with the failure carried in the body instead: + +```json +{ + "status": "FAILED", + "error": { "code": "BIZ_CRAWL_FAILED", "message": "catalogcrawler: fetching manifest: ..." } +} +``` + +No per-catalog metadata (digests, versions, verification outcomes) is returned to the caller — check `docker logs onix-bap` for those details if a crawl isn't returning what you expect. + +--- + ## Configuration Reference ### Collection Variables (Postman) From d543d5a24610c38143bb747c98af99aec7b0b42a Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Thu, 23 Jul 2026 19:23:03 +0530 Subject: [PATCH 06/12] Add Beckn Catalog Index schema for the beckn-catalogs registry Reference schema for the details payload of a catalog-index DeDi file (registry.name: "beckn-catalogs"): one record per catalogId, describing its version, lifecycle status, visibility, and the digest/location of its content (plain, unsigned Beckn Catalog JSON at parts[].url). Matches the draft-07, flat, additionalProperties:false style of nfh-trust-labs/DeDi's own Beckn_subscriber.json / Beckn_subscriber_reference.json, intended as a starting point to eventually move there. --- schemas/Beckn_catalog_index.json | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 schemas/Beckn_catalog_index.json diff --git a/schemas/Beckn_catalog_index.json b/schemas/Beckn_catalog_index.json new file mode 100644 index 0000000..37e114c --- /dev/null +++ b/schemas/Beckn_catalog_index.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Beckn Catalog Index", + "description": "A registry entry describing one catalog a Beckn provider publishes: its lifecycle status, visibility, schema types, and the digest/location of its content. One record per catalogId. The actual Catalog content lives at parts[].url as plain, unsigned Beckn JSON -- its integrity is covered entirely by the digest recorded here.", + "type": "object", + "additionalProperties": false, + "properties": { + "catalogId": { + "type": "string", + "description": "Unique identifier of the catalog within the publisher's namespace." + }, + "version": { + "type": "integer", + "minimum": 1, + "description": "Monotonically increasing version counter for this catalog. MUST NOT decrease between publishes; a consumer that has already seen a higher version for this catalogId MUST reject a lower one as a rollback, independent of digest or signature validity." + }, + "catalogType": { + "type": "string", + "enum": ["MASTER", "REGULAR"], + "description": "Whether this is a MASTER catalog that other REGULAR catalogs may extend, or a REGULAR catalog." + }, + "status": { + "type": "string", + "enum": ["ACTIVE", "RETIRED"], + "description": "Lifecycle state of the catalog. RETIRED catalogs are not authoritative and SHOULD NOT be indexed by consumers." + }, + "visibility": { + "description": "Who this catalog is intended for: either the literal string \"public\" (openly readable, no access restriction), or an object naming the specific networks it is scoped to.", + "oneOf": [ + { "const": "public" }, + { + "type": "object", + "additionalProperties": false, + "required": ["networks"], + "properties": { + "networks": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "networkIds this catalog is restricted to." + } + } + } + ] + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "When this catalog's content last actually changed (distinct from a re-issue purely for freshness)." + }, + "schemaTypes": { + "type": "array", + "items": { "type": "string", "format": "uri" }, + "description": "Domain-specific JSON-LD context/schema URLs the catalog's resources and offers conform to. Optional -- omit for a catalog with no domain-specific extension." + }, + "parts": { + "type": "array", + "minItems": 1, + "description": "The catalog's content, as one or more plain Beckn Catalog JSON files.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["url", "digest", "lastModified"], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Absolute URL of the plain Catalog JSON file. May be hosted anywhere, independent of the manifest's or this index's own host." + }, + "digest": { + "type": "string", + "pattern": "^sha-256:[0-9a-f]{64}$", + "description": "SHA-256 digest of the file at url, prefixed with the algorithm name. This is the catalog file's entire integrity guarantee -- it carries no signature of its own." + }, + "lastModified": { + "type": "string", + "format": "date-time", + "description": "When this specific part was last modified." + } + } + } + } + }, + "required": ["catalogId", "version", "catalogType", "status", "visibility", "updatedAt", "parts"] +} From 52e13b46a11b0b76fbb6149f0455b10e96732aea Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Tue, 28 Jul 2026 16:28:17 +0530 Subject: [PATCH 07/12] schemas: rewrite Beckn Catalog Index to match the current file spec The published schema described a single catalog entry from a pre-file-spec design: visibility as public/{networks}, a flat parts[] list with no signature at all, no index wrapper. The current file spec's catalog index has since moved to a signed, versioned model entirely -- this rewrite matches it: - Top-level document: {participantId, version, next_update, catalogs[]}, not a single entry. - Each catalog entry is either ACTIVE (catalogType, optional networkIds/ authMethods/schemaTypes/crawlHint, baseline + changes) or RETIRED (catalogId, status, retiredAt only, enforced via if/then/else so a tombstone can't also carry files). - baseline/changes entries are file-entry objects requiring version, with url/size/digest/signature only required together -- allowing the spec's lightweight forward-pointer form ({"version": 43}, no file yet). - Each file's signature is its own object {keyId, value, validUntil} -- the tuple it signs (per its description) now includes size, matching the file spec's binding update. - authMethods reshaped to the current method/header/signedHeaders/ freshnessSeconds form (no more algorithm/challenge fields). Verified against the file spec's own worked example (all three catalog entries -- ACTIVE with a forward-pointer change, ACTIVE restricted with authMethods, and a RETIRED tombstone) and against a battery of documents that should be rejected (the old single-entry shape, a RETIRED entry carrying a baseline, a fileEntry missing digest, a bad digest pattern, and the old authMethods shape). --- schemas/Beckn_catalog_index.json | 213 ++++++++++++++++++++++--------- 1 file changed, 155 insertions(+), 58 deletions(-) diff --git a/schemas/Beckn_catalog_index.json b/schemas/Beckn_catalog_index.json index 37e114c..75a88e9 100644 --- a/schemas/Beckn_catalog_index.json +++ b/schemas/Beckn_catalog_index.json @@ -1,85 +1,182 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://schema.beckn.org/dedi/beckn-catalog-index-schema.json", "title": "Beckn Catalog Index", - "description": "A registry entry describing one catalog a Beckn provider publishes: its lifecycle status, visibility, schema types, and the digest/location of its content. One record per catalogId. The actual Catalog content lives at parts[].url as plain, unsigned Beckn JSON -- its integrity is covered entirely by the digest recorded here.", + "description": "A publisher's catalog index: a plain Beckn file (not a DeDi file, and not required to be signed as a whole -- trust rides on each catalog file's own per-entry signature). Lists every catalog the publisher has, each carrying identity, lifecycle status, visibility, and either a baseline + change-file chain (ACTIVE) or a tombstone (RETIRED).", "type": "object", "additionalProperties": false, + "required": ["participantId", "version", "catalogs"], "properties": { - "catalogId": { + "participantId": { "type": "string", - "description": "Unique identifier of the catalog within the publisher's namespace." + "description": "The publisher's identity -- a domain (e.g. \"open-economy.nfh.global\"). Every catalogId in this index is scoped under it." }, "version": { "type": "integer", "minimum": 1, - "description": "Monotonically increasing version counter for this catalog. MUST NOT decrease between publishes; a consumer that has already seen a higher version for this catalogId MUST reject a lower one as a rollback, independent of digest or signature validity." + "description": "Monotonically increasing version counter for the index as a whole -- a crawler's cursor. MUST NOT decrease between publishes; distinct from any one catalog's own baseline/change-file versions." }, - "catalogType": { - "type": "string", - "enum": ["MASTER", "REGULAR"], - "description": "Whether this is a MASTER catalog that other REGULAR catalogs may extend, or a REGULAR catalog." - }, - "status": { - "type": "string", - "enum": ["ACTIVE", "RETIRED"], - "description": "Lifecycle state of the catalog. RETIRED catalogs are not authoritative and SHOULD NOT be indexed by consumers." - }, - "visibility": { - "description": "Who this catalog is intended for: either the literal string \"public\" (openly readable, no access restriction), or an object naming the specific networks it is scoped to.", - "oneOf": [ - { "const": "public" }, - { - "type": "object", - "additionalProperties": false, - "required": ["networks"], - "properties": { - "networks": { - "type": "array", - "items": { "type": "string" }, - "minItems": 1, - "description": "networkIds this catalog is restricted to." - } - } - } - ] - }, - "updatedAt": { + "next_update": { "type": "string", "format": "date-time", - "description": "When this catalog's content last actually changed (distinct from a re-issue purely for freshness)." - }, - "schemaTypes": { - "type": "array", - "items": { "type": "string", "format": "uri" }, - "description": "Domain-specific JSON-LD context/schema URLs the catalog's resources and offers conform to. Optional -- omit for a catalog with no domain-specific extension." + "description": "Bounds how long any cached copy of this index may be believed; a crawler past this MUST re-fetch before relying on it." }, - "parts": { + "catalogs": { "type": "array", - "minItems": 1, - "description": "The catalog's content, as one or more plain Beckn Catalog JSON files.", - "items": { - "type": "object", + "description": "One entry per catalogId the publisher has ever published, including retired ones (kept as tombstones so a crawler can distinguish \"gone\" from \"never existed\").", + "items": { "$ref": "#/$defs/catalogEntry" } + } + }, + "$defs": { + "catalogEntry": { + "type": "object", + "required": ["catalogId", "status"], + "properties": { + "catalogId": { + "type": "string", + "description": "Unique identifier of the catalog, participant-scoped (e.g. \"open-economy.nfh.global/electronics-2026\")." + }, + "status": { + "type": "string", + "enum": ["ACTIVE", "RETIRED"], + "description": "Lifecycle state. RETIRED is a tombstone -- no files, no baseline, no changes." + } + }, + "if": { + "properties": { "status": { "const": "RETIRED" } } + }, + "then": { "additionalProperties": false, - "required": ["url", "digest", "lastModified"], + "required": ["catalogId", "status", "retiredAt"], "properties": { - "url": { + "catalogId": true, + "status": true, + "retiredAt": { "type": "string", - "format": "uri", - "description": "Absolute URL of the plain Catalog JSON file. May be hosted anywhere, independent of the manifest's or this index's own host." - }, - "digest": { + "format": "date-time", + "description": "When this catalog was retired." + } + } + }, + "else": { + "additionalProperties": false, + "required": ["catalogId", "status", "catalogType", "baseline"], + "properties": { + "catalogId": true, + "status": true, + "catalogType": { "type": "string", - "pattern": "^sha-256:[0-9a-f]{64}$", - "description": "SHA-256 digest of the file at url, prefixed with the algorithm name. This is the catalog file's entire integrity guarantee -- it carries no signature of its own." + "enum": ["MASTER", "REGULAR"], + "description": "Whether this is a MASTER catalog that other REGULAR catalogs may extend, or a REGULAR catalog." + }, + "networkIds": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Networks this catalog is restricted to. Absent or omitted means public -- there is no explicit \"public\" marker, the field is simply not present." + }, + "authMethods": { + "type": "array", + "items": { "$ref": "#/$defs/authMethod" }, + "description": "How a crawler authenticates a restricted download. Only meaningful when networkIds is present. A list, not a single value, so new methods extend without changing the shape." }, - "lastModified": { + "schemaTypes": { + "type": "array", + "items": { "type": "string", "format": "uri" }, + "description": "Domain-specific JSON-LD context/schema URLs the catalog's resources and offers conform to. Optional -- omit for a catalog with no domain-specific extension." + }, + "crawlHint": { "type": "string", - "format": "date-time", - "description": "When this specific part was last modified." + "enum": ["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"], + "description": "A suggested crawl frequency, like a sitemap's changefreq. Purely advisory -- a crawler stays in control of its own schedule and budget; this hint may be honored for fast-moving catalogs, never treated as authoritative." + }, + "baseline": { "$ref": "#/$defs/fileEntry" }, + "changes": { + "type": "array", + "items": { "$ref": "#/$defs/fileEntry" }, + "description": "Change files since the baseline, in version order. May include a lightweight forward-pointer entry carrying only \"version\" (no url/digest/signature yet), signaling a newer version exists before its file is ready to serve." } } } + }, + "fileEntry": { + "type": "object", + "additionalProperties": false, + "required": ["version"], + "properties": { + "version": { + "type": "integer", + "minimum": 1, + "description": "This file's version. Monotonic within its catalogId, independent of the index's own version." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Absolute, immutable URL of this file. A new version is always a new URL -- nothing is overwritten in place." + }, + "size": { + "type": "integer", + "minimum": 0, + "description": "Byte size of the file at url. Required whenever url/digest/signature are present -- part of the signed tuple (see signature), and what makes the fetch-baseline-instead cutover rule computable before downloading anything." + }, + "digest": { + "type": "string", + "pattern": "^sha-256:[0-9a-f]{64}$", + "description": "SHA-256 digest of the file at url, prefixed with the algorithm name." + }, + "signature": { "$ref": "#/$defs/signature" } + }, + "dependencies": { + "url": ["size", "digest", "signature"], + "size": ["url", "digest", "signature"], + "digest": ["url", "size", "signature"], + "signature": ["url", "size", "digest"] + } + }, + "signature": { + "type": "object", + "additionalProperties": false, + "required": ["keyId", "value", "validUntil"], + "properties": { + "keyId": { + "type": "string", + "description": "Identifies the publisher key this signature was made with (matches a kid in the manifest's keys[])." + }, + "value": { + "type": "string", + "description": "Signature value. Ed25519 (or ES256) over the JCS canonicalization of {catalogId, version, url, size, digest, validUntil} for the file entry this signature is attached to -- a tuple, not a bare hash, so it cannot be replayed for a different file, version, or past its validity window. May equally be encoded as a detached JWS instead of a plain signature value; the encoding is a schema decision, not a semantic one." + }, + "validUntil": { + "type": "string", + "format": "date-time", + "description": "Caps how long this signed entry may be presented, independent of the index's own next_update." + } + } + }, + "authMethod": { + "type": "object", + "additionalProperties": false, + "required": ["method", "header", "signedHeaders", "freshnessSeconds"], + "properties": { + "method": { + "type": "string", + "description": "e.g. \"signed-request\" -- self-describing; a crawler reads this object and knows what to build. Extend the enum as new methods are added; do not change this shape to add one." + }, + "header": { + "type": "string", + "description": "The HTTP header the crawler's signature goes in (e.g. \"Authorization\")." + }, + "signedHeaders": { + "type": "array", + "items": { "type": "string" }, + "description": "The standard Beckn HTTP Signature's signed pseudo-headers/headers (e.g. [\"(created)\", \"(expires)\", \"(request-target)\", \"host\", \"digest\"]). No custom challenge fields -- the signature standard's own request binding is what ties a signature to one URL. The verification algorithm itself is never declared here; a gateway takes it from the requester's registered key." + }, + "freshnessSeconds": { + "type": "integer", + "minimum": 1, + "description": "Replay bound: how long after (created) a signed request remains acceptable. 60 is the spec's suggested default for a machine-to-machine fetch." + } + } } - }, - "required": ["catalogId", "version", "catalogType", "status", "visibility", "updatedAt", "parts"] + } } From 25b5501e37e8ae36154b60da49a35877b3127551 Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Wed, 29 Jul 2026 08:16:52 +0530 Subject: [PATCH 08/12] feat(generic-devkit): add catalog/publish devkit config for generic BPP Wires the catalog-publisher plugin into the generic BPP devkit: new catalogPublish module in generic-bpp.yaml, docker-compose updates, Postman requests for the /catalog/publish endpoint, and README docs. Tested end-to-end against the generic devkit. --- .gitignore | 1 + generic-devkit/README.md | 49 +++++++++++++++- generic-devkit/config/generic-bpp.yaml | 58 +++++++++++++++++++ .../install/docker-compose-generic-local.yml | 32 ++++++++++ ...BPPBecknStarterKit.postman_collection.json | 28 +++++---- 5 files changed, 155 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 076e35b..f6bdca9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store generic-devkit/postman/.DS_Store generic-devkit/install/ngrok.yml +generic-devkit/data/ diff --git a/generic-devkit/README.md b/generic-devkit/README.md index 48cc14e..407925e 100644 --- a/generic-devkit/README.md +++ b/generic-devkit/README.md @@ -10,7 +10,7 @@ select → on_select → init → on_init → confirm → on_confirm → status / on_status → track / on_track → update / on_update → cancel / on_cancel → rate / on_rate → support / on_support -(+ catalog/publish → catalog/on_publish) +(+ catalog/pull, + catalog/publish -- both DS-internal triggers, not part of the signed transaction flow above) ``` --- @@ -80,7 +80,6 @@ Use the **BAP collection** to drive the transaction lifecycle in order: | 8 | `cancel` | Fulfillment | | 9 | `rate` | Post-Fulfillment | | 10 | `support` | Post-Fulfillment | -| 11 | `catalog/publish` | Catalog Publishing | Each request returns an `ACK`. The corresponding `on_*` callback from the BPP arrives at `sandbox-bap` and can be viewed in the BAP logs: @@ -145,6 +144,52 @@ No per-catalog metadata (digests, versions, verification outcomes) is returned t --- +## Catalog Publisher (`catalog/publish`) + +`onix-bpp` exposes `/catalog/publish` — a DS-internal trigger that publishes one or more plain Beckn Catalog objects: it diffs each against what was last published (producing a fresh baseline, an incremental change file, or a no-op), signs the result, and writes a manifest + catalog index under the handler's `outputRoot` (`/catalog` in the container, `generic-devkit/data/catalog` on the host — see `docker-compose-generic-local.yml`). Like `catalog/pull`, this is an unsigned, same-operator call, **not** the full signed, async `catalog/publish` Beckn action beckn.yaml describes (context/action envelope, routing, `on_publish` callback) — that is a materially larger scope this devkit does not implement yet. See [beckn-onix's catalogpublisher README](https://github.com/beckn/beckn-onix/blob/catalog-publisher/pkg/plugin/implementation/catalogpublisher/README.md) for the full design background. + +### Trigger it + +Use the **`publish`** request under the BPP collection's **`2 — Catalog Publishing`** folder, or call it directly: + +```bash +curl -X POST http://localhost:8082/catalog/publish \ + -H "Content-Type: application/json" \ + -d '{ + "catalogs": [ + { "id": "bpp.example.com/CAT-GENERIC-001", "descriptor": { "name": "Generic Catalog" }, "provider": { "id": "PROV-EXAMPLE-01" }, "resources": [ /* ... */ ] } + ] + }' +``` + +Each catalog's own top-level `"id"` is used verbatim as its catalogId — it is not derived from a domain, so submit the full id you want published. `retire` (a list of catalogIds) and `forceBaseline` (bypass diffing, publish a fresh baseline) are also accepted at the top level, alongside or instead of `catalogs`. + +### Sample response + +```json +{ + "status": "COMPLETED", + "results": [ + { "catalogId": "bpp.example.com/CAT-GENERIC-001", "status": "ACCEPTED", "version": 1 } + ] +} +``` + +`status` is always present (`COMPLETED`/`FAILED` for the call as a whole); each entry in `results` borrows beckn.yaml's `CatalogProcessingResult` vocabulary (`ACCEPTED`/`REJECTED`) per catalog — a bad submission (e.g. missing `id`) is `REJECTED` with a `reason`, without failing the rest of the batch: + +```json +{ + "status": "COMPLETED", + "results": [ + { "catalogId": "", "status": "REJECTED", "reason": "missing catalogId" } + ] +} +``` + +A fatal failure (e.g. signing failure) returns `200` with `status: FAILED` and an `error` object instead, matching `catalog/pull`'s convention. Publishing the same catalogId again with edited `resources`/`offers` produces an incremental change file and bumps its version instead of a fresh baseline; publishing it unchanged is a no-op. Inspect `generic-devkit/data/catalog/` on the host to see the generated manifest, catalog index, and versioned catalog files directly. + +--- + ## Configuration Reference ### Collection Variables (Postman) diff --git a/generic-devkit/config/generic-bpp.yaml b/generic-devkit/config/generic-bpp.yaml index 31447e2..e7a54ed 100644 --- a/generic-devkit/config/generic-bpp.yaml +++ b/generic-devkit/config/generic-bpp.yaml @@ -160,3 +160,61 @@ modules: - validateSchema - sign - validateAckSign + + # ───────────────────────────────────────────────────────────────── + # catalog/publish — DS-internal trigger for the catalog-publisher + # plugin. Unsigned, same-operator call (not a network-facing Beckn + # action): no validateSign/addRoute/signAck. Given one or more plain + # Beckn Catalog objects, it diffs against what was last published + # (baseline vs. incremental change file), signs the result, and + # writes it under outputRoot. Public catalogs only in this phase. + # Listens at: http://localhost:8082/catalog/publish + # See beckn-onix's pkg/plugin/implementation/catalogpublisher/README.md + # ───────────────────────────────────────────────────────────────── + - name: catalogPublish + path: /catalog/publish + handler: + type: catalogPublish + role: bpp + outputRoot: /catalog + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s + plugins: + # registry+cache exist only to satisfy keyManager's constructor + # (KeyManagerProvider.New always requires a RegistryLookup); the + # publisher itself never calls either. + registry: + id: dediregistry + config: + timeout: 10 + retry_max: 3 + retry_wait_min: 100ms + retry_wait_max: 500ms + cache: + id: cache + config: + addr: redis:6379 + keyManager: + id: simplekeymanager + config: + subscriberId: bpp.example.com + keyId: 76EU7ofwRCF1aobQkShARrf1PAUsNpHqWUJoynPu9w45YFKmzqaPmy + signingPrivateKey: 393fVJJsfqssJCRAjedKR2cKAjSLrjU3t+bICQ5KzYM= + signingPublicKey: CqVy97DW45bcZPPrWIYGe2ldl9C93NFeVciiAEYsvR0= + encrPrivateKey: 393fVJJsfqssJCRAjedKR2cKAjSLrjU3t+bICQ5KzYM= + encrPublicKey: CqVy97DW45bcZPPrWIYGe2ldl9C93NFeVciiAEYsvR0= + catalogPublisher: + id: catalogpublisher + config: + # No subscriberId here -- the handler derives it automatically + # from keyManager.config.subscriberId above (the single source + # of truth for which keyset gets loaded). Set it explicitly + # here only if it must legitimately differ from keyManager's. + # indexSchemaURL is likewise not configurable -- it's a + # hardcoded package constant pending a shared becknconstants + # home for canonical schema URLs. + nextUpdateIn: "336h" + fileValidityIn: "336h" diff --git a/generic-devkit/install/docker-compose-generic-local.yml b/generic-devkit/install/docker-compose-generic-local.yml index f339e0e..cab9562 100644 --- a/generic-devkit/install/docker-compose-generic-local.yml +++ b/generic-devkit/install/docker-compose-generic-local.yml @@ -1,4 +1,30 @@ services: + # ============================================================ + # beckn-router — Caddy reverse proxy, single entry point for + # both adapters. Exposes port 9000 on the host. + # + # /bap/* → onix-bap:8081 + # /bpp/* → onix-bpp:8082 + # + # For public-internet callbacks, tunnel port 9000 via ngrok: + # cp ngrok.yml.example ngrok.yml # fill in authtoken + domain + # ngrok start --all --config ngrok.yml + # Then set PUBLIC_URL in your Postman collection to the tunnel URL. + # ============================================================ + beckn-router: + image: caddy:alpine + container_name: beckn-router + ports: + - "9000:9000" + networks: + - beckn_network + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + depends_on: + - onix-bap + - onix-bpp + restart: unless-stopped + # ============================================================ # Redis — shared cache for both adapters # ============================================================ @@ -42,6 +68,11 @@ services: # onix-bpp — Beckn adapter acting as BPP # Caller (outbound): http://localhost:8082/bpp/caller/ # Receiver (inbound): http://localhost:8082/bpp/receiver/ + # Catalog publish (internal trigger): http://localhost:8082/catalog/publish + # -- writes manifest/index/catalog files under /catalog in the + # container; ../data/catalog on the host, so they persist across + # `docker compose down`/`up` and can be inspected or moved + # elsewhere. # ============================================================ onix-bpp: image: beckn-onix:latest @@ -59,6 +90,7 @@ services: REDIS_ADDR: redis:6379 volumes: - ../config:/app/config + - ../data/catalog:/catalog command: ["./server", "--config=/app/config/generic-bpp.yaml"] # ============================================================ diff --git a/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json b/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json index 828f6ef..511503f 100644 --- a/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json +++ b/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json @@ -8,7 +8,7 @@ }, "item": [ { - "name": "1 — Transaction", + "name": "1 \u2014 Transaction", "item": [ { "name": "on_select", @@ -109,7 +109,7 @@ ] }, { - "name": "2 — Catalog Publishing", + "name": "2 \u2014 Catalog Publishing", "item": [ { "name": "publish", @@ -123,7 +123,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"beckn.one/testnet\",\n \"action\": \"catalog/publish\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"catalogs\": [\n {\n \"id\": \"CAT-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Generic Catalog\",\n \"shortDesc\": \"Daily essentials generic items\"\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n \"resources\": [\n {\n \"id\": \"ITEM-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Bru Gold Instant Coffee Powder\",\n \"shortDesc\": \"Rich and aromatic instant coffee powder, 200g\",\n \"longDesc\": \"Bru Gold is a premium blend of coffee and chicory, crafted for those who love a rich, flavourful cup. Made from handpicked coffee beans roasted to perfection, this instant coffee powder dissolves quickly to give you a smooth, aromatic brew every time. Whether you prefer your coffee black or with milk, Bru Gold delivers a satisfying coffee experience. Perfect for filter coffee, cappuccino, or a classic South Indian style coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://www.jiomart.com/images/product/original/599173064/599173064.jpg\",\n \"mimeType\": \"image/jpeg\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 18200,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n {\n \"id\": \"ITEM-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Classic Item\",\n \"shortDesc\": \"Pure soluble coffee, bold and rich, 100g\",\n \"longDesc\": \"Nescafe Classic is made from 100% pure coffee beans, carefully selected and roasted to deliver a bold, rich flavour in every cup. This instant coffee dissolves instantly in hot or cold water, making it ideal for a quick coffee fix anytime. The deep roast brings out natural coffee aromas that energise your mornings. Loved by coffee enthusiasts across India, Nescafe Classic is the trusted choice for a consistently great cup of coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://m.media-amazon.com/images/S/aplus-media/vc/bb6a0196-cad0-4395-b85e-134ef725c0f7._CR0,0,1251,1251_PT0_SX300__.png\",\n \"mimeType\": \"image/png\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-02\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 32500,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 76.6394,\n 12.2958\n ]\n },\n \"address\": {\n \"streetAddress\": \"12 Sayyaji Rao Road, Devaraja Market\",\n \"addressLocality\": \"Mysore\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"570001\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n }\n ],\n \"offers\": [\n {\n \"id\": \"OFFER-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Bundle\",\n \"shortDesc\": \"Bru Gold + Nescafe Classic at 15% off - your morning coffee essentials\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-001\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n },\n {\n \"id\": \"OFFER-GROCERY-STAPLES-SAVER-002\",\n \"descriptor\": {\n \"name\": \"Hot Beverages Pack\",\n \"shortDesc\": \"Nescafe Classic + Tata Tea Premium combo - 10% off on your daily beverages\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-002\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n }\n ],\n \"publishDirectives\": {\n \"catalogType\": \"regular\"\n }\n }\n ]\n }\n}", + "raw": "{\n \"catalogs\": [\n {\n \"id\": \"bpp.example.com/CAT-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Generic Catalog\",\n \"shortDesc\": \"Daily essentials generic items\"\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n \"resources\": [\n {\n \"id\": \"ITEM-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Bru Gold Instant Coffee Powder\",\n \"shortDesc\": \"Rich and aromatic instant coffee powder, 200g\",\n \"longDesc\": \"Bru Gold is a premium blend of coffee and chicory, crafted for those who love a rich, flavourful cup. Made from handpicked coffee beans roasted to perfection, this instant coffee powder dissolves quickly to give you a smooth, aromatic brew every time. Whether you prefer your coffee black or with milk, Bru Gold delivers a satisfying coffee experience. Perfect for filter coffee, cappuccino, or a classic South Indian style coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://www.jiomart.com/images/product/original/599173064/599173064.jpg\",\n \"mimeType\": \"image/jpeg\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 18200,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n {\n \"id\": \"ITEM-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Classic Item\",\n \"shortDesc\": \"Pure soluble coffee, bold and rich, 100g\",\n \"longDesc\": \"Nescafe Classic is made from 100% pure coffee beans, carefully selected and roasted to deliver a bold, rich flavour in every cup. This instant coffee dissolves instantly in hot or cold water, making it ideal for a quick coffee fix anytime. The deep roast brings out natural coffee aromas that energise your mornings. Loved by coffee enthusiasts across India, Nescafe Classic is the trusted choice for a consistently great cup of coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://m.media-amazon.com/images/S/aplus-media/vc/bb6a0196-cad0-4395-b85e-134ef725c0f7._CR0,0,1251,1251_PT0_SX300__.png\",\n \"mimeType\": \"image/png\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-02\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 32500,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 76.6394,\n 12.2958\n ]\n },\n \"address\": {\n \"streetAddress\": \"12 Sayyaji Rao Road, Devaraja Market\",\n \"addressLocality\": \"Mysore\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"570001\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n }\n ],\n \"offers\": [\n {\n \"id\": \"OFFER-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Bundle\",\n \"shortDesc\": \"Bru Gold + Nescafe Classic at 15% off - your morning coffee essentials\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-001\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n },\n {\n \"id\": \"OFFER-GROCERY-STAPLES-SAVER-002\",\n \"descriptor\": {\n \"name\": \"Hot Beverages Pack\",\n \"shortDesc\": \"Nescafe Classic + Tata Tea Premium combo - 10% off on your daily beverages\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-002\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n }\n ]\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -131,15 +131,16 @@ } }, "url": { - "raw": "{{bpp_adapter_url}}/publish", + "raw": "{{bpp_base_url}}/catalog/publish", "host": [ - "{{bpp_adapter_url}}" + "{{bpp_base_url}}" ], "path": [ + "catalog", "publish" ] }, - "description": "Network returns catalog processing result after publish." + "description": "DS-internal trigger for the catalog-publisher plugin: given one or more plain Beckn Catalog objects (no context/message envelope), diffs each against what was last published (baseline vs. incremental change file), signs the result, and writes it under the handler's outputRoot. Unsigned -- same-operator call, not a signed network action. Response: {status: COMPLETED, results: [{catalogId, status: ACCEPTED|REJECTED, version, reason}]} or {status: FAILED, error: {...}} on a fatal failure. Public catalogs only in this phase (no networkIds/authMethods)." }, "response": [] } @@ -153,7 +154,7 @@ "type": "text/javascript", "requests": {}, "exec": [ - "// Beckn Protocol v2.0.0 — BPP Generic Collection", + "// Beckn Protocol v2.0.0 \u2014 BPP Generic Collection", "// Pre-request: set transaction_id to match the BAP collection for end-to-end testing", "if (!pm.collectionVariables.get('transaction_id')) {", " pm.collectionVariables.set('transaction_id', pm.variables.replaceIn('{{$guid}}'));", @@ -199,7 +200,7 @@ { "key": "bap_uri", "value": "{{public_url}}/bap/receiver", - "description": "BAP receiver callback URI — derived from public_url via beckn-router" + "description": "BAP receiver callback URI \u2014 derived from public_url via beckn-router" }, { "key": "bpp_id", @@ -209,7 +210,7 @@ { "key": "bpp_uri", "value": "{{public_url}}/bpp/receiver", - "description": "BPP receiver callback URI — derived from public_url via beckn-router" + "description": "BPP receiver callback URI \u2014 derived from public_url via beckn-router" }, { "key": "networkId", @@ -219,7 +220,12 @@ { "key": "transaction_id", "value": "", - "description": "Transaction ID — set once in pre-request script, shared across all calls in a flow" + "description": "Transaction ID \u2014 set once in pre-request script, shared across all calls in a flow" + }, + { + "key": "bpp_base_url", + "value": "http://localhost:8082", + "description": "BPP adapter host:port, no path suffix -- used by root-level paths like /catalog/publish." } ] -} \ No newline at end of file +} From 3696ec9a27d9b91e857abf1b20f71ce4e10d618a Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Wed, 29 Jul 2026 09:09:35 +0530 Subject: [PATCH 09/12] feat(generic-devkit): serve catalog-publish output over the existing ngrok tunnel Adds a /catalog/* static route to beckn-router's Caddyfile, serving ../data/catalog (onix-bpp's catalogPublish outputRoot) read-only through the same port-9000 tunnel the BAP/BPP routes already use -- no second port or tunnel needed. Sets catalogBaseURL in generic-bpp.yaml to that tunnel's URL + /catalog so generated manifest/index/catalog files carry real, publicly fetchable URLs instead of the pending-artifact-store:// placeholder. --- generic-devkit/config/generic-bpp.yaml | 5 +++++ generic-devkit/install/Caddyfile | 13 +++++++++++-- .../install/docker-compose-generic-local.yml | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/generic-devkit/config/generic-bpp.yaml b/generic-devkit/config/generic-bpp.yaml index e7a54ed..d961cd4 100644 --- a/generic-devkit/config/generic-bpp.yaml +++ b/generic-devkit/config/generic-bpp.yaml @@ -218,3 +218,8 @@ modules: # home for canonical schema URLs. nextUpdateIn: "336h" fileValidityIn: "336h" + # catalogBaseURL: the one public URL prefix outputRoot is + # served under -- beckn-router's ngrok tunnel, at the /catalog/* + # route added to install/Caddyfile (serves ../data/catalog + # statically). Update this if your ngrok domain changes. + catalogBaseURL: "https://angular-absently-gab.ngrok-free.dev/catalog" diff --git a/generic-devkit/install/Caddyfile b/generic-devkit/install/Caddyfile index d9bd46c..0d74b36 100644 --- a/generic-devkit/install/Caddyfile +++ b/generic-devkit/install/Caddyfile @@ -5,8 +5,12 @@ # beckn-router — single public entry point for both adapters # Port 9000 is mapped to the host; ngrok tunnels this one port. # -# /bap/* → onix-bap:8081 (BAP receiver and caller) -# /bpp/* → onix-bpp:8082 (BPP receiver and caller) +# /bap/* → onix-bap:8081 (BAP receiver and caller) +# /bpp/* → onix-bpp:8082 (BPP receiver and caller) +# /catalog/* → static files under ../data/catalog (catalogPublish's +# outputRoot) -- the manifest/index/catalog files a crawler fetches. +# Mirrors outputRoot's own layout 1:1, so catalogBaseURL in +# generic-bpp.yaml is this tunnel's URL + "/catalog". :9000 { handle /bap/* { reverse_proxy onix-bap:8081 @@ -14,4 +18,9 @@ handle /bpp/* { reverse_proxy onix-bpp:8082 } + handle /catalog/* { + root * /srv/catalog + uri strip_prefix /catalog + file_server + } } diff --git a/generic-devkit/install/docker-compose-generic-local.yml b/generic-devkit/install/docker-compose-generic-local.yml index cab9562..a2e5fac 100644 --- a/generic-devkit/install/docker-compose-generic-local.yml +++ b/generic-devkit/install/docker-compose-generic-local.yml @@ -20,6 +20,7 @@ services: - beckn_network volumes: - ./Caddyfile:/etc/caddy/Caddyfile + - ../data/catalog:/srv/catalog:ro depends_on: - onix-bap - onix-bpp From 01585e734fd3212a15f29ff99df11fd573341bee Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Wed, 29 Jul 2026 09:51:34 +0530 Subject: [PATCH 10/12] feat(generic-devkit): enable schema validation for catalogPublish Adds schemaValidator (schemav2validator) to the catalogPublish module in generic-bpp.yaml, so submitted catalogs are validated against catalog/publish's real message.catalogs[]:Catalog[] schema before they're published. No type/location set, matching the other modules in this file, so it resolves the canonical protocol-specifications-v2 beckn.yaml via becknConstants. checkPolicy (opapolicychecker) intentionally left out for now. --- generic-devkit/config/generic-bpp.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/generic-devkit/config/generic-bpp.yaml b/generic-devkit/config/generic-bpp.yaml index d961cd4..883d08c 100644 --- a/generic-devkit/config/generic-bpp.yaml +++ b/generic-devkit/config/generic-bpp.yaml @@ -223,3 +223,15 @@ modules: # route added to install/Caddyfile (serves ../data/catalog # statically). Update this if your ngrok domain changes. catalogBaseURL: "https://angular-absently-gab.ngrok-free.dev/catalog" + # Validates submitted catalogs before they're published: the + # handler wraps them in {context:{action:"catalog/publish"}, + # message:{catalogs:[...]}} and validates that envelope against + # catalog/publish's real message.catalogs[]:Catalog[] schema (no + # type/location set -- same as bppTxnReceiver/bppTxnCaller above, + # so it resolves the canonical protocol-specifications-v2 beckn.yaml + # via becknConstants). checkPolicy (opapolicychecker) intentionally + # left out here -- not needed yet. + schemaValidator: + id: schemav2validator + config: + cacheTTL: "3600" From 467baba215f44548048dc478b93a31ba68831b98 Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Wed, 29 Jul 2026 10:57:19 +0530 Subject: [PATCH 11/12] refactor(generic-devkit): rename catalog-publish output folder/route to beckn Renames the on-host data dir, container mount, and Caddy static route for catalogPublish's output from catalog to beckn throughout: data/catalog -> data/beckn, /catalog (container) -> /beckn, /srv/catalog -> /srv/beckn, Caddyfile's /catalog/* route -> /beckn/*, and outputRoot/catalogBaseURL in generic-bpp.yaml updated to match. The /catalog/publish API path itself is unrelated and untouched. --- generic-devkit/README.md | 4 ++-- generic-devkit/config/generic-bpp.yaml | 8 ++++---- generic-devkit/install/Caddyfile | 10 +++++----- .../install/docker-compose-generic-local.yml | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/generic-devkit/README.md b/generic-devkit/README.md index 407925e..c26dc7b 100644 --- a/generic-devkit/README.md +++ b/generic-devkit/README.md @@ -146,7 +146,7 @@ No per-catalog metadata (digests, versions, verification outcomes) is returned t ## Catalog Publisher (`catalog/publish`) -`onix-bpp` exposes `/catalog/publish` — a DS-internal trigger that publishes one or more plain Beckn Catalog objects: it diffs each against what was last published (producing a fresh baseline, an incremental change file, or a no-op), signs the result, and writes a manifest + catalog index under the handler's `outputRoot` (`/catalog` in the container, `generic-devkit/data/catalog` on the host — see `docker-compose-generic-local.yml`). Like `catalog/pull`, this is an unsigned, same-operator call, **not** the full signed, async `catalog/publish` Beckn action beckn.yaml describes (context/action envelope, routing, `on_publish` callback) — that is a materially larger scope this devkit does not implement yet. See [beckn-onix's catalogpublisher README](https://github.com/beckn/beckn-onix/blob/catalog-publisher/pkg/plugin/implementation/catalogpublisher/README.md) for the full design background. +`onix-bpp` exposes `/catalog/publish` — a DS-internal trigger that publishes one or more plain Beckn Catalog objects: it diffs each against what was last published (producing a fresh baseline, an incremental change file, or a no-op), signs the result, and writes a manifest + catalog index under the handler's `outputRoot` (`/beckn` in the container, `generic-devkit/data/beckn` on the host — see `docker-compose-generic-local.yml`). Like `catalog/pull`, this is an unsigned, same-operator call, **not** the full signed, async `catalog/publish` Beckn action beckn.yaml describes (context/action envelope, routing, `on_publish` callback) — that is a materially larger scope this devkit does not implement yet. See [beckn-onix's catalogpublisher README](https://github.com/beckn/beckn-onix/blob/catalog-publisher/pkg/plugin/implementation/catalogpublisher/README.md) for the full design background. ### Trigger it @@ -186,7 +186,7 @@ Each catalog's own top-level `"id"` is used verbatim as its catalogId — it is } ``` -A fatal failure (e.g. signing failure) returns `200` with `status: FAILED` and an `error` object instead, matching `catalog/pull`'s convention. Publishing the same catalogId again with edited `resources`/`offers` produces an incremental change file and bumps its version instead of a fresh baseline; publishing it unchanged is a no-op. Inspect `generic-devkit/data/catalog/` on the host to see the generated manifest, catalog index, and versioned catalog files directly. +A fatal failure (e.g. signing failure) returns `200` with `status: FAILED` and an `error` object instead, matching `catalog/pull`'s convention. Publishing the same catalogId again with edited `resources`/`offers` produces an incremental change file and bumps its version instead of a fresh baseline; publishing it unchanged is a no-op. Inspect `generic-devkit/data/beckn/` on the host to see the generated manifest, catalog index, and versioned catalog files directly. --- diff --git a/generic-devkit/config/generic-bpp.yaml b/generic-devkit/config/generic-bpp.yaml index 883d08c..32aa0c3 100644 --- a/generic-devkit/config/generic-bpp.yaml +++ b/generic-devkit/config/generic-bpp.yaml @@ -176,7 +176,7 @@ modules: handler: type: catalogPublish role: bpp - outputRoot: /catalog + outputRoot: /beckn httpClientConfig: maxIdleConns: 1000 maxIdleConnsPerHost: 200 @@ -219,10 +219,10 @@ modules: nextUpdateIn: "336h" fileValidityIn: "336h" # catalogBaseURL: the one public URL prefix outputRoot is - # served under -- beckn-router's ngrok tunnel, at the /catalog/* - # route added to install/Caddyfile (serves ../data/catalog + # served under -- beckn-router's ngrok tunnel, at the /beckn/* + # route added to install/Caddyfile (serves ../data/beckn # statically). Update this if your ngrok domain changes. - catalogBaseURL: "https://angular-absently-gab.ngrok-free.dev/catalog" + catalogBaseURL: "https://angular-absently-gab.ngrok-free.dev/beckn" # Validates submitted catalogs before they're published: the # handler wraps them in {context:{action:"catalog/publish"}, # message:{catalogs:[...]}} and validates that envelope against diff --git a/generic-devkit/install/Caddyfile b/generic-devkit/install/Caddyfile index 0d74b36..088adc2 100644 --- a/generic-devkit/install/Caddyfile +++ b/generic-devkit/install/Caddyfile @@ -7,10 +7,10 @@ # # /bap/* → onix-bap:8081 (BAP receiver and caller) # /bpp/* → onix-bpp:8082 (BPP receiver and caller) -# /catalog/* → static files under ../data/catalog (catalogPublish's +# /beckn/* → static files under ../data/beckn (catalogPublish's # outputRoot) -- the manifest/index/catalog files a crawler fetches. # Mirrors outputRoot's own layout 1:1, so catalogBaseURL in -# generic-bpp.yaml is this tunnel's URL + "/catalog". +# generic-bpp.yaml is this tunnel's URL + "/beckn". :9000 { handle /bap/* { reverse_proxy onix-bap:8081 @@ -18,9 +18,9 @@ handle /bpp/* { reverse_proxy onix-bpp:8082 } - handle /catalog/* { - root * /srv/catalog - uri strip_prefix /catalog + handle /beckn/* { + root * /srv/beckn + uri strip_prefix /beckn file_server } } diff --git a/generic-devkit/install/docker-compose-generic-local.yml b/generic-devkit/install/docker-compose-generic-local.yml index a2e5fac..8c72eae 100644 --- a/generic-devkit/install/docker-compose-generic-local.yml +++ b/generic-devkit/install/docker-compose-generic-local.yml @@ -20,7 +20,7 @@ services: - beckn_network volumes: - ./Caddyfile:/etc/caddy/Caddyfile - - ../data/catalog:/srv/catalog:ro + - ../data/beckn:/srv/beckn:ro depends_on: - onix-bap - onix-bpp @@ -70,8 +70,8 @@ services: # Caller (outbound): http://localhost:8082/bpp/caller/ # Receiver (inbound): http://localhost:8082/bpp/receiver/ # Catalog publish (internal trigger): http://localhost:8082/catalog/publish - # -- writes manifest/index/catalog files under /catalog in the - # container; ../data/catalog on the host, so they persist across + # -- writes manifest/index/catalog files under /beckn in the + # container; ../data/beckn on the host, so they persist across # `docker compose down`/`up` and can be inspected or moved # elsewhere. # ============================================================ @@ -91,7 +91,7 @@ services: REDIS_ADDR: redis:6379 volumes: - ../config:/app/config - - ../data/catalog:/catalog + - ../data/beckn:/beckn command: ["./server", "--config=/app/config/generic-bpp.yaml"] # ============================================================ From c25a74c7f4f621eb2eda442564741bf6cddd3e3f Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Wed, 29 Jul 2026 16:32:17 +0530 Subject: [PATCH 12/12] fix(generic-devkit): remove catalogPull, wire catalog-publish for the pinned-image compose too catalogPull is being developed independently now; the beckn-onix catalog-publisher branch no longer registers that handler type, so generic-bap.yaml's catalogPull module crashed the BAP on startup ("invalid module: catalogPull"). Removes that module block and its Postman/README references. Adds manifestSubscriberId to generic-bpp.yaml's catalogPublisher config -- the DeDi-native namespace/registry/recordName identifier the node-manifest self-lookup requires, kept separate from subscriberId (used for keyset/signing) since the two have different formats. bppTxnReceiver/bppTxnCaller keep their original bpp.example.com identity (keys + Postman bpp_id variable) -- only catalogPublish's own keyManager block and manifestSubscriberId use the new staging.p-node.fabric.nfh.global test identity, so the regular select/init/confirm transaction flow keeps signing correctly. docker-compose-generic.yml (the pinned fidedocker/onix-adapter image, as opposed to -local.yml's locally-built beckn-onix:latest) was missing the volume mounts and doc comment catalog-publish needs -- added those too so it's ready once a release image with the catalogPublish handler is pinned there. --- generic-devkit/README.md | 74 +-- generic-devkit/config/generic-bap.yaml | 53 -- generic-devkit/config/generic-bpp.yaml | 33 +- .../install/docker-compose-generic.yml | 7 + ...BAPBecknStarterKit.postman_collection.json | 490 ++++++++---------- ...BPPBecknStarterKit.postman_collection.json | 458 ++++++++-------- 6 files changed, 501 insertions(+), 614 deletions(-) diff --git a/generic-devkit/README.md b/generic-devkit/README.md index c26dc7b..cd3ad7b 100644 --- a/generic-devkit/README.md +++ b/generic-devkit/README.md @@ -10,7 +10,7 @@ select → on_select → init → on_init → confirm → on_confirm → status / on_status → track / on_track → update / on_update → cancel / on_cancel → rate / on_rate → support / on_support -(+ catalog/pull, + catalog/publish -- both DS-internal triggers, not part of the signed transaction flow above) +(+ catalog/publish -- a DS-internal trigger, not part of the signed transaction flow above) ``` --- @@ -92,61 +92,9 @@ Use the **BPP collection** to simulate BPP-initiated callbacks directly (e.g. un --- -## Catalog Crawler (`catalog/pull`) - -`onix-bap` also exposes `/catalog/pull` — a DS-internal trigger that fetches and verifies a provider node's published manifest → index → catalog chain (self-hosted, DeDi-signed catalogs), independent of the discover/select/... transaction flow above. Unlike the other endpoints, it's an unsigned, same-operator call (no `Authorization` header) — see [beckn-onix's catalogcrawler README](https://github.com/beckn/beckn-onix/blob/catalog-crawler/pkg/plugin/implementation/catalogcrawler/README.md) for the full design background. - -### Trigger it - -Use the **`catalog pull`** request under the BAP collection's **`3 — Catalog`** folder, or call it directly: - -```bash -curl -X POST http://localhost:8081/catalog/pull \ - -H "Content-Type: application/json" \ - -d '{ - "receiverId": "https://angular-absently-gab.ngrok-free.dev", - "networkId": "beckn.one/testnet", - "mode": "full" - }' -``` - -`receiverId` is the provider node's address to crawl (for now, treated as a literal domain/URI, not resolved via DID). `mode` is `full` or `incremental` (currently behave identically — incremental digest-skip isn't implemented yet). - -### Sample response - -```json -{ - "status": "COMPLETED", - "catalogs": [ - { - "id": "CAT-GENERIC-001", - "descriptor": { "name": "Generic Catalog", "shortDesc": "Daily essentials generic items" }, - "provider": { "id": "PROV-EXAMPLE-01", "descriptor": { "name": "BP Pvt Ltd" }, "availableAt": [ /* ... */ ] }, - "resources": [ /* ... items, as published by the provider ... */ ], - "offers": [ /* ... offers ... */ ], - "validity": { "startDate": "2026-01-01T00:00:00Z", "endDate": "2026-12-31T23:59:59Z" }, - "isActive": true - } - ] -} -``` - -Matches beckn.yaml's `CatalogPullCallbackAction` shape exactly — `status` is always present (`COMPLETED`/`FAILED`), `catalogs` only on success. A failed crawl (unreachable provider, bad `receiverId`) still returns `200`, with the failure carried in the body instead: - -```json -{ - "status": "FAILED", - "error": { "code": "BIZ_CRAWL_FAILED", "message": "catalogcrawler: fetching manifest: ..." } -} -``` - -No per-catalog metadata (digests, versions, verification outcomes) is returned to the caller — check `docker logs onix-bap` for those details if a crawl isn't returning what you expect. - ---- - ## Catalog Publisher (`catalog/publish`) -`onix-bpp` exposes `/catalog/publish` — a DS-internal trigger that publishes one or more plain Beckn Catalog objects: it diffs each against what was last published (producing a fresh baseline, an incremental change file, or a no-op), signs the result, and writes a manifest + catalog index under the handler's `outputRoot` (`/beckn` in the container, `generic-devkit/data/beckn` on the host — see `docker-compose-generic-local.yml`). Like `catalog/pull`, this is an unsigned, same-operator call, **not** the full signed, async `catalog/publish` Beckn action beckn.yaml describes (context/action envelope, routing, `on_publish` callback) — that is a materially larger scope this devkit does not implement yet. See [beckn-onix's catalogpublisher README](https://github.com/beckn/beckn-onix/blob/catalog-publisher/pkg/plugin/implementation/catalogpublisher/README.md) for the full design background. +`onix-bpp` exposes `/catalog/publish` — a DS-internal trigger that publishes one or more plain Beckn Catalog objects: it diffs each against what was last published (producing a fresh baseline, an incremental change file, or a no-op), signs the result, and writes a manifest + catalog index under the handler's `outputRoot` (`/beckn` in the container, `generic-devkit/data/beckn` on the host — see `docker-compose-generic-local.yml`). This is an unsigned, same-operator call, **not** the full signed, async `catalog/publish` Beckn action beckn.yaml describes (context/action envelope, routing, `on_publish` callback) — that is a materially larger scope this devkit does not implement yet. See [beckn-onix's catalogpublisher README](https://github.com/beckn/beckn-onix/blob/catalog-publisher/pkg/plugin/implementation/catalogpublisher/README.md) for the full design background. ### Trigger it @@ -156,13 +104,19 @@ Use the **`publish`** request under the BPP collection's **`2 — Catalog Publis curl -X POST http://localhost:8082/catalog/publish \ -H "Content-Type: application/json" \ -d '{ - "catalogs": [ - { "id": "bpp.example.com/CAT-GENERIC-001", "descriptor": { "name": "Generic Catalog" }, "provider": { "id": "PROV-EXAMPLE-01" }, "resources": [ /* ... */ ] } - ] + "context": { "action": "catalog/publish" }, + "message": { + "catalogs": [ + { "id": "staging.p-node.fabric.nfh.global/CAT-GENERIC-001", "descriptor": { "name": "Generic Catalog" }, "provider": { "id": "PROV-EXAMPLE-01" }, "resources": [ /* ... */ ] } + ], + "publishDirectives": [ + { "catalogId": "staging.p-node.fabric.nfh.global/CAT-GENERIC-001", "visibleTo": ["beckn.one/testnet", "nfh.global/testnet"], "catalogType": "REGULAR" } + ] + } }' ``` -Each catalog's own top-level `"id"` is used verbatim as its catalogId — it is not derived from a domain, so submit the full id you want published. `retire` (a list of catalogIds) and `forceBaseline` (bypass diffing, publish a fresh baseline) are also accepted at the top level, alongside or instead of `catalogs`. +Request body matches beckn.yaml's real `CatalogPublishAction` envelope shape (`context`/`message.catalogs[]`/`message.publishDirectives[]`) — `context` only carries `action` since every other `Context` field is optional and none are meaningful for this unsigned, same-operator call. `publishDirectives[]` entries are matched to a catalog by `catalogId`; `catalogType` (`MASTER`/`REGULAR`) is required by the spec, and `visibleTo` restricts which networks may fetch that catalog (empty/omitted means public) — both map straight onto the same-named fields in the published catalog index. Each catalog's own top-level `"id"` is used verbatim as its catalogId — it is not derived from a domain, so submit the full id you want published. `retire` (a list of catalogIds) and `forceBaseline` (bypass diffing, publish a fresh baseline) are this handler's own additions with no beckn.yaml equivalent — accepted as siblings of `context`/`message`, alongside or instead of `message.catalogs`. ### Sample response @@ -170,7 +124,7 @@ Each catalog's own top-level `"id"` is used verbatim as its catalogId — it is { "status": "COMPLETED", "results": [ - { "catalogId": "bpp.example.com/CAT-GENERIC-001", "status": "ACCEPTED", "version": 1 } + { "catalogId": "staging.p-node.fabric.nfh.global/CAT-GENERIC-001", "status": "ACCEPTED", "version": 1 } ] } ``` @@ -186,7 +140,7 @@ Each catalog's own top-level `"id"` is used verbatim as its catalogId — it is } ``` -A fatal failure (e.g. signing failure) returns `200` with `status: FAILED` and an `error` object instead, matching `catalog/pull`'s convention. Publishing the same catalogId again with edited `resources`/`offers` produces an incremental change file and bumps its version instead of a fresh baseline; publishing it unchanged is a no-op. Inspect `generic-devkit/data/beckn/` on the host to see the generated manifest, catalog index, and versioned catalog files directly. +A fatal failure (e.g. signing failure) returns `200` with `status: FAILED` and an `error` object instead. Publishing the same catalogId again with edited `resources`/`offers` produces an incremental change file and bumps its version instead of a fresh baseline; publishing it unchanged is a no-op. Inspect `generic-devkit/data/beckn/` on the host to see the generated manifest, catalog index, and versioned catalog files directly. --- diff --git a/generic-devkit/config/generic-bap.yaml b/generic-devkit/config/generic-bap.yaml index 03ade76..0ac5c75 100644 --- a/generic-devkit/config/generic-bap.yaml +++ b/generic-devkit/config/generic-bap.yaml @@ -159,56 +159,3 @@ modules: - validateSchema - sign - validateAckSign - - # ───────────────────────────────────────────────────────────────── - # catalog/pull — DS-internal trigger for the catalog-crawler plugin. - # Unsigned, same-operator call (not a network-facing Beckn action): - # no validateSign/addRoute/signAck. Given a bppUri, it fetches and - # verifies that provider's manifest -> index -> catalog chain and - # returns the result synchronously. - # Listens at: http://localhost:8081/catalog/pull - # See beckn-onix's pkg/plugin/implementation/catalogcrawler/README.md - # ───────────────────────────────────────────────────────────────── - - name: catalogPull - path: /catalog/pull - handler: - type: catalogPull - role: bap - httpClientConfig: - maxIdleConns: 1000 - maxIdleConnsPerHost: 200 - idleConnTimeout: 300s - responseHeaderTimeout: 5s - plugins: - # registry+cache exist only to satisfy keyManager's constructor - # (KeyManagerProvider.New always requires a RegistryLookup); the - # crawler itself never calls either -- it resolves the provider - # node's domain directly from the DS-supplied bppUri. - registry: - id: dediregistry - config: - timeout: 10 - retry_max: 3 - retry_wait_min: 100ms - retry_wait_max: 500ms - cache: - id: cache - config: - addr: redis:6379 - keyManager: - id: simplekeymanager - config: - subscriberId: bap.example.com - keyId: 76EU7LZ7gfqj13dWDKR1Uitnim11mCoxWBPdzLxUpAMBPVdANKgyFM - signingPrivateKey: TTQMAEy0xJcoRXRNofZAwZq1c3VH6j98UUaHP7budQQ= - signingPublicKey: g/3swjI93IhZ0SScrVZapeLjU+W0AeiSid3LViYZJFo= - encrPrivateKey: TTQMAEy0xJcoRXRNofZAwZq1c3VH6j98UUaHP7budQQ= - encrPublicKey: g/3swjI93IhZ0SScrVZapeLjU+W0AeiSid3LViYZJFo= - signer: - id: signer - crawler: - id: catalogcrawler - config: - maxArtifactSize: "10485760" - fetchTimeout: "15s" - retryMax: "2" diff --git a/generic-devkit/config/generic-bpp.yaml b/generic-devkit/config/generic-bpp.yaml index 32aa0c3..c2e9d3c 100644 --- a/generic-devkit/config/generic-bpp.yaml +++ b/generic-devkit/config/generic-bpp.yaml @@ -44,8 +44,6 @@ modules: keyManager: id: simplekeymanager config: - # Testnet sandbox subscriber registered on beckn.one/testnet - # Replace with your own registered subscriber ID and keys for production subscriberId: bpp.example.com keyId: 76EU7ofwRCF1aobQkShARrf1PAUsNpHqWUJoynPu9w45YFKmzqaPmy signingPrivateKey: 393fVJJsfqssJCRAjedKR2cKAjSLrjU3t+bICQ5KzYM= @@ -200,12 +198,12 @@ modules: keyManager: id: simplekeymanager config: - subscriberId: bpp.example.com - keyId: 76EU7ofwRCF1aobQkShARrf1PAUsNpHqWUJoynPu9w45YFKmzqaPmy - signingPrivateKey: 393fVJJsfqssJCRAjedKR2cKAjSLrjU3t+bICQ5KzYM= - signingPublicKey: CqVy97DW45bcZPPrWIYGe2ldl9C93NFeVciiAEYsvR0= - encrPrivateKey: 393fVJJsfqssJCRAjedKR2cKAjSLrjU3t+bICQ5KzYM= - encrPublicKey: CqVy97DW45bcZPPrWIYGe2ldl9C93NFeVciiAEYsvR0= + subscriberId: staging.p-node.fabric.nfh.global + keyId: 76EU7aSyU6nCGC9i2svAkZUwJvCpBx92LvBrAZR48YVtRg3SjH86vM + signingPrivateKey: psy4wPmeHbD7rkF3JIYL2hbNien62BNCtr6T1C2DoaI= + signingPublicKey: gFoAD0w7SgnB3UaqMr7wK0uq4J0XWmfo4g+WPyhIXgY= + encrPrivateKey: psy4wPmeHbD7rkF3JIYL2hbNien62BNCtr6T1C2DoaI= + encrPublicKey: gFoAD0w7SgnB3UaqMr7wK0uq4J0XWmfo4g+WPyhIXgY= catalogPublisher: id: catalogpublisher config: @@ -223,6 +221,14 @@ modules: # route added to install/Caddyfile (serves ../data/beckn # statically). Update this if your ngrok domain changes. catalogBaseURL: "https://angular-absently-gab.ngrok-free.dev/beckn" + # manifestSubscriberId: the DeDi-native namespace/registry/ + # recordName path for this node's manifest record -- NOT the + # same as keyManager.config.subscriberId above (a plain Beckn + # subscriberId used for keyset lookup/signing; setting *that* + # to a 3-part path would break signing instead). Used only by + # the manifestLoader check below. Replace with your real + # record's namespace/registry/recordName. + manifestSubscriberId: "nfh.global/staging-nodes/staging-p-node" # Validates submitted catalogs before they're published: the # handler wraps them in {context:{action:"catalog/publish"}, # message:{catalogs:[...]}} and validates that envelope against @@ -235,3 +241,14 @@ modules: id: schemav2validator config: cacheTTL: "3600" + # Read-only: checks (via dediregistry, using catalogPublisher. + # config.manifestSubscriberId above -- same self-lookup + # schemaversionmediator already does at cold start, via its own + # nodeId config) whether the node manifest already declares this + # publisher's catalog index under catalog.catalogIndexes. If not, a + # proposed updated manifest is staged locally under + # outputRoot/index/node-manifest.staged.yaml and a warning is + # returned -- the real manifest on DeDi is never written to by + # this handler; review the staged file and push it yourself. + manifestLoader: + id: manifestloader diff --git a/generic-devkit/install/docker-compose-generic.yml b/generic-devkit/install/docker-compose-generic.yml index 0f18436..e4ce38f 100644 --- a/generic-devkit/install/docker-compose-generic.yml +++ b/generic-devkit/install/docker-compose-generic.yml @@ -20,6 +20,7 @@ services: - beckn_network volumes: - ./Caddyfile:/etc/caddy/Caddyfile + - ../data/beckn:/srv/beckn:ro depends_on: - onix-bap - onix-bpp @@ -68,6 +69,11 @@ services: # onix-bpp — Beckn adapter acting as BPP # Caller (outbound): http://localhost:8082/bpp/caller/ # Receiver (inbound): http://localhost:8082/bpp/receiver/ + # Catalog publish (internal trigger): http://localhost:8082/catalog/publish + # -- writes manifest/index/catalog files under /beckn in the + # container; ../data/beckn on the host, so they persist across + # `docker compose down`/`up` and can be inspected or moved + # elsewhere. # ============================================================ onix-bpp: image: fidedocker/onix-adapter:1.7.5 @@ -85,6 +91,7 @@ services: REDIS_ADDR: redis:6379 volumes: - ../config:/app/config + - ../data/beckn:/beckn command: ["./server", "--config=/app/config/generic-bpp.yaml"] # ============================================================ diff --git a/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json b/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json index 7b0c9eb..12506ab 100644 --- a/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json +++ b/generic-devkit/postman/BAPBecknStarterKit.postman_collection.json @@ -1,266 +1,228 @@ { - "info": { - "_postman_id": "9094f707-1fb4-4bf0-8c92-6f53041a1181", - "name": "BAP Beckn Starter Kit", - "description": "Usecase-agnostic BAP requests for all Beckn Protocol v2.0.0 actions. All schema objects carry JSON-LD @context and @type annotations.\n\nSet bap_adapter_url = http://localhost:8081/bap/caller before running.", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "8677993" - }, - "item": [ - { - "name": "1 — Discovery", - "item": [ - { - "name": "discover", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"discover\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\",\n \"schemaContext\":[]\n },\n \"message\": {\n \"intent\": {\n \"filters\": {\n \"type\": \"jsonpath\",\n \"expression\":\"$.catalogs[*].offers[*] ? (@.offerAttributes.tariffModel == 'FLAT_DISCOUNT' && @.offerAttributes.priceSpecification.price < 100)\" \n }\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bap_adapter_url}}/discover", - "host": [ - "{{bap_adapter_url}}" - ], - "path": [ - "discover" - ] - }, - "description": "BAP selects resources from catalog.\n\nSpec rules (beckn.yaml):\n• Commitment.required = [status, resources, offer]\n• status.descriptor.code ∈ {DRAFT, ACTIVE, CLOSED}\n• resources = array of Resource (each needs @type, id, descriptor, quantity)\n• offer.required = [@type, id, resourceIds]\n• @context omitted on all core beckn.yaml objects" - }, - "response": [] - } - ] - }, - { - "name": "2 — Transaction", - "item": [ - { - "name": "select", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"select\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\",\n \"schemaContext\":[]\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"consideration\": [\n {\n \"id\": \"consideration-001\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"2000.00\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"breakup\": [\n {\n \"title\": \"Base Price\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"1800.00\"\n }\n },\n {\n \"title\": \"Taxes\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"200.00\"\n }\n }\n ]\n }\n ]\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bap_adapter_url}}/select", - "host": [ - "{{bap_adapter_url}}" - ], - "path": [ - "select" - ] - }, - "description": "BAP selects resources from catalog.\n\nSpec rules (beckn.yaml):\n• Commitment.required = [status, resources, offer]\n• status.descriptor.code ∈ {DRAFT, ACTIVE, CLOSED}\n• resources = array of Resource (each needs @type, id, descriptor, quantity)\n• offer.required = [@type, id, resourceIds]\n• @context omitted on all core beckn.yaml objects" - }, - "response": [] - }, - { - "name": "init", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"init\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"commitments\": [\n {\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"DRAFT\"\n }\n ]\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bap_adapter_url}}/init", - "host": [ - "{{bap_adapter_url}}" - ], - "path": [ - "init" - ] - }, - "description": "BAP provides buyer details, performance preferences, and payment intent.\n\nSpec rules (beckn.yaml):\n• participants carry buyer contact via participantAttributes (Attributes bag)\n• performance.required = [@context, @type] per spec — @context kept as spec const\n• settlements carry payment method via settlementAttributes (Attributes bag)\n• @context omitted on all core beckn.yaml objects except Performance (spec-required)" - }, - "response": [] - }, - { - "name": "confirm", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"confirm\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"COMPLETE\"\n }\n ]\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bap_adapter_url}}/confirm", - "host": [ - "{{bap_adapter_url}}" - ], - "path": [ - "confirm" - ] - }, - "description": "BAP confirms the contract after payment or binding commitment." - }, - "response": [] - } - ] - }, - { - "name": "3 — Catalog", - "item": [ - { - "name": "catalog pull", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"receiverId\": \"{{bpp_uri}}\",\n \"networkId\": \"{{networkId}}\",\n \"mode\": \"full\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bap_base_url}}/catalog/pull", - "host": [ - "{{bap_base_url}}" - ], - "path": [ - "catalog", - "pull" - ] - }, - "description": "DS-internal trigger for the catalog-crawler plugin: fetches and verifies the given provider node's manifest -> index -> catalog chain and returns them. Unsigned -- same-operator call, not a signed network action. Response matches beckn.yaml's CatalogPullCallbackAction shape: {status: COMPLETED, catalogs: [...]} or {status: FAILED, error: {...}}." - }, - "response": [] - } - ] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "// Beckn Protocol v2.0.0 — BAP Generic Collection", - "// Pre-request: transaction_id can be overridden here for end-to-end flows", - "if (!pm.collectionVariables.get('transaction_id')) {", - " pm.collectionVariables.set('transaction_id', pm.variables.replaceIn('{{$guid}}'));", - "}" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "pm.test('Status code is 200', function () {", - " pm.response.to.have.status(200);", - "});", - "pm.test('Response has ack', function () {", - " const json = pm.response.json();", - " pm.expect(json).to.have.property('message');", - " pm.expect(json.message).to.have.property('ack');", - " pm.expect(json.message.ack.status).to.eql('ACK');", - "});" - ] - } - } - ], - "variable": [ - { - "key": "bap_adapter_url", - "value": "http://localhost:8081/bap/caller", - "description": "BAP adapter caller endpoint (outbound)" - }, - { - "key": "bap_id", - "value": "bap.example.com", - "description": "BAP subscriber ID (FQDN)" - }, - { - "key": "public_url", - "value": "http://beckn-router:9000", - "description": "Base URL for bapUri / bppUri callbacks. Default routes within Docker via beckn-router. For external services (discover/publish): change to https://your-domain.ngrok-free.app" - }, - { - "key": "bap_uri", - "value": "{{public_url}}/bap/receiver", - "description": "BAP receiver callback URI — derived from public_url via beckn-router" - }, - { - "key": "bpp_id", - "value": "bpp.example.com", - "description": "BPP subscriber ID (FQDN)" - }, - { - "key": "bpp_uri", - "value": "{{public_url}}/bpp/receiver", - "description": "BPP receiver callback URI — derived from public_url via beckn-router" - }, - { - "key": "networkId", - "value": "beckn.one/testnet", - "description": "Network identifier" - }, - { - "key": "transaction_id", - "value": "", - "description": "Transaction ID — set once in pre-request script, shared across all calls in a flow" - }, - { - "key": "bap_base_url", - "value": "http://localhost:8081", - "description": "BAP adapter host:port, no path suffix -- used by root-level paths like /catalog/pull." - } - ] + "info": { + "_postman_id": "9094f707-1fb4-4bf0-8c92-6f53041a1181", + "name": "BAP Beckn Starter Kit", + "description": "Usecase-agnostic BAP requests for all Beckn Protocol v2.0.0 actions. All schema objects carry JSON-LD @context and @type annotations.\n\nSet bap_adapter_url = http://localhost:8081/bap/caller before running.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "8677993" + }, + "item": [ + { + "name": "1 \u2014 Discovery", + "item": [ + { + "name": "discover", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"discover\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\",\n \"schemaContext\":[]\n },\n \"message\": {\n \"intent\": {\n \"filters\": {\n \"type\": \"jsonpath\",\n \"expression\":\"$.catalogs[*].offers[*] ? (@.offerAttributes.tariffModel == 'FLAT_DISCOUNT' && @.offerAttributes.priceSpecification.price < 100)\" \n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bap_adapter_url}}/discover", + "host": [ + "{{bap_adapter_url}}" + ], + "path": [ + "discover" + ] + }, + "description": "BAP selects resources from catalog.\n\nSpec rules (beckn.yaml):\n\u2022 Commitment.required = [status, resources, offer]\n\u2022 status.descriptor.code \u2208 {DRAFT, ACTIVE, CLOSED}\n\u2022 resources = array of Resource (each needs @type, id, descriptor, quantity)\n\u2022 offer.required = [@type, id, resourceIds]\n\u2022 @context omitted on all core beckn.yaml objects" + }, + "response": [] + } + ] + }, + { + "name": "2 \u2014 Transaction", + "item": [ + { + "name": "select", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"select\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\",\n \"schemaContext\":[]\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"consideration\": [\n {\n \"id\": \"consideration-001\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"2000.00\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"breakup\": [\n {\n \"title\": \"Base Price\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"1800.00\"\n }\n },\n {\n \"title\": \"Taxes\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"200.00\"\n }\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bap_adapter_url}}/select", + "host": [ + "{{bap_adapter_url}}" + ], + "path": [ + "select" + ] + }, + "description": "BAP selects resources from catalog.\n\nSpec rules (beckn.yaml):\n\u2022 Commitment.required = [status, resources, offer]\n\u2022 status.descriptor.code \u2208 {DRAFT, ACTIVE, CLOSED}\n\u2022 resources = array of Resource (each needs @type, id, descriptor, quantity)\n\u2022 offer.required = [@type, id, resourceIds]\n\u2022 @context omitted on all core beckn.yaml objects" + }, + "response": [] + }, + { + "name": "init", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"init\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"commitments\": [\n {\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"DRAFT\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bap_adapter_url}}/init", + "host": [ + "{{bap_adapter_url}}" + ], + "path": [ + "init" + ] + }, + "description": "BAP provides buyer details, performance preferences, and payment intent.\n\nSpec rules (beckn.yaml):\n\u2022 participants carry buyer contact via participantAttributes (Attributes bag)\n\u2022 performance.required = [@context, @type] per spec \u2014 @context kept as spec const\n\u2022 settlements carry payment method via settlementAttributes (Attributes bag)\n\u2022 @context omitted on all core beckn.yaml objects except Performance (spec-required)" + }, + "response": [] + }, + { + "name": "confirm", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"networkId\": \"{{networkId}}\",\n \"action\": \"confirm\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"COMPLETE\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bap_adapter_url}}/confirm", + "host": [ + "{{bap_adapter_url}}" + ], + "path": [ + "confirm" + ] + }, + "description": "BAP confirms the contract after payment or binding commitment." + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// Beckn Protocol v2.0.0 \u2014 BAP Generic Collection", + "// Pre-request: transaction_id can be overridden here for end-to-end flows", + "if (!pm.collectionVariables.get('transaction_id')) {", + " pm.collectionVariables.set('transaction_id', pm.variables.replaceIn('{{$guid}}'));", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response has ack', function () {", + " const json = pm.response.json();", + " pm.expect(json).to.have.property('message');", + " pm.expect(json.message).to.have.property('ack');", + " pm.expect(json.message.ack.status).to.eql('ACK');", + "});" + ] + } + } + ], + "variable": [ + { + "key": "bap_adapter_url", + "value": "http://localhost:8081/bap/caller", + "description": "BAP adapter caller endpoint (outbound)" + }, + { + "key": "bap_id", + "value": "bap.example.com", + "description": "BAP subscriber ID (FQDN)" + }, + { + "key": "public_url", + "value": "http://beckn-router:9000", + "description": "Base URL for bapUri / bppUri callbacks. Default routes within Docker via beckn-router. For external services (discover/publish): change to https://your-domain.ngrok-free.app" + }, + { + "key": "bap_uri", + "value": "{{public_url}}/bap/receiver", + "description": "BAP receiver callback URI \u2014 derived from public_url via beckn-router" + }, + { + "key": "bpp_id", + "value": "bpp.example.com", + "description": "BPP subscriber ID (FQDN)" + }, + { + "key": "bpp_uri", + "value": "{{public_url}}/bpp/receiver", + "description": "BPP receiver callback URI \u2014 derived from public_url via beckn-router" + }, + { + "key": "networkId", + "value": "beckn.one/testnet", + "description": "Network identifier" + }, + { + "key": "transaction_id", + "value": "", + "description": "Transaction ID \u2014 set once in pre-request script, shared across all calls in a flow" + }, + { + "key": "bap_base_url", + "value": "http://localhost:8081", + "description": "BAP adapter host:port, no path suffix." + } + ] } diff --git a/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json b/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json index 511503f..bbe31e2 100644 --- a/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json +++ b/generic-devkit/postman/BPPBecknStarterKit.postman_collection.json @@ -1,231 +1,231 @@ { - "info": { - "_postman_id": "71877d77-839a-44ff-942c-d36937d643a1", - "name": "BPP Beckn Starter Kit", - "description": "Usecase-agnostic BPP on_* callbacks for all Beckn Protocol v2.0.0 responses. All schema objects carry JSON-LD @context and @type annotations.\n\nSet bpp_adapter_url = http://localhost:8082/bpp/caller before running.", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "8677993" - }, - "item": [ - { - "name": "1 \u2014 Transaction", - "item": [ - { - "name": "on_select", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"beckn.one/testnet\",\n \"action\": \"on_select\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"consideration\": [\n {\n \"id\": \"consideration-001\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"2000.00\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"breakup\": [\n {\n \"title\": \"Base Price\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"1800.00\"\n }\n },\n {\n \"title\": \"Taxes\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"200.00\"\n }\n }\n ]\n }\n ]\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bpp_adapter_url}}/on_select", - "host": [ - "{{bpp_adapter_url}}" - ], - "path": [ - "on_select" - ] - }, - "description": "BPP returns updated contract draft with pricing after selection." - }, - "response": [] - }, - { - "name": "on_init", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"beckn.one/testnet\",\n \"action\": \"on_init\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"commitments\": [\n {\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"DRAFT\"\n }\n ]\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bpp_adapter_url}}/on_init", - "host": [ - "{{bpp_adapter_url}}" - ], - "path": [ - "on_init" - ] - }, - "description": "BPP confirms initialised contract with payment details." - }, - "response": [] - }, - { - "name": "on_confirm", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"context\": {\n \"networkId\": \"beckn.one/testnet\",\n \"action\": \"on_confirm\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"COMPLETE\"\n }\n ]\n }\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bpp_adapter_url}}/on_confirm", - "host": [ - "{{bpp_adapter_url}}" - ], - "path": [ - "on_confirm" - ] - }, - "description": "BPP confirms contract as binding and provides performance details." - }, - "response": [] - } - ] - }, - { - "name": "2 \u2014 Catalog Publishing", - "item": [ - { - "name": "publish", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"catalogs\": [\n {\n \"id\": \"bpp.example.com/CAT-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Generic Catalog\",\n \"shortDesc\": \"Daily essentials generic items\"\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n \"resources\": [\n {\n \"id\": \"ITEM-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Bru Gold Instant Coffee Powder\",\n \"shortDesc\": \"Rich and aromatic instant coffee powder, 200g\",\n \"longDesc\": \"Bru Gold is a premium blend of coffee and chicory, crafted for those who love a rich, flavourful cup. Made from handpicked coffee beans roasted to perfection, this instant coffee powder dissolves quickly to give you a smooth, aromatic brew every time. Whether you prefer your coffee black or with milk, Bru Gold delivers a satisfying coffee experience. Perfect for filter coffee, cappuccino, or a classic South Indian style coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://www.jiomart.com/images/product/original/599173064/599173064.jpg\",\n \"mimeType\": \"image/jpeg\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 18200,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n {\n \"id\": \"ITEM-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Classic Item\",\n \"shortDesc\": \"Pure soluble coffee, bold and rich, 100g\",\n \"longDesc\": \"Nescafe Classic is made from 100% pure coffee beans, carefully selected and roasted to deliver a bold, rich flavour in every cup. This instant coffee dissolves instantly in hot or cold water, making it ideal for a quick coffee fix anytime. The deep roast brings out natural coffee aromas that energise your mornings. Loved by coffee enthusiasts across India, Nescafe Classic is the trusted choice for a consistently great cup of coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://m.media-amazon.com/images/S/aplus-media/vc/bb6a0196-cad0-4395-b85e-134ef725c0f7._CR0,0,1251,1251_PT0_SX300__.png\",\n \"mimeType\": \"image/png\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-02\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 32500,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 76.6394,\n 12.2958\n ]\n },\n \"address\": {\n \"streetAddress\": \"12 Sayyaji Rao Road, Devaraja Market\",\n \"addressLocality\": \"Mysore\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"570001\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n }\n ],\n \"offers\": [\n {\n \"id\": \"OFFER-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Bundle\",\n \"shortDesc\": \"Bru Gold + Nescafe Classic at 15% off - your morning coffee essentials\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-001\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n },\n {\n \"id\": \"OFFER-GROCERY-STAPLES-SAVER-002\",\n \"descriptor\": {\n \"name\": \"Hot Beverages Pack\",\n \"shortDesc\": \"Nescafe Classic + Tata Tea Premium combo - 10% off on your daily beverages\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-002\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n }\n ]\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{bpp_base_url}}/catalog/publish", - "host": [ - "{{bpp_base_url}}" - ], - "path": [ - "catalog", - "publish" - ] - }, - "description": "DS-internal trigger for the catalog-publisher plugin: given one or more plain Beckn Catalog objects (no context/message envelope), diffs each against what was last published (baseline vs. incremental change file), signs the result, and writes it under the handler's outputRoot. Unsigned -- same-operator call, not a signed network action. Response: {status: COMPLETED, results: [{catalogId, status: ACCEPTED|REJECTED, version, reason}]} or {status: FAILED, error: {...}} on a fatal failure. Public catalogs only in this phase (no networkIds/authMethods)." - }, - "response": [] - } - ] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "requests": {}, - "exec": [ - "// Beckn Protocol v2.0.0 \u2014 BPP Generic Collection", - "// Pre-request: set transaction_id to match the BAP collection for end-to-end testing", - "if (!pm.collectionVariables.get('transaction_id')) {", - " pm.collectionVariables.set('transaction_id', pm.variables.replaceIn('{{$guid}}'));", - "}" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "requests": {}, - "exec": [ - "pm.test('Status code is 200', function () {", - " pm.response.to.have.status(200);", - "});", - "pm.test('Response has ack', function () {", - " const json = pm.response.json();", - " pm.expect(json).to.have.property('message');", - " pm.expect(json.message).to.have.property('ack');", - " pm.expect(json.message.ack.status).to.eql('ACK');", - "});" - ] - } - } - ], - "variable": [ - { - "key": "bpp_adapter_url", - "value": "http://localhost:8082/bpp/caller", - "description": "BAP adapter caller endpoint (outbound)" - }, - { - "key": "bap_id", - "value": "bap.example.com", - "description": "BAP subscriber ID (FQDN)" - }, - { - "key": "public_url", - "value": "http://beckn-router:9000", - "description": "Base URL for bapUri / bppUri callbacks. Default routes within Docker via beckn-router. For external services (discover/publish): change to https://your-domain.ngrok-free.app" - }, - { - "key": "bap_uri", - "value": "{{public_url}}/bap/receiver", - "description": "BAP receiver callback URI \u2014 derived from public_url via beckn-router" - }, - { - "key": "bpp_id", - "value": "bpp.example.com", - "description": "BPP subscriber ID (FQDN)" - }, - { - "key": "bpp_uri", - "value": "{{public_url}}/bpp/receiver", - "description": "BPP receiver callback URI \u2014 derived from public_url via beckn-router" - }, - { - "key": "networkId", - "value": "beckn.one/testnet", - "description": "Network identifier" - }, - { - "key": "transaction_id", - "value": "", - "description": "Transaction ID \u2014 set once in pre-request script, shared across all calls in a flow" - }, - { - "key": "bpp_base_url", - "value": "http://localhost:8082", - "description": "BPP adapter host:port, no path suffix -- used by root-level paths like /catalog/publish." - } - ] + "info": { + "_postman_id": "71877d77-839a-44ff-942c-d36937d643a1", + "name": "BPP Beckn Starter Kit", + "description": "Usecase-agnostic BPP on_* callbacks for all Beckn Protocol v2.0.0 responses. All schema objects carry JSON-LD @context and @type annotations.\n\nSet bpp_adapter_url = http://localhost:8082/bpp/caller before running.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "8677993" + }, + "item": [ + { + "name": "1 \u2014 Transaction", + "item": [ + { + "name": "on_select", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"networkId\": \"beckn.one/testnet\",\n \"action\": \"on_select\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"consideration\": [\n {\n \"id\": \"consideration-001\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"2000.00\"\n },\n \"status\": {\n \"code\": \"DRAFT\"\n },\n \"breakup\": [\n {\n \"title\": \"Base Price\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"1800.00\"\n }\n },\n {\n \"title\": \"Taxes\",\n \"price\": {\n \"currency\": \"INR\",\n \"value\": \"200.00\"\n }\n }\n ]\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bpp_adapter_url}}/on_select", + "host": [ + "{{bpp_adapter_url}}" + ], + "path": [ + "on_select" + ] + }, + "description": "BPP returns updated contract draft with pricing after selection." + }, + "response": [] + }, + { + "name": "on_init", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"networkId\": \"beckn.one/testnet\",\n \"action\": \"on_init\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"commitments\": [\n {\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"DRAFT\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bpp_adapter_url}}/on_init", + "host": [ + "{{bpp_adapter_url}}" + ], + "path": [ + "on_init" + ] + }, + "description": "BPP confirms initialised contract with payment details." + }, + "response": [] + }, + { + "name": "on_confirm", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"networkId\": \"beckn.one/testnet\",\n \"action\": \"on_confirm\",\n \"version\": \"2.0.0\",\n \"bapId\": \"{{bap_id}}\",\n \"bapUri\": \"{{bap_uri}}\",\n \"bppId\": \"{{bpp_id}}\",\n \"bppUri\": \"{{bpp_uri}}\",\n \"transactionId\": \"{{transaction_id}}\",\n \"messageId\": \"{{$guid}}\",\n \"timestamp\": \"{{$isoTimestamp}}\",\n \"ttl\": \"PT30S\"\n },\n \"message\": {\n \"contract\": {\n \"id\": \"draft-contract-0001\",\n \"commitments\": [\n {\n \"id\": \"commitment-001\",\n \"status\": {\n \"descriptor\": {\n \"code\": \"DRAFT\"\n }\n },\n \"resources\": [\n {\n \"id\": \"resource-001\",\n \"descriptor\": {\n \"name\": \"Basic Service Package\",\n \"code\": \"BSP-001\"\n },\n \"quantity\": {\n \"unitQuantity\": 2,\n \"unitCode\": \"UNIT\"\n }\n }\n ],\n \"offer\": {\n \"id\": \"offer-001\",\n \"resourceIds\": [\n \"resource-001\"\n ]\n }\n }\n ],\n \"participants\": [\n {\n \"id\": \"participant-buyer-001\",\n \"descriptor\": {\n \"name\": \"John Doe\",\n \"code\": \"buyer\"\n }\n }\n ],\n \"performance\": [\n {\n \"id\": \"perf-001\"\n }\n ],\n \"settlements\": [\n {\n \"id\": \"settlement-001\",\n \"status\": \"COMPLETE\"\n }\n ]\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bpp_adapter_url}}/on_confirm", + "host": [ + "{{bpp_adapter_url}}" + ], + "path": [ + "on_confirm" + ] + }, + "description": "BPP confirms contract as binding and provides performance details." + }, + "response": [] + } + ] + }, + { + "name": "2 \u2014 Catalog Publishing", + "item": [ + { + "name": "publish", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"context\": {\n \"action\": \"catalog/publish\"\n },\n \"message\": {\n \"catalogs\": [\n {\n \"id\": \"staging.p-node.fabric.nfh.global/CAT-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Generic Catalog\",\n \"shortDesc\": \"Daily essentials generic items\"\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n \"resources\": [\n {\n \"id\": \"ITEM-GENERIC-001\",\n \"descriptor\": {\n \"name\": \"Bru Gold Instant Coffee Powder\",\n \"shortDesc\": \"Rich and aromatic instant coffee powder, 200g\",\n \"longDesc\": \"Bru Gold is a premium blend of coffee and chicory, crafted for those who love a rich, flavourful cup. Made from handpicked coffee beans roasted to perfection, this instant coffee powder dissolves quickly to give you a smooth, aromatic brew every time. Whether you prefer your coffee black or with milk, Bru Gold delivers a satisfying coffee experience. Perfect for filter coffee, cappuccino, or a classic South Indian style coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://www.jiomart.com/images/product/original/599173064/599173064.jpg\",\n \"mimeType\": \"image/jpeg\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"BP Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 18200,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 77.6401,\n 12.9116\n ]\n },\n \"address\": {\n \"streetAddress\": \"27th Main Rd, Sector 2, HSR Layout\",\n \"addressLocality\": \"Bengaluru\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"560102\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n },\n {\n \"id\": \"ITEM-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Classic Item\",\n \"shortDesc\": \"Pure soluble coffee, bold and rich, 100g\",\n \"longDesc\": \"Nescafe Classic is made from 100% pure coffee beans, carefully selected and roasted to deliver a bold, rich flavour in every cup. This instant coffee dissolves instantly in hot or cold water, making it ideal for a quick coffee fix anytime. The deep roast brings out natural coffee aromas that energise your mornings. Loved by coffee enthusiasts across India, Nescafe Classic is the trusted choice for a consistently great cup of coffee.\",\n \"mediaFile\": [\n {\n \"uri\": \"https://m.media-amazon.com/images/S/aplus-media/vc/bb6a0196-cad0-4395-b85e-134ef725c0f7._CR0,0,1251,1251_PT0_SX300__.png\",\n \"mimeType\": \"image/png\",\n \"label\": \"Product image\"\n }\n ]\n },\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-02\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"rating\": {\n \"ratingValue\": 4.1,\n \"ratingCount\": 32500,\n \"bestRating\": 5,\n \"worstRating\": 1\n },\n \"availableAt\": [\n {\n \"geo\": {\n \"type\": \"Point\",\n \"coordinates\": [\n 76.6394,\n 12.2958\n ]\n },\n \"address\": {\n \"streetAddress\": \"12 Sayyaji Rao Road, Devaraja Market\",\n \"addressLocality\": \"Mysore\",\n \"addressRegion\": \"Karnataka\",\n \"postalCode\": \"570001\",\n \"addressCountry\": \"IN\"\n }\n }\n ]\n }\n ],\n \"offers\": [\n {\n \"id\": \"OFFER-GENERIC-002\",\n \"descriptor\": {\n \"name\": \"Generic Bundle\",\n \"shortDesc\": \"Bru Gold + Nescafe Classic at 15% off - your morning coffee essentials\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-001\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n },\n {\n \"id\": \"OFFER-GROCERY-STAPLES-SAVER-002\",\n \"descriptor\": {\n \"name\": \"Hot Beverages Pack\",\n \"shortDesc\": \"Nescafe Classic + Tata Tea Premium combo - 10% off on your daily beverages\"\n },\n \"resourceIds\": [\n \"ITEM-GENERIC-002\"\n ],\n \"provider\": {\n \"id\": \"PROV-EXAMPLE-01\",\n \"descriptor\": {\n \"name\": \"Mart Pvt Ltd\"\n }\n },\n \"validity\": {\n \"startDate\": \"2026-03-04T00:00:00Z\",\n \"endDate\": \"2026-03-31T23:59:59Z\"\n }\n }\n ]\n }\n ],\n \"publishDirectives\": [\n {\n \"catalogId\": \"staging.p-node.fabric.nfh.global/CAT-GENERIC-001\",\n \"visibleTo\": [\n \"beckn.one/testnet\",\n \"nfh.global/testnet\"\n ],\n \"catalogType\": \"REGULAR\"\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{bpp_base_url}}/catalog/publish", + "host": [ + "{{bpp_base_url}}" + ], + "path": [ + "catalog", + "publish" + ] + }, + "description": "DS-internal trigger for the catalog-publisher plugin: request body matches beckn.yaml's real CatalogPublishAction envelope shape (context/message.catalogs[]/message.publishDirectives[]) so schema validation runs against the real request, no synthesized wrapper. publishDirectives[].visibleTo maps onto each catalog's networkIds in the published index (empty/omitted = public). Diffs each catalog against what was last published (baseline vs. incremental change file), signs the result, and writes it under the handler's outputRoot. Unsigned -- same-operator call, not a signed network action. Response: {status: COMPLETED, results: [{catalogId, status: ACCEPTED|REJECTED, version, reason}]} or {status: FAILED, error: {...}} on a fatal failure." + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "requests": {}, + "exec": [ + "// Beckn Protocol v2.0.0 \u2014 BPP Generic Collection", + "// Pre-request: set transaction_id to match the BAP collection for end-to-end testing", + "if (!pm.collectionVariables.get('transaction_id')) {", + " pm.collectionVariables.set('transaction_id', pm.variables.replaceIn('{{$guid}}'));", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "requests": {}, + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "pm.test('Response has ack', function () {", + " const json = pm.response.json();", + " pm.expect(json).to.have.property('message');", + " pm.expect(json.message).to.have.property('ack');", + " pm.expect(json.message.ack.status).to.eql('ACK');", + "});" + ] + } + } + ], + "variable": [ + { + "key": "bpp_adapter_url", + "value": "http://localhost:8082/bpp/caller", + "description": "BAP adapter caller endpoint (outbound)" + }, + { + "key": "bap_id", + "value": "bap.example.com", + "description": "BAP subscriber ID (FQDN)" + }, + { + "key": "public_url", + "value": "http://beckn-router:9000", + "description": "Base URL for bapUri / bppUri callbacks. Default routes within Docker via beckn-router. For external services (discover/publish): change to https://your-domain.ngrok-free.app" + }, + { + "key": "bap_uri", + "value": "{{public_url}}/bap/receiver", + "description": "BAP receiver callback URI \u2014 derived from public_url via beckn-router" + }, + { + "key": "bpp_id", + "value": "bpp.example.com", + "description": "BPP subscriber ID (FQDN)" + }, + { + "key": "bpp_uri", + "value": "{{public_url}}/bpp/receiver", + "description": "BPP receiver callback URI \u2014 derived from public_url via beckn-router" + }, + { + "key": "networkId", + "value": "beckn.one/testnet", + "description": "Network identifier" + }, + { + "key": "transaction_id", + "value": "", + "description": "Transaction ID \u2014 set once in pre-request script, shared across all calls in a flow" + }, + { + "key": "bpp_base_url", + "value": "http://localhost:8082", + "description": "BPP adapter host:port, no path suffix -- used by root-level paths like /catalog/publish." + } + ] }