Orishare
Integration prompts

Prompt: Node backend

A prompt to paste into a coding agent that sends trusted events and reads customer state from a Node.js backend with a secret key.

Paste the prompt below into your coding agent from the root of your Node.js service. Put the test secret key in the service's secret store first; the agent must read it from the environment, never write it into code. If the frontend will read customer state, ask the operator for a secret key that includes customer_sessions:write (not a default scope) and for the publishable key; neither can be created with a secret key.

Integrate this Node.js backend with Orishare (loyalty and engagement API).

Read first, and use only what they define:
- https://docs.orishare.com/docs/agent-guide, especially the credential matrix at
  https://docs.orishare.com/docs/agent-guide#credential-matrix and "Stable retry ids"
- 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:
- Secret key from process.env.ORISHARE_SECRET_KEY (osk_test_... in a test environment). Server only. Never
  send it to a browser, never log it, never commit it.
- Do not use a personal access token (opat_...) at runtime; it is for operators.
- If a browser needs Orishare, mint it a customer session (step 4); never give it this key.

Build:
1. Plain fetch with API = 'https://api.orishare.com' (the origin; paths start with /v1).
   @orishare/api-client is not on npm (it lives in the Orishare repository); do not add it.
2. After each business moment commits in our database, send it with POST /v1/events:
   { id, name, customer_id, occurred_at, properties }. customer_id is OUR user id (the external
   id), never an Orishare cust_ id. The id is derived from the business record (order_<id>,
   refund_<id>) so a retry is a duplicate, not a second event; the event id is the only dedupe
   (a secret-key POST /v1/events ignores Idempotency-Key). Event names are dotted snake_case
   and an unknown name is accepted as a new event, so keep them in one constant list. Events to
   send: <order.paid with total_cents and order_id; order.refunded with order_id>.
3. For bulk backfills use { events: [...] } (at most 100). The answer is 207 with one result per
   item, matched by index (a rejected item has error and no id); resend only rejected items after
   fixing them, with the same ids.
4. If the frontend needs customer reads, add an authenticated endpoint that resolves our signed-in
   user to their internal Orishare id (cust_..., from POST /v1/customers or
   GET /v1/customers?external_id=...) and calls POST /v1/customer_sessions with
   { grant_type: "secret_key", customer_id } and one Idempotency-Key per mint intent (reused on
   its retries), returning only { customer_token }. If a retry is answered by replay
   (customer_token: null), mint again with a new Idempotency-Key. This needs
   customer_sessions:write on the key; a 403 insufficient_scope means the operator must issue
   such a key.
5. Trusted reads: GET /v1/customers/{id}/state and GET /v1/customers/{id}/balances.
6. Async: a 202 is stored, not evaluated. When we need the outcome, poll GET /v1/events/{id} with
   the evt_ id from the admission result until status is evaluated (our business id is its
   client_event_id and is not accepted there), or use mode: "sync" only on low-volume paths (it
   may still return 202 with fallback: true).
7. Webhooks: expose a POST endpoint and verify Orishare-Signature (t=<unix>,v1=<hex>) as
   HMAC-SHA256 of `${t}.${rawBody}` with the endpoint secret, constant-time compare, reject t
   older than 300 seconds; make the handler idempotent by Orishare-Event-Id.
8. Repeating requests, exactly as 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 (timeout,
   connection error, 5xx) resend only when the operation's retry class in capabilities.json is
   atomic_replay or keyed_resume (rest_operations[].retry in capabilities.json), with the
   same Idempotency-Key and body. POST /v1/events is outcome_unknown but deduplicated by event
   id: resend the same event with the same id. Any other outcome_unknown write: read the
   resource first. No blanket retry loops for writes; never resend after 409 event_id_conflict
   or 422 idempotency_key_reused.
9. Hosted widget (if I ask for it): https://docs.orishare.com/docs/hosted-widget, backend path.
   Reuse the session endpoint of step 4 (it returns only { customer_token }, minted fresh on each
   call; 401 or null when nobody is signed in) and add to our page the script tag for
   <widget origin>/v1/embed.js (the widget origin is in the dashboard snippet; ask me for it) plus
   orishare.push(['mount', { portal: <ptl_...>, publishableKey: <opk_...>, getSession }]),
   plus target: '#element' only when the portal's layout is inline (omit it for a launcher),
   where getSession posts to that endpoint (same origin as the page) and returns its JSON. When
   our user signs out, push ['signOut'], wait for the signed_out event, then end our own session;
   push ['signIn'] after sign-in. The minted session is ours:
   the widget never revokes it; it expires on its own (15 minutes by default). The secret key
   never leaves this service; the publishable key in the snippet is fine.

Add tests with a stubbed fetch for the event sender (same id on retry, 207 handling).
Verify against the test environment and report changed files and anything unverified.