openvibers/openvibe.contracts Commits

v0.23.0: OpenVibe.Deals is alpha (deals.* capabilities)

OP OpenVibers committed 23 Sept 2026 · ae3fdecd5b · 14 files, +300 −9


Changes

ddiff --git a/manifests/capabilities/deals.flag.create.json b/manifests/capabilities/deals.flag.create.json
nnew file mode 100644
iindex 00000000..fcc58964
---- /dev/null
++++ b/manifests/capabilities/deals.flag.create.json
@@@ -0,0 +1,19 @@
++{
++ "id": "deals.flag.create",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Report a problem with an offer for the acting person (expired, price_wrong, duplicate, spam, other). One open report per person and offer; rate-limited. Flags never change an offer by themselves.",
++ "permissions": [
++ "deals:write"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "deals-write",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/offers/:id/flags"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.offer.expire.json b/manifests/capabilities/deals.offer.expire.json
nnew file mode 100644
iindex 00000000..176c6899
---- /dev/null
++++ b/manifests/capabilities/deals.offer.expire.json
@@@ -0,0 +1,22 @@
++{
++ "id": "deals.offer.expire",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Mark an active offer expired (the person who submitted it, or a moderator). Unknown expiry is never assumed; offers with a stated end expire on their own.",
++ "permissions": [
++ "deals:write"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "deals-write",
++ "events": [
++ "deals.offer.expired",
++ "deals.index_document.upserted"
++ ],
++ "implementedBy": [
++ "POST /api/v1/offers/:id/expire"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.offer.merge.json b/manifests/capabilities/deals.offer.merge.json
nnew file mode 100644
iindex 00000000..427c6ac0
---- /dev/null
++++ b/manifests/capabilities/deals.offer.merge.json
@@@ -0,0 +1,24 @@
++{
++ "id": "deals.offer.merge",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Duplicate resolution (moderation): merge an offer into another, or unmerge it. Nothing is copied or deleted: votes, observations, sources and comments stay where they were written and the canonical offer reads across the group (a voter counts once). Audited with the tallies before and after; reversible.",
++ "permissions": [
++ "deals:moderate"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "deals-moderation",
++ "events": [
++ "deals.offer.updated",
++ "deals.index_document.upserted",
++ "deals.index_document.deleted"
++ ],
++ "implementedBy": [
++ "POST /api/v1/offers/:id/merge",
++ "POST /api/v1/offers/:id/unmerge"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.offer.moderate.json b/manifests/capabilities/deals.offer.moderate.json
nnew file mode 100644
iindex 00000000..d7d27679
---- /dev/null
++++ b/manifests/capabilities/deals.offer.moderate.json
@@@ -0,0 +1,27 @@
++{
++ "id": "deals.offer.moderate",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Moderation: disable or enable an offer, record a person's review of imported or AI-assisted text (reviews are recorded for a usr_ subject only), read and resolve flags including vote-ring flags. Audited.",
++ "permissions": [
++ "deals:moderate"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "deals-moderation",
++ "events": [
++ "deals.offer.updated",
++ "deals.index_document.upserted",
++ "deals.index_document.deleted"
++ ],
++ "implementedBy": [
++ "POST /api/v1/offers/:id/disable",
++ "POST /api/v1/offers/:id/enable",
++ "POST /api/v1/offers/:id/review",
++ "GET /api/v1/flags",
++ "POST /api/v1/flags/:id/resolve"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.offer.submit.json b/manifests/capabilities/deals.offer.submit.json
nnew file mode 100644
iindex 00000000..fc3b3316
---- /dev/null
++++ b/manifests/capabilities/deals.offer.submit.json
@@@ -0,0 +1,23 @@
++{
++ "id": "deals.offer.submit",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Submit a deal for the acting person (X-OV-Subject): a link, title and what the person sees now (price with its currency, shipping, condition, availability — each optional, never defaulted). The first observation is recorded with its time. A link already posted answers 409 offer.duplicate with the existing offer. With X-OV-Origin: ai the text is AI-assisted: disclosed and noindex until a person reviews it. Rate-limited per person.",
++ "permissions": [
++ "deals:write"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "deals-write",
++ "events": [
++ "deals.offer.created",
++ "deals.watch.matched",
++ "deals.index_document.upserted"
++ ],
++ "implementedBy": [
++ "POST /api/v1/offers"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.offer.update.json b/manifests/capabilities/deals.offer.update.json
nnew file mode 100644
iindex 00000000..446103d3
---- /dev/null
++++ b/manifests/capabilities/deals.offer.update.json
@@@ -0,0 +1,24 @@
++{
++ "id": "deals.offer.update",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Edit an offer the acting person submitted (title, description, category, stated expiry, product link), or record a new observation of any active offer for the acting person (POST …/observations: price with currency, shipping, condition, availability, with the time it was observed). Prices are never edited in place; each observation is a new row. Services may state observed_at (never in the future).",
++ "permissions": [
++ "deals:write"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "deals-write",
++ "events": [
++ "deals.offer.updated",
++ "deals.watch.matched",
++ "deals.index_document.upserted"
++ ],
++ "implementedBy": [
++ "PATCH /api/v1/offers/:id",
++ "POST /api/v1/offers/:id/observations"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.product.resolve.json b/manifests/capabilities/deals.product.resolve.json
nnew file mode 100644
iindex 00000000..7f4599d3
---- /dev/null
++++ b/manifests/capabilities/deals.product.resolve.json
@@@ -0,0 +1,21 @@
++{
++ "id": "deals.product.resolve",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Resolve a product by its aliases (gtin, mpn, sku, exact name, url), creating it when none matches (create: false to only look up). Aliases that already name another product are reported as conflicts, never re-pointed.",
++ "permissions": [
++ "deals:write"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "deals-write",
++ "events": [
++ "deals.index_document.upserted"
++ ],
++ "implementedBy": [
++ "POST /api/v1/products/resolve"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.vote.remove.json b/manifests/capabilities/deals.vote.remove.json
nnew file mode 100644
iindex 00000000..f7964120
---- /dev/null
++++ b/manifests/capabilities/deals.vote.remove.json
@@@ -0,0 +1,21 @@
++{
++ "id": "deals.vote.remove",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Remove the acting person's vote on an offer (kept as a removed row for history). Same abuse controls as deals.vote.set.",
++ "permissions": [
++ "deals:vote"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "deals-vote",
++ "events": [
++ "deals.vote.changed"
++ ],
++ "implementedBy": [
++ "DELETE /api/v1/offers/:id/vote"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.vote.set.json b/manifests/capabilities/deals.vote.set.json
nnew file mode 100644
iindex 00000000..bed4247d
---- /dev/null
++++ b/manifests/capabilities/deals.vote.set.json
@@@ -0,0 +1,21 @@
++{
++ "id": "deals.vote.set",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Set the acting person's vote (+1 / -1) on an offer. Server-authoritative: weight and hotness are computed by Deals (hot@1), never taken from the request. Rate-limited per person, per IP hash and per offer; no votes on one's own offer; vote-ring detection flags moderators.",
++ "permissions": [
++ "deals:vote"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "deals-vote",
++ "events": [
++ "deals.vote.changed"
++ ],
++ "implementedBy": [
++ "PUT /api/v1/offers/:id/vote"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.watch.create.json b/manifests/capabilities/deals.watch.create.json
nnew file mode 100644
iindex 00000000..02f995bb
---- /dev/null
++++ b/manifests/capabilities/deals.watch.create.json
@@@ -0,0 +1,19 @@
++{
++ "id": "deals.watch.create",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Create a keyword, product or price-below watch, or a saved search (never notifies), for the acting person. A watch notifies at most once per observation (deals.watch.matched, internal; Deals never emails).",
++ "permissions": [
++ "deals:watch"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "deals-write",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/watches"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.watch.delete.json b/manifests/capabilities/deals.watch.delete.json
nnew file mode 100644
iindex 00000000..fa188d83
---- /dev/null
++++ b/manifests/capabilities/deals.watch.delete.json
@@@ -0,0 +1,19 @@
++{
++ "id": "deals.watch.delete",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Delete one of the acting person's watches or saved searches.",
++ "permissions": [
++ "deals:watch"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "deals-write",
++ "events": [],
++ "implementedBy": [
++ "DELETE /api/v1/watches/:id"
++ ]
++}
ddiff --git a/manifests/capabilities/deals.watch.read.json b/manifests/capabilities/deals.watch.read.json
nnew file mode 100644
iindex 00000000..cc52b0a0
---- /dev/null
++++ b/manifests/capabilities/deals.watch.read.json
@@@ -0,0 +1,19 @@
++{
++ "id": "deals.watch.read",
++ "version": "1.0",
++ "owner": "deals",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "List the acting person's watches and saved searches with their notification counts.",
++ "permissions": [
++ "deals:watch"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "deals-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/watches"
++ ]
++}
ddiff --git a/manifests/services/deals.json b/manifests/services/deals.json
iindex b9d33a54..222e7f1b 100644
---- a/manifests/services/deals.json
++++ b/manifests/services/deals.json
@@@ -1,16 +1,48 @@
{
"version": "0.1.0",
"contractRanges": {
-- "openvibe-contracts": "^0.1.0"
++ "openvibe-contracts": ">=0.19.0 <1.0.0"
},
"id": "deals",
"name": "OpenVibe.Deals",
-- "status": "placeholder",
++ "status": "alpha",
"repository": "OpenVibers/OpenVibe.Deals",
-- "domains": [],
-- "capabilities": [],
-- "eventsProduced": [],
-- "eventsConsumed": [],
-- "namespacesOwned": [],
-- "notes": "Deals submitted and voted on by the community, with source, price and freshness always shown. Charter only; no runtime."
++ "domains": [
++ "openvibe.deals"
++ ],
++ "publicOrigin": "https://openvibe.deals",
++ "health": "/api/health",
++ "ready": "/api/ready",
++ "capabilities": [
++ "deals.offer.submit",
++ "deals.offer.update",
++ "deals.offer.expire",
++ "deals.offer.merge",
++ "deals.offer.moderate",
++ "deals.vote.set",
++ "deals.vote.remove",
++ "deals.watch.create",
++ "deals.watch.read",
++ "deals.watch.delete",
++ "deals.product.resolve",
++ "deals.flag.create"
++ ],
++ "eventsProduced": [
++ "deals.offer.created",
++ "deals.offer.updated",
++ "deals.offer.expired",
++ "deals.vote.changed",
++ "deals.watch.matched",
++ "deals.index_document.upserted",
++ "deals.index_document.deleted"
++ ],
++ "eventsConsumed": [
++ "sources.item.created",
++ "sources.item.updated",
++ "sources.item.removed"
++ ],
++ "namespacesOwned": [
++ "deals.*"
++ ],
++ "notes": "Port 4840. Owns deal_products, deal_product_aliases, deal_offers, deal_offer_sources, deal_votes, deal_hotness_snapshots, deal_watches, deal_price_observations, deal_flags (+ deal_stores) in its own SQLite. Consumes OpenVibe.Network (SSO, identity.subject.resolve), OpenVibe.Sources (sources.item.read; sources.item.* events only wake the pull), OpenVibe.Community (community.comment.write, community.comment.moderate) and publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads deals.index_document.* from Events; deals.watch.matched is for Network notifications (consumer not built yet)."
}
ddiff --git a/package.json b/package.json
iindex ec2e3bf5..6158270b 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.22.0",
++ "version": "0.23.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",