Orishare
Integration prompts

Prompt: Supabase web app

A prompt to paste into a coding agent that integrates a Supabase-authenticated web app with Orishare customer sessions.

Paste the prompt below into your coding agent from the root of your web app, after replacing the values in angle brackets. The agent needs no Orishare secret.

Before you run it, as the operator: create a test publishable key, create an identity connection with profile supabase_access for your Supabase project (asymmetric RS256 or ES256 signing keys; HS256 projects are not supported), mark the browser events as client_allowed, allow your site origin, and enable anonymous exchange if visitors should earn before signing in. See the agent guide. Give the agent the publishable key and connection id.

For the hosted widget (step 9), also: add your page's exact origin to the portal's allowed_parent_origins and publish the portal (dashboard Widget area, or a proposal you confirm), and add the same origin to the client configuration's allowed_origins. Give the agent the portal id (ptl_...) and the widget origin from the dashboard snippet.

Integrate this web app with Orishare (loyalty and engagement API) using Supabase Auth.

Read first, and use only what they define:
- https://docs.orishare.com/docs/agent-guide and its credential matrix at
  https://docs.orishare.com/docs/agent-guide#credential-matrix
- https://docs.orishare.com/capabilities.json (supported operations and credential kinds)
- https://docs.orishare.com/openapi.json (the only source of routes and schemas)
Do not invent routes, fields or rule actions.

Credentials and where they live:
- Publishable key <opk_...> and identity connection id <ic_...>: public config
  (for example NEXT_PUBLIC_ORISHARE_PUBLISHABLE_KEY). Not secrets.
- Customer session token: in memory only; never in storage, cookies, URLs or logs.
- Never put a secret key (osk_...), a personal access token (opat_...) or the Supabase
  service-role key in frontend code. Stop and ask if a step seems to need one.

Build:
1. orishare.ts with API = 'https://api.orishare.com' (the origin, without /v1; every path below
   already starts with /v1, and receipt_url values are origin-relative).
2. On SIGNED_IN and TOKEN_REFRESHED, take session.access_token from supabase.auth and exchange
   it with POST /v1/customer_sessions: Authorization: Bearer <publishable key>,
   one Idempotency-Key per exchange intent reused on every retry of that exchange, body
   { grant_type: "identity_token", connection_id, provider_token }, credentials: 'omit'. If a
   retried exchange is answered by replay (still 201, headers Idempotent-Replayed: true and
   Idempotent-Replay-Redacted: customer_token, body customer_token: null), start a deliberate
   new exchange: a new intent, a new key and a current
   access token. Each exchange starts a new session family.
   Anonymous visitors (if enabled): before sign-in, exchange { grant_type: "anonymous" } and keep
   that token in memory. On SIGNED_IN, right after the exchange and while the anonymous token is
   still live, merge with POST /v1/customer/merges: headers carry the publishable key and the
   SIGNED-IN token, body { source_customer_token: <anonymous token> }, one Idempotency-Key.
   Exchange alone never merges; without this call the anonymous history is lost. Then drop the
   anonymous token.
3. Every customer call sends Authorization: Bearer <publishable key> and
   X-Orishare-Customer-Token.
4. Renew with POST /v1/customer/session_renewals before expires_at (one Idempotency-Key per
   renewal, reused on its retries); on SIGNED_OUT call
   POST /v1/customer/session_family_revocations (it revokes only the current family) and drop
   the token.
5. Client events go to POST /v1/customer/events with a stable event id and one Idempotency-Key
   per intent reused on retries; poll GET /v1/customer/event_receipts/{id} until evaluated and
   read all of effects (currency_deltas, quest_progress, and when present graph_quest_progress,
   mechanic_changes, coupons, discounts). A resend of the same id with a new key shows the
   original event's effects again: never render them twice. Client-allowed
   events: <workout.logged>. Payments are sent by our backend only.
6. Read GET /v1/customer/state, GET /v1/customer/balances and the mechanic collections the state
   marks available (GET /v1/customer/streaks, GET /v1/customer/levels,
   GET /v1/customer/leaderboard_positions). A streak appears only after the customer's first
   contribution; an empty balances list means zero; match programs by program_key. Use ETag and
   If-None-Match; amounts are strings.
7. Repeat a request only as the guide's "When a request may be repeated" section says
   (https://docs.orishare.com/docs/agent-guide#when-a-request-may-be-repeated): after 429, or 409
   with Retry-After, wait and resend unchanged; after a lost answer, resend only operations whose
   retry class in capabilities.json is atomic_replay or keyed_resume (all customer calls used
   here), with the same Idempotency-Key and body. Never loop on other 4xx; surface error.code.
8. A Supabase anonymous user (is_anonymous) stays anonymous assurance.
9. If I ask for the hosted widget instead of (or beside) a custom rewards UI, add it as
   https://docs.orishare.com/docs/hosted-widget shows, identity path: the script tag for
   <widget origin>/v1/embed.js and orishare.push(['mount', { portal: <ptl_...>,
   publishableKey, connectionId, getIdentityToken: async () =>
   (await supabase.auth.getSession()).data.session?.access_token ?? null }]), plus
   target: '#element' only when the portal's layout is inline; omit target for a launcher
   (GET /v1/public/portals/{portal_id}, no credential, shows layout.mode).
   The widget origin is in the dashboard snippet; ask me for it. getIdentityToken returns null
   when nobody is signed in. The loader calls it as soon as the widget's frame has loaded and
   connected after mount (so the exchange happens on page load, even while a launcher is
   closed) and again whenever the widget needs a session (after signIn and after
   session_needed), and exchanges the token itself, so do not refresh or exchange it for the
   widget. When the user signs out of Supabase push ['signOut'] (the loader then revokes the
   session it minted) and wait for the signed_out event before clearing our own state; push
   ['signIn'] after sign-in; on the sign_in_requested event open our sign-in. The page origin
   must be in the portal's allowed_parent_origins and in the client configuration's
   allowed_origins (operator settings; GET /v1/public/portals/{portal_id}, no credential, shows
   the portal's allowed_parent_origins and layout). The widget never sends events and never
   needs a secret key.

Verify in the test environment end to end and report changed files and anything unverified.