diff --git a/demo/self-report/.dockerignore b/demo/self-report/.dockerignore new file mode 100644 index 0000000..0d060bc --- /dev/null +++ b/demo/self-report/.dockerignore @@ -0,0 +1,2 @@ +vendor/ +README.md diff --git a/demo/self-report/.gitignore b/demo/self-report/.gitignore new file mode 100644 index 0000000..ed2a291 --- /dev/null +++ b/demo/self-report/.gitignore @@ -0,0 +1,2 @@ +vendor/ +fly.toml diff --git a/demo/self-report/DEPLOY.md b/demo/self-report/DEPLOY.md new file mode 100644 index 0000000..b3f42fe --- /dev/null +++ b/demo/self-report/DEPLOY.md @@ -0,0 +1,82 @@ +# Deploying the Self-Report Demo Site (Fly.io) + +The live instance is one always-warm 256 MB machine (~$2–3/mo), TLS +automatic, remote builds (no local arch concerns). Its hostname is +deliberately kept out of this public repo — find it in the sandbox +merchant-site registration (or ask the team). `fly.toml.example` is the +canonical config template; it pins `auto_stop_machines = 'off'` / +`min_machines_running = 1` because a probe target must never cold-start. + +> An AWS App Runner variant of this runbook existed previously; it was +> dropped after IAM friction (`iam:PassRole`) — see git history if ever +> needed. + +## One-time setup + +```bash +brew install flyctl +flyctl auth login # browser flow (signup included) + +cd demo/self-report +cp fly.toml.example fly.toml # fly.toml is gitignored — app name stays local +flyctl apps create # then set the same name in fly.toml +flyctl secrets set SUPERTAB_MERCHANT_API_KEY=placeholder SUPERTAB_ENFORCEMENT=observe --stage +flyctl deploy --ha=false # single machine; fly.toml does the rest +``` + +The API key starts as `placeholder` on purpose: sandbox registration needs +the site's domain, which only exists after the first deploy. Everything +probe-related works meanwhile — `/healthz` short-circuits before the config +check and challenge verification uses the public platform JWKS; only +analytics delivery would 401-and-drop. + +## Smoke test + +```bash +HOST=.fly.dev +curl -s https://$HOST/healthz # → ok +curl -si https://$HOST/.well-known/supertab/status | head -5 # → 404 {"supertab":true}, no-store +curl -s https://$HOST/ | grep "SDK version" # → the pinned SDK version +``` + +## Register the site in sandbox and set the real key (required) + +1. Register `https://.fly.dev` as a merchant + website in the **sandbox** environment — registration issues the + merchant API key. The backend only mints status challenges with `aud` = + a registered origin; unregistered probes silently get the decoy. +2. Swap in the real key (this alone triggers a redeploy, ~30 s): + + ```bash + flyctl secrets set SUPERTAB_MERCHANT_API_KEY= + ``` + +## The end-to-end probe + +Fire a backend live-health check (`self_report`) for the registered site. +Expected: `200` with `runtime: null`, `sdkVersion`, +`component: {kind: "php-sdk", version}`, `enforcement: "observe"`, +`eventReporting: true`. + +Note: the backend resolves only `ts-sdk` against a registry so far +(laterpay/supertab-connect#1094); `php-sdk` degrades to "show version, no +nudge" until its resolver lands. Expected, not a failure. + +## Updating (each new SDK release) + +```bash +cd demo/self-report +# bump the pin in composer.json, then: +composer update getsupertab/connect-sdk-php +flyctl deploy --ha=false +``` + +Commit the pin + lockfile change back to the repo. + +**Config experiments** (no rebuild): `flyctl secrets set +SUPERTAB_ENFORCEMENT=enforce` (or `SUPERTAB_ANALYTICS=0`, +`SUPERTAB_BASE_URL=…`) — each set redeploys, and the next probe reflects +the new values. + +**Ops one-liners**: `flyctl status` (machine state), `flyctl logs` +(live tail), `flyctl apps destroy ` (teardown). diff --git a/demo/self-report/Dockerfile b/demo/self-report/Dockerfile new file mode 100644 index 0000000..41deea9 --- /dev/null +++ b/demo/self-report/Dockerfile @@ -0,0 +1,20 @@ +# Dependencies — resolved from the committed lockfile for reproducible builds. +FROM composer:2 AS deps +WORKDIR /app +COPY composer.json composer.lock ./ +RUN composer install --no-dev --no-interaction --no-progress + +FROM php:8.3-apache + +# The app listens on 8080 (see fly.toml's internal_port; also the default +# for most container platforms); route every request that isn't an +# existing file to the front controller. +RUN sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf \ + && sed -i 's///' /etc/apache2/sites-available/000-default.conf \ + && printf 'FallbackResource /index.php\nSetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1\n' > /etc/apache2/conf-available/fallback.conf \ + && a2enconf fallback + +COPY --from=deps /app/vendor /var/www/html/vendor +COPY index.php /var/www/html/ + +EXPOSE 8080 diff --git a/demo/self-report/README.md b/demo/self-report/README.md new file mode 100644 index 0000000..56ef11a --- /dev/null +++ b/demo/self-report/README.md @@ -0,0 +1,61 @@ +# Self-Report Demo Site + +Vanilla-PHP publisher for testing the `/.well-known/supertab/status` +self-report endpoint end-to-end against the real (sandbox) Supertab Connect +API. Every request flows through `SupertabConnect::handleRequest()` — the +status endpoint is served by the SDK itself, with zero endpoint-specific +code in this app. It also serves as the canonical "plain PHP" integration +reference. + +Unlike the sibling `demo/` CLI demo (self-contained, mock API), this app +pins the **released Packagist SDK** and talks to the real API. Testing a new +SDK release = bump the pin in `composer.json`, rebuild, push. + +## Configuration + +| Env var | Default | Purpose | +|---------|---------|---------| +| `SUPERTAB_MERCHANT_API_KEY` | — (required) | Sandbox merchant API key | +| `SUPERTAB_BASE_URL` | `https://api-connect.sbx.supertab.co` | API base URL | +| `SUPERTAB_ENFORCEMENT` | `observe` | `disabled` \| `observe` \| `enforce` — reflected in the status payload | +| `SUPERTAB_ANALYTICS` | on (`0`/`false`/`off` to disable) | Toggles analytics → the payload's `eventReporting` | + +## Run locally + +```bash +composer install +SUPERTAB_MERCHANT_API_KEY= php -S localhost:8080 index.php +``` + +Smoke checks: + +```bash +curl -s localhost:8080/healthz # → ok +curl -si localhost:8080/.well-known/supertab/status | head -5 # → 404 {"supertab":true} +curl -s localhost:8080/ | head -3 # → demo HTML page +``` + +## Deploy + +See [DEPLOY.md](DEPLOY.md) — the site runs on Fly.io as one always-warm +machine (`fly.toml.example` committed here; the live hostname is kept out +of the repo — see the sandbox merchant-site registration). + +## Register the site (required for probes) + +The backend only mints status challenges (`aud` = origin) for origins it +knows. Register the service URL — `https://.fly.dev` — as a +merchant website in **sandbox**. If the URL changes (app recreated), +re-register. + +## Probe flow + +1. Unauthenticated: `curl -si https:///.well-known/supertab/status` + → `404` + `{"supertab":true}` + `Cache-Control: no-store` (decoy). +2. Garbage bearer: same decoy, never a 500 (challenge verification fails + closed). +3. Backend live-health probe for the registered site → `200` with + `{runtime, sdkVersion, component: {kind: "php-sdk", version}, + enforcement, eventReporting}`. +4. Flip `SUPERTAB_ENFORCEMENT` / `SUPERTAB_ANALYTICS` on the service → + next probe reflects the change. diff --git a/demo/self-report/composer.json b/demo/self-report/composer.json new file mode 100644 index 0000000..1d47968 --- /dev/null +++ b/demo/self-report/composer.json @@ -0,0 +1,11 @@ +{ + "name": "supertab/self-report-demo", + "description": "Vanilla-PHP demo site for testing the /.well-known/supertab/status self-report endpoint against the real API.", + "type": "project", + "license": "MIT", + "require": { + "php": ">=8.1", + "getsupertab/connect-sdk-php": "1.4.0-beta.9" + }, + "minimum-stability": "stable" +} diff --git a/demo/self-report/composer.lock b/demo/self-report/composer.lock new file mode 100644 index 0000000..563cf84 --- /dev/null +++ b/demo/self-report/composer.lock @@ -0,0 +1,146 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "66cd0c5a4fa4536131c20d1d85a6d524", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v7.1.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/php-jwt.git", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/googleapis/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/googleapis/php-jwt/issues", + "source": "https://github.com/googleapis/php-jwt/tree/v7.1.0" + }, + "time": "2026-06-11T17:54:14+00:00" + }, + { + "name": "getsupertab/connect-sdk-php", + "version": "v1.4.0-beta.9", + "source": { + "type": "git", + "url": "https://github.com/getsupertab/connect-sdk-php.git", + "reference": "ad5459a7f73d8a50f284b4dee6182b33b96f08eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsupertab/connect-sdk-php/zipball/ad5459a7f73d8a50f284b4dee6182b33b96f08eb", + "reference": "ad5459a7f73d8a50f284b4dee6182b33b96f08eb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "firebase/php-jwt": "^7.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5 || ^11.0", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Supertab\\Connect\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Supertab", + "homepage": "https://supertab.co" + } + ], + "description": "Supertab Connect PHP SDK", + "homepage": "https://github.com/getsupertab/connect-sdk-php", + "keywords": [ + "connect", + "jwt", + "license", + "rsl", + "supertab" + ], + "support": { + "issues": "https://github.com/getsupertab/connect-sdk-php/issues", + "source": "https://github.com/getsupertab/connect-sdk-php/tree/v1.4.0-beta.9" + }, + "time": "2026-07-14T08:41:59+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "getsupertab/connect-sdk-php": 10 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/demo/self-report/fly.toml.example b/demo/self-report/fly.toml.example new file mode 100644 index 0000000..60fde76 --- /dev/null +++ b/demo/self-report/fly.toml.example @@ -0,0 +1,29 @@ +# Fly.io config template for the self-report demo site (see DEPLOY.md). +# Copy to fly.toml and set your app name (the real deployment's name/URL +# is deliberately kept out of the repo): +# cp fly.toml.example fly.toml && fly apps create +# Always-warm single machine: this is a probe target — it must never +# scale to zero or backend status probes would hit cold starts. + +app = 'REPLACE-WITH-YOUR-APP-NAME' +primary_region = 'fra' + +[build] + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = 'off' + auto_start_machines = true + min_machines_running = 1 + + [[http_service.checks]] + interval = '30s' + timeout = '5s' + grace_period = '10s' + method = 'GET' + path = '/healthz' + +[[vm]] + size = 'shared-cpu-1x' + memory = '256mb' diff --git a/demo/self-report/index.php b/demo/self-report/index.php new file mode 100644 index 0000000..74030a0 --- /dev/null +++ b/demo/self-report/index.php @@ -0,0 +1,100 @@ +handleRequest(RequestContext::fromGlobals()); + +foreach ($result->headers as $name => $value) { + header("{$name}: {$value}"); +} + +if (! $result instanceof AllowResult) { + // BLOCK and RESPOND both carry a complete response to emit — the + // RESPOND branch is what serves the self-report status endpoint. + http_response_code($result->status); + echo $result->body; + exit; +} + +// ── Allowed: minimal demo page ─────────────────────────────────────── +$sdkVersion = htmlspecialchars(HttpClient::resolveVersion(), ENT_QUOTES, 'UTF-8'); +$mode = htmlspecialchars($enforcement->value, ENT_QUOTES, 'UTF-8'); + +header('Content-Type: text/html; charset=UTF-8'); +echo << + + + + +Supertab Connect PHP SDK — self-report demo + + +

Supertab Connect PHP SDK — self-report demo

+

This site routes every request through SupertabConnect::handleRequest(). +The backend's status probe is answered at /.well-known/supertab/status +by the SDK itself — there is no endpoint-specific code here.

+
    +
  • SDK version: {$sdkVersion}
  • +
  • Enforcement mode: {$mode}
  • +
+ + +HTML;