diff --git a/.github/workflows/distribution-catalog.yml b/.github/workflows/distribution-catalog.yml index 8321e35..c7a3fc1 100644 --- a/.github/workflows/distribution-catalog.yml +++ b/.github/workflows/distribution-catalog.yml @@ -5,6 +5,8 @@ on: paths: - ".github/workflows/distribution-catalog.yml" - "distribution/**" + - "auth_samples_*/.github/social-preview.*" + - "ai_saas_starter/.github/social-preview.png" - "LICENSE" - "README.md" push: @@ -13,6 +15,8 @@ on: paths: - ".github/workflows/distribution-catalog.yml" - "distribution/**" + - "auth_samples_*/.github/social-preview.*" + - "ai_saas_starter/.github/social-preview.png" - "LICENSE" - "README.md" @@ -47,5 +51,8 @@ jobs: - name: Validate catalog run: npm run validate + - name: Check social previews are reproducible + run: npm run previews && git diff --exit-code -- ../auth_samples_*/.github/social-preview.svg + - name: Audit validator dependencies run: npm audit --audit-level=moderate diff --git a/.github/workflows/sync-satellites.yml b/.github/workflows/sync-satellites.yml index d47d386..3c21bb7 100644 --- a/.github/workflows/sync-satellites.yml +++ b/.github/workflows/sync-satellites.yml @@ -38,6 +38,9 @@ jobs: - name: Install distribution tooling working-directory: distribution run: npm ci + - name: Validate catalog and managed assets before synchronization + working-directory: distribution + run: npm run validate - name: Select affected templates id: selection working-directory: distribution diff --git a/README.md b/README.md index c2d2e9c..bdcbfc1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ The samples in this repository show practical Tuurio ID client integrations for | Vue 3 | Vite + Composition API | [Vue guide](https://id.tuurio.com/public/developers/examples/vue) | [auth_samples_vue3](./auth_samples_vue3) | | Angular | Standalone Components | [Angular guide](https://id.tuurio.com/public/developers/examples/angular) | [auth_samples_angular](./auth_samples_angular) | | Next.js | App Router + Auth.js | [Next.js guide](https://id.tuurio.com/public/developers/examples/nextjs) | [auth_samples_nextjs](./auth_samples_nextjs) | +| SvelteKit | SvelteKit + server hooks | [SvelteKit guide](https://id.tuurio.com/public/developers/examples/sveltekit) | [auth_samples_sveltekit](./auth_samples_sveltekit) | +| Nuxt | Nuxt + server routes | [Nuxt guide](https://id.tuurio.com/public/developers/examples/nuxt) | [auth_samples_nuxt](./auth_samples_nuxt) | +| Astro | Astro + server sessions | [Astro guide](https://id.tuurio.com/public/developers/examples/astro) | [auth_samples_astro](./auth_samples_astro) | +| React Router | React Router framework mode | [React Router guide](https://id.tuurio.com/public/developers/examples/react-router) | [auth_samples_react_router](./auth_samples_react_router) | ### Mobile and Native @@ -52,6 +56,8 @@ The samples in this repository show practical Tuurio ID client integrations for | Java | Spring Boot 3 | [Spring Boot guide](https://id.tuurio.com/public/developers/examples/spring-boot) | [auth_samples_java](./auth_samples_java) | | PHP | Vanilla PHP | [PHP guide](https://id.tuurio.com/public/developers/examples/php) | [auth_samples_php](./auth_samples_php) | | Laravel | Laravel 12 | [Laravel guide](https://id.tuurio.com/public/developers/examples/laravel) | [auth_samples_laravel](./auth_samples_laravel) | +| Python | Django | [Django guide](https://id.tuurio.com/public/developers/examples/django) | [auth_samples_django](./auth_samples_django) | +| Python | FastAPI | [FastAPI guide](https://id.tuurio.com/public/developers/examples/fastapi) | [auth_samples_fastapi](./auth_samples_fastapi) | ## Getting Started @@ -75,7 +81,7 @@ npm run dev ## Template distribution -The reviewed catalog and target repository metadata live in [`distribution/templates.yml`](./distribution/templates.yml). It currently inventories 14 implemented samples and six planned additions. The distribution pipeline validates, packages, and synchronizes each source directory into a separately discoverable GitHub template repository without making the satellite an independent source of truth. +The reviewed catalog and target repository metadata live in [`distribution/templates.yml`](./distribution/templates.yml). It inventories 20 implemented framework templates. The distribution pipeline validates, packages, and synchronizes each source directory into a separately discoverable GitHub template repository without making the satellite an independent source of truth. Validate the catalog with: diff --git a/auth_samples_android/.github/social-preview.png b/auth_samples_android/.github/social-preview.png new file mode 100644 index 0000000..351d91b Binary files /dev/null and b/auth_samples_android/.github/social-preview.png differ diff --git a/auth_samples_android/.github/social-preview.svg b/auth_samples_android/.github/social-preview.svg new file mode 100644 index 0000000..baf559f --- /dev/null +++ b/auth_samples_android/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR ANDROID + Android OIDC AuthStarter + Native sign-in with Authorization Code + PKCE + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_angular/.github/social-preview.png b/auth_samples_angular/.github/social-preview.png new file mode 100644 index 0000000..0be2738 Binary files /dev/null and b/auth_samples_angular/.github/social-preview.png differ diff --git a/auth_samples_angular/.github/social-preview.svg b/auth_samples_angular/.github/social-preview.svg new file mode 100644 index 0000000..645a790 --- /dev/null +++ b/auth_samples_angular/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR ANGULAR + Angular OIDC AuthStarter + Secure browser login with OIDC + PKCE + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_astro/.github/social-preview.png b/auth_samples_astro/.github/social-preview.png new file mode 100644 index 0000000..9376dd2 Binary files /dev/null and b/auth_samples_astro/.github/social-preview.png differ diff --git a/auth_samples_astro/.github/social-preview.svg b/auth_samples_astro/.github/social-preview.svg new file mode 100644 index 0000000..ffab8ce --- /dev/null +++ b/auth_samples_astro/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR ASTRO + Astro OIDC AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_django/.github/social-preview.png b/auth_samples_django/.github/social-preview.png new file mode 100644 index 0000000..a5d6e41 Binary files /dev/null and b/auth_samples_django/.github/social-preview.png differ diff --git a/auth_samples_django/.github/social-preview.svg b/auth_samples_django/.github/social-preview.svg new file mode 100644 index 0000000..ad2b343 --- /dev/null +++ b/auth_samples_django/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR DJANGO + Django OIDC AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_fastapi/.github/social-preview.png b/auth_samples_fastapi/.github/social-preview.png new file mode 100644 index 0000000..9bfab4e Binary files /dev/null and b/auth_samples_fastapi/.github/social-preview.png differ diff --git a/auth_samples_fastapi/.github/social-preview.svg b/auth_samples_fastapi/.github/social-preview.svg new file mode 100644 index 0000000..4fa24bf --- /dev/null +++ b/auth_samples_fastapi/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR FASTAPI + FastAPI OIDC AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_flutter/.github/social-preview.png b/auth_samples_flutter/.github/social-preview.png new file mode 100644 index 0000000..5dc017f Binary files /dev/null and b/auth_samples_flutter/.github/social-preview.png differ diff --git a/auth_samples_flutter/.github/social-preview.svg b/auth_samples_flutter/.github/social-preview.svg new file mode 100644 index 0000000..bdf5672 --- /dev/null +++ b/auth_samples_flutter/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR FLUTTER + Flutter OIDC AuthStarter + Native sign-in with Authorization Code + PKCE + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_go/.github/social-preview.png b/auth_samples_go/.github/social-preview.png new file mode 100644 index 0000000..80bccec Binary files /dev/null and b/auth_samples_go/.github/social-preview.png differ diff --git a/auth_samples_go/.github/social-preview.svg b/auth_samples_go/.github/social-preview.svg new file mode 100644 index 0000000..3406508 --- /dev/null +++ b/auth_samples_go/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR GO + Go OIDC Auth Starter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_ios/.github/social-preview.png b/auth_samples_ios/.github/social-preview.png new file mode 100644 index 0000000..45a01d9 Binary files /dev/null and b/auth_samples_ios/.github/social-preview.png differ diff --git a/auth_samples_ios/.github/social-preview.svg b/auth_samples_ios/.github/social-preview.svg new file mode 100644 index 0000000..f411f42 --- /dev/null +++ b/auth_samples_ios/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR IOS + iOS OIDC Auth Starter + Native sign-in with Authorization Code + PKCE + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_java/.github/social-preview.png b/auth_samples_java/.github/social-preview.png new file mode 100644 index 0000000..cae3320 Binary files /dev/null and b/auth_samples_java/.github/social-preview.png differ diff --git a/auth_samples_java/.github/social-preview.svg b/auth_samples_java/.github/social-preview.svg new file mode 100644 index 0000000..f6e8b2e --- /dev/null +++ b/auth_samples_java/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR SPRING + Spring Boot OIDCStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_laravel/.github/social-preview.png b/auth_samples_laravel/.github/social-preview.png new file mode 100644 index 0000000..9ba4578 Binary files /dev/null and b/auth_samples_laravel/.github/social-preview.png differ diff --git a/auth_samples_laravel/.github/social-preview.svg b/auth_samples_laravel/.github/social-preview.svg new file mode 100644 index 0000000..1b6a92e --- /dev/null +++ b/auth_samples_laravel/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR LARAVEL + Laravel OIDC AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_lovable/.github/social-preview.png b/auth_samples_lovable/.github/social-preview.png new file mode 100644 index 0000000..a6ea345 Binary files /dev/null and b/auth_samples_lovable/.github/social-preview.png differ diff --git a/auth_samples_lovable/.github/social-preview.svg b/auth_samples_lovable/.github/social-preview.svg new file mode 100644 index 0000000..f29283d --- /dev/null +++ b/auth_samples_lovable/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR REACT + Lovable Auth Starter + Secure browser login with OIDC + PKCE + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_nextjs/.github/social-preview.png b/auth_samples_nextjs/.github/social-preview.png new file mode 100644 index 0000000..f8b35dc Binary files /dev/null and b/auth_samples_nextjs/.github/social-preview.png differ diff --git a/auth_samples_nextjs/.github/social-preview.svg b/auth_samples_nextjs/.github/social-preview.svg new file mode 100644 index 0000000..d30c251 --- /dev/null +++ b/auth_samples_nextjs/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR NEXTJS + Next.js Auth Starter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_node/.github/social-preview.png b/auth_samples_node/.github/social-preview.png new file mode 100644 index 0000000..7a3f45e Binary files /dev/null and b/auth_samples_node/.github/social-preview.png differ diff --git a/auth_samples_node/.github/social-preview.svg b/auth_samples_node/.github/social-preview.svg new file mode 100644 index 0000000..7513b69 --- /dev/null +++ b/auth_samples_node/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR NODE + Express OIDC AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_nuxt/.github/social-preview.png b/auth_samples_nuxt/.github/social-preview.png new file mode 100644 index 0000000..3333d4e Binary files /dev/null and b/auth_samples_nuxt/.github/social-preview.png differ diff --git a/auth_samples_nuxt/.github/social-preview.svg b/auth_samples_nuxt/.github/social-preview.svg new file mode 100644 index 0000000..e685a9b --- /dev/null +++ b/auth_samples_nuxt/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR NUXT + Nuxt OIDC Auth Starter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_php/.github/social-preview.png b/auth_samples_php/.github/social-preview.png new file mode 100644 index 0000000..a0af550 Binary files /dev/null and b/auth_samples_php/.github/social-preview.png differ diff --git a/auth_samples_php/.github/social-preview.svg b/auth_samples_php/.github/social-preview.svg new file mode 100644 index 0000000..bd96531 --- /dev/null +++ b/auth_samples_php/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR PHP + PHP OIDC Auth Starter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_python/.github/social-preview.png b/auth_samples_python/.github/social-preview.png new file mode 100644 index 0000000..4a7486e Binary files /dev/null and b/auth_samples_python/.github/social-preview.png differ diff --git a/auth_samples_python/.github/social-preview.svg b/auth_samples_python/.github/social-preview.svg new file mode 100644 index 0000000..98cc783 --- /dev/null +++ b/auth_samples_python/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR PYTHON + Flask OIDC AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_react/.github/social-preview.png b/auth_samples_react/.github/social-preview.png new file mode 100644 index 0000000..f4d73c3 Binary files /dev/null and b/auth_samples_react/.github/social-preview.png differ diff --git a/auth_samples_react/.github/social-preview.svg b/auth_samples_react/.github/social-preview.svg new file mode 100644 index 0000000..d76c2b3 --- /dev/null +++ b/auth_samples_react/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR REACT + React Vite OIDC Starter + Secure browser login with OIDC + PKCE + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_react_router/.github/social-preview.png b/auth_samples_react_router/.github/social-preview.png new file mode 100644 index 0000000..baee049 Binary files /dev/null and b/auth_samples_react_router/.github/social-preview.png differ diff --git a/auth_samples_react_router/.github/social-preview.svg b/auth_samples_react_router/.github/social-preview.svg new file mode 100644 index 0000000..babd5d5 --- /dev/null +++ b/auth_samples_react_router/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR REACT-ROUTER + React Router AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_sveltekit/.github/social-preview.png b/auth_samples_sveltekit/.github/social-preview.png new file mode 100644 index 0000000..6ee8ad6 Binary files /dev/null and b/auth_samples_sveltekit/.github/social-preview.png differ diff --git a/auth_samples_sveltekit/.github/social-preview.svg b/auth_samples_sveltekit/.github/social-preview.svg new file mode 100644 index 0000000..2f0b39b --- /dev/null +++ b/auth_samples_sveltekit/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR SVELTEKIT + SvelteKit OIDC AuthStarter + Server-side authentication with OpenID Connect + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/auth_samples_vue3/.github/social-preview.png b/auth_samples_vue3/.github/social-preview.png new file mode 100644 index 0000000..62b1b61 Binary files /dev/null and b/auth_samples_vue3/.github/social-preview.png differ diff --git a/auth_samples_vue3/.github/social-preview.svg b/auth_samples_vue3/.github/social-preview.svg new file mode 100644 index 0000000..c7e0034 --- /dev/null +++ b/auth_samples_vue3/.github/social-preview.svg @@ -0,0 +1,45 @@ + + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + TUURIO ID FOR VUE + Vue OIDC Auth Starter + Secure browser login with OIDC + PKCE + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + + diff --git a/distribution/README.md b/distribution/README.md index db66aa1..6141927 100644 --- a/distribution/README.md +++ b/distribution/README.md @@ -18,6 +18,7 @@ From `distribution/`: ```bash npm ci +npm run previews npm run validate npm run affected -- --base --head npm run sync -- --id react-vite @@ -26,6 +27,8 @@ npm run verify:remotes -- --id react-vite Dry-run is the default. Add `--apply` only after reviewing the package and target repository. A manual full-catalog synchronization omits `--id`; the GitHub workflow is preferred because it provides per-template isolation and summaries. +`npm run previews` deterministically regenerates the 1280 × 640 PNG and SVG social-preview assets for all 20 framework templates with a pinned renderer and font files. Use `npm run previews -- --id react-vite` for a single catalog entry. The validator compares generated PNG pixels as well as SVG source and rejects missing, stale, oversized, transparent, or incorrectly sized assets before synchronization. The AI SaaS Starter retains separate curated artwork that is validated but never overwritten by this generator. + ## Contribution policy Implementation changes belong in this repository under the source path named in the satellite's `.tuurio-template.json`. Open pull requests against `Tuurio/auth_samples`, run the source sample's tests plus `npm run validate` in `distribution/`, and let the post-merge workflow propagate the reviewed result. diff --git a/distribution/README.template.md b/distribution/README.template.md index 0d862a5..4048e0d 100644 --- a/distribution/README.template.md +++ b/distribution/README.template.md @@ -4,6 +4,8 @@ [![Verify template](https://github.com/{{repository}}/actions/workflows/verify.yml/badge.svg)](https://github.com/{{repository}}/actions/workflows/verify.yml) +![{{displayName}} social preview](.github/social-preview.png) + > Generated from [`Tuurio/auth_samples/{{source}}`](https://github.com/Tuurio/auth_samples/tree/main/{{source}}). Submit implementation fixes upstream so they are not replaced by the next synchronized release. ## What you get diff --git a/distribution/package-lock.json b/distribution/package-lock.json index d63aadf..02284fc 100644 --- a/distribution/package-lock.json +++ b/distribution/package-lock.json @@ -6,9 +6,866 @@ "": { "name": "@tuurio/auth-sample-distribution", "devDependencies": { + "@resvg/resvg-js": "2.6.2", + "dejavu-fonts-ttf": "2.37.3", + "sharp": "0.35.3", "yaml": "2.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/dejavu-fonts-ttf": { + "version": "2.37.3", + "resolved": "https://registry.npmjs.org/dejavu-fonts-ttf/-/dejavu-fonts-ttf-2.37.3.tgz", + "integrity": "sha512-f1hd7jJbeQa1VWcw+K2KrTXS50zTMaHpVC4XIKJpNcDeYR5ajMtj/iLlQDYNvLOKamUB3ARVVCf79lNwNVztSQ==", + "dev": true, + "license": "SEE LICENSE IN README.md AND LICENSE" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", diff --git a/distribution/package.json b/distribution/package.json index 221f755..7e164f8 100644 --- a/distribution/package.json +++ b/distribution/package.json @@ -4,14 +4,18 @@ "type": "module", "scripts": { "test": "node --test scripts/*.test.mjs", - "validate": "npm test && node scripts/validate-manifest.mjs", + "validate": "npm test && node scripts/validate-manifest.mjs && node scripts/validate-social-previews.mjs", "package": "node scripts/package-template.mjs", + "previews": "node scripts/generate-social-previews.mjs", "affected": "node scripts/affected-templates.mjs", "bootstrap": "node scripts/bootstrap-template-repos.mjs", "sync": "node scripts/sync-template-repos.mjs", "verify:remotes": "node scripts/verify-template-repos.mjs" }, "devDependencies": { + "@resvg/resvg-js": "2.6.2", + "dejavu-fonts-ttf": "2.37.3", + "sharp": "0.35.3", "yaml": "2.9.0" } } diff --git a/distribution/scripts/generate-social-previews.mjs b/distribution/scripts/generate-social-previews.mjs new file mode 100644 index 0000000..916613c --- /dev/null +++ b/distribution/scripts/generate-social-previews.mjs @@ -0,0 +1,201 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; +import { Resvg } from "@resvg/resvg-js"; +import sharp from "sharp"; +import { loadCatalog, parseCommonArgs, repositoryRoot, selectTemplates } from "./catalog.mjs"; + +export const SOCIAL_PREVIEW_WIDTH = 1280; +export const SOCIAL_PREVIEW_HEIGHT = 640; +const socialPreviewRenderFontFamily = "DejaVu Sans"; +const socialPreviewSvgFontFamily = "DejaVu Sans, Arial, Helvetica, sans-serif"; +const socialPreviewFontFiles = [ + resolve(import.meta.dirname, "../node_modules/dejavu-fonts-ttf/ttf/DejaVuSans.ttf"), + resolve(import.meta.dirname, "../node_modules/dejavu-fonts-ttf/ttf/DejaVuSans-Bold.ttf"), +]; +const socialPreviewContentX = 72; +const socialPreviewPanelX = 860; +const socialPreviewTitleRightPadding = 48; +const socialPreviewTitleFontSize = 58; +const socialPreviewTitleFontWeight = 700; +const socialPreviewTitleLetterSpacing = -1.5; +const socialPreviewTitleMaximumWidth = socialPreviewPanelX - socialPreviewContentX - socialPreviewTitleRightPadding; + +for (const fontFile of socialPreviewFontFiles) { + if (!existsSync(fontFile)) { + throw new Error(`Social preview font file is missing: ${fontFile}. Run npm ci in distribution/.`); + } +} + +const outcomeByKind = { + browser: "Secure browser login with OIDC + PKCE", + native: "Native sign-in with Authorization Code + PKCE", + server: "Server-side authentication with OpenID Connect", +}; + +const accentByKind = { + browser: "#4d94ff", + native: "#f2b84b", + server: "#6ee7b7", +}; + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function createResvg(svg) { + return new Resvg(svg, { + background: "#07111f", + font: { + fontFiles: socialPreviewFontFiles, + loadSystemFonts: false, + defaultFontFamily: socialPreviewRenderFontFamily, + }, + }); +} + +function textWidth(value, { fontSize, fontWeight, letterSpacing }) { + const svg = `${escapeXml(value)}`; + return createResvg(svg).getBBox()?.width ?? Number.POSITIVE_INFINITY; +} + +function titleWidth(value) { + return textWidth(value, { + fontSize: socialPreviewTitleFontSize, + fontWeight: socialPreviewTitleFontWeight, + letterSpacing: socialPreviewTitleLetterSpacing, + }); +} + +function titleLines(value, maximumWidth = socialPreviewTitleMaximumWidth) { + if (!/^[\x20-\x7E]+$/.test(value)) { + throw new Error(`${value}: displayName contains characters outside the reviewed social-preview font set`); + } + const words = value.split(/\s+/).filter(Boolean); + const lines = []; + let current = ""; + for (const word of words) { + if (titleWidth(word) > maximumWidth) { + throw new Error(`${value}: displayName contains a word that does not fit the social preview`); + } + const candidate = current ? `${current} ${word}` : word; + if (current && titleWidth(candidate) > maximumWidth) { + lines.push(current); + current = word; + } else { + current = candidate; + } + } + if (current) lines.push(current); + if (lines.length > 2) { + throw new Error(`${value}: displayName does not fit two social-preview title lines`); + } + return lines; +} + +export function renderSocialPreviewSvg(template) { + const accent = accentByKind[template.kind] ?? accentByKind.browser; + const outcome = outcomeByKind[template.kind] ?? outcomeByKind.browser; + if (!/^[a-z0-9.+-]{1,24}$/.test(template.framework)) { + throw new Error(`${template.framework}: framework must use 1-24 lowercase ASCII slug characters`); + } + const eyebrow = `TUURIO ID FOR ${template.framework.toUpperCase()}`; + if ( + textWidth(eyebrow, { fontSize: 16, fontWeight: 700, letterSpacing: 3 }) > socialPreviewTitleMaximumWidth + ) { + throw new Error(`${template.framework}: framework eyebrow does not fit the social preview`); + } + const lines = titleLines(template.displayName).map(escapeXml); + const title = lines + .map( + (line, index) => `${line}`, + ) + .join(""); + const outcomeY = lines.length > 1 ? 390 : 330; + + return ` + + + + + + + + t + Tuurio ID + + + STARTER TEMPLATE + + ${eyebrow} + ${title} + ${escapeXml(outcome)} + + + + OpenID Connect + + PKCE + + Apache-2.0 + + + + SECURE SIGN-IN FLOW + + + + + APP + ID + + Your app + Tuurio ID + Signed in + + Vibe-code the app. + Not the security. + EU-hosted identity · Exact redirects + +`; +} + +export async function renderSocialPreviewPng(svg) { + const rendered = createResvg(svg).render().asPng(); + return sharp(rendered) + .flatten({ background: "#07111f" }) + .png({ compressionLevel: 9, palette: false }) + .toBuffer(); +} + +export async function writeSocialPreview(template, { root = repositoryRoot } = {}) { + const targetDirectory = resolve(root, template.source, ".github"); + const svgPath = resolve(targetDirectory, "social-preview.svg"); + const pngPath = resolve(targetDirectory, "social-preview.png"); + const svg = renderSocialPreviewSvg(template); + const png = await renderSocialPreviewPng(svg); + mkdirSync(targetDirectory, { recursive: true }); + writeFileSync(svgPath, `${svg}\n`); + writeFileSync(pngPath, png); + return { id: template.id, svgPath, pngPath }; +} + +export async function generateSocialPreviews({ ids = [], root = repositoryRoot } = {}) { + const templates = selectTemplates(loadCatalog({ root }), ids); + return Promise.all(templates.map((template) => writeSocialPreview(template, { root }))); +} + +if (fileURLToPath(import.meta.url) === resolve(process.argv[1] ?? "")) { + const args = parseCommonArgs(process.argv.slice(2)); + if (args.output || args.apply || args.initialize) { + throw new Error("Social previews support only optional repeated --id arguments"); + } + const results = await generateSocialPreviews({ ids: args.ids }); + console.log(JSON.stringify({ generated: results.length, templates: results.map(({ id }) => id) })); +} diff --git a/distribution/scripts/generate-social-previews.test.mjs b/distribution/scripts/generate-social-previews.test.mjs new file mode 100644 index 0000000..81b54f1 --- /dev/null +++ b/distribution/scripts/generate-social-previews.test.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import test from "node:test"; +import sharp from "sharp"; +import { + generateSocialPreviews, + renderSocialPreviewPng, + renderSocialPreviewSvg, + SOCIAL_PREVIEW_HEIGHT, + SOCIAL_PREVIEW_WIDTH, + writeSocialPreview, +} from "./generate-social-previews.mjs"; + +const template = { + id: "react-vite", + displayName: "React Vite OIDC Starter", + framework: "react", + kind: "browser", + source: "sample", +}; + +test("renders a branded preview without transient or sensitive values", () => { + const svg = renderSocialPreviewSvg(template); + assert.match(svg, /Tuurio ID/); + assert.match(svg.replace(/<[^>]+>/g, ""), /React Vite OIDC Starter/); + assert.match(svg, /Secure browser login with OIDC \+ PKCE/); + assert.match(svg, /EU-hosted identity/); + assert.match(svg, /font-family="DejaVu Sans, Arial, Helvetica, sans-serif"/); + assert.doesNotMatch(svg, /tenantId|client_secret|redirect-uri|@1\./i); +}); + +test("escapes manifest text before rendering SVG", () => { + const svg = renderSocialPreviewSvg({ ...template, displayName: "React & " }); + assert.match(svg, /React & <Vite>/); + assert.doesNotMatch(svg, /React & /); +}); + +test("rejects titles that cannot fit without truncation", () => { + assert.throws( + () => renderSocialPreviewSvg({ ...template, displayName: "One Two Three Four Five Six Seven Eight Nine" }), + /does not fit two social-preview title lines/, + ); + assert.throws( + () => renderSocialPreviewSvg({ ...template, displayName: "ThisSingleWordCannotFitInTheAvailableSpace" }), + /contains a word that does not fit/, + ); + assert.throws( + () => renderSocialPreviewSvg({ ...template, displayName: "WWWWWWWWWWWWWW" }), + /contains a word that does not fit/, + ); + assert.throws( + () => renderSocialPreviewSvg({ ...template, displayName: "React 登录 Starter" }), + /outside the reviewed social-preview font set/, + ); +}); + +test("never routes curated product artwork through the framework generator", async () => { + await assert.rejects(generateSocialPreviews({ ids: ["ai-saas"] }), /Unknown template id\(s\): ai-saas/); +}); + +test("renders the same PNG bytes on repeated invocations in one runtime", async () => { + const svg = renderSocialPreviewSvg(template); + const [first, second] = await Promise.all([renderSocialPreviewPng(svg), renderSocialPreviewPng(svg)]); + assert.ok(first.equals(second)); +}); + +test("writes a 1280 by 640 RGB PNG and its SVG source", async () => { + const root = mkdtempSync(resolve(tmpdir(), "tuurio-social-preview-")); + try { + const result = await writeSocialPreview(template, { root }); + const metadata = await sharp(result.pngPath).metadata(); + assert.equal(metadata.width, SOCIAL_PREVIEW_WIDTH); + assert.equal(metadata.height, SOCIAL_PREVIEW_HEIGHT); + assert.equal(metadata.hasAlpha, false); + assert.match(readFileSync(result.svgPath, "utf8"), /STARTER TEMPLATE/); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/distribution/scripts/validate-manifest.mjs b/distribution/scripts/validate-manifest.mjs index c4d96ef..94c98b9 100644 --- a/distribution/scripts/validate-manifest.mjs +++ b/distribution/scripts/validate-manifest.mjs @@ -136,6 +136,9 @@ export const validateManifest = (manifest, { repositoryRoot = root } = {}) => { errors.push(`${prefix}: ${field} is required when files are configured`); } } + if (typeof template.framework === "string" && !/^[a-z0-9.+-]{1,24}$/.test(template.framework)) { + errors.push(`${prefix}: framework must use 1-24 lowercase ASCII slug characters`); + } } if (template.status === "ready") { try { diff --git a/distribution/scripts/validate-manifest.test.mjs b/distribution/scripts/validate-manifest.test.mjs index 6878fc8..fc5e914 100644 --- a/distribution/scripts/validate-manifest.test.mjs +++ b/distribution/scripts/validate-manifest.test.mjs @@ -91,3 +91,10 @@ test("requires packaging metadata with an allow-list", () => { assert.ok(errors.some((error) => error.includes("files must contain"))); assert.ok(errors.some((error) => error.includes("framework is required"))); }); + +test("rejects a framework label that cannot render safely", () => { + const manifest = copy(); + manifest.templates[0].framework = "React framework with spaces 登录"; + const { errors } = validateManifest(manifest, { repositoryRoot: root }); + assert.ok(errors.some((error) => error.includes("framework must use 1-24 lowercase ASCII slug characters"))); +}); diff --git a/distribution/scripts/validate-social-previews.mjs b/distribution/scripts/validate-social-previews.mjs new file mode 100644 index 0000000..7d1b46f --- /dev/null +++ b/distribution/scripts/validate-social-previews.mjs @@ -0,0 +1,140 @@ +import { existsSync, readFileSync, statSync } from "node:fs"; +import { resolve } from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; +import { loadCatalog, repositoryRoot } from "./catalog.mjs"; +import { + renderSocialPreviewPng, + renderSocialPreviewSvg, + SOCIAL_PREVIEW_HEIGHT, + SOCIAL_PREVIEW_WIDTH, +} from "./generate-social-previews.mjs"; + +const previewPaths = [".github/social-preview.png", ".github/social-preview.svg"]; +const ignoredChannelDelta = 2; +const maximumChannelDelta = 16; +const maximumMaterialPixelRatio = 0.001; + +function pixelsMatchWithinRasterTolerance(expected, actual) { + if ( + expected.info.width !== actual.info.width || + expected.info.height !== actual.info.height || + expected.info.channels !== actual.info.channels || + expected.data.length !== actual.data.length + ) { + return false; + } + + let largestChannelDelta = 0; + let materiallyDifferentPixels = 0; + for (let offset = 0; offset < expected.data.length; offset += expected.info.channels) { + let largestPixelDelta = 0; + for (let channel = 0; channel < expected.info.channels; channel += 1) { + const delta = Math.abs(expected.data[offset + channel] - actual.data[offset + channel]); + largestChannelDelta = Math.max(largestChannelDelta, delta); + largestPixelDelta = Math.max(largestPixelDelta, delta); + } + if (largestPixelDelta > ignoredChannelDelta) materiallyDifferentPixels += 1; + } + + const totalPixels = expected.info.width * expected.info.height; + return ( + largestChannelDelta <= maximumChannelDelta && + materiallyDifferentPixels / totalPixels <= maximumMaterialPixelRatio + ); +} + +async function validatePng(template, pngPath, errors) { + const prefix = template.id; + if (!existsSync(pngPath)) { + errors.push(`${prefix}: missing ${previewPaths[0]}`); + return null; + } + + try { + const metadata = await sharp(pngPath).metadata(); + if (metadata.width !== SOCIAL_PREVIEW_WIDTH || metadata.height !== SOCIAL_PREVIEW_HEIGHT) { + errors.push(`${prefix}: PNG must be ${SOCIAL_PREVIEW_WIDTH}x${SOCIAL_PREVIEW_HEIGHT}`); + } + if (metadata.hasAlpha) errors.push(`${prefix}: PNG must not have an alpha channel`); + if (statSync(pngPath).size > 1_000_000) errors.push(`${prefix}: PNG exceeds 1 MB`); + return metadata; + } catch (error) { + errors.push(`${prefix}: PNG cannot be decoded (${error.message})`); + return null; + } +} + +export async function validateGeneratedSocialPreview(template, { root = repositoryRoot } = {}) { + const errors = []; + const prefix = template.id; + for (const relativePath of previewPaths) { + if (!template.files?.includes(relativePath)) { + errors.push(`${prefix}: ${relativePath} is not in the managed file allow-list`); + } + } + + const svgPath = resolve(root, template.source, previewPaths[1]); + const pngPath = resolve(root, template.source, previewPaths[0]); + const expectedSvgSource = renderSocialPreviewSvg(template); + const expectedSvg = `${expectedSvgSource}\n`; + if (!existsSync(svgPath)) { + errors.push(`${prefix}: missing ${previewPaths[1]}`); + } else if (readFileSync(svgPath, "utf8") !== expectedSvg) { + errors.push(`${prefix}: SVG is stale; run npm run previews`); + } + + if (!(await validatePng(template, pngPath, errors))) return errors; + + const expectedPng = await renderSocialPreviewPng(expectedSvgSource); + const [expectedPixels, actualPixels] = await Promise.all([ + sharp(expectedPng).removeAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true }), + sharp(pngPath).removeAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true }), + ]); + if (!pixelsMatchWithinRasterTolerance(expectedPixels, actualPixels)) { + errors.push(`${prefix}: PNG is stale; run npm run previews`); + } + return errors; +} + +export async function validateCuratedSocialPreview(entry, { root = repositoryRoot } = {}) { + const errors = []; + if (!entry.files?.includes(previewPaths[0])) { + errors.push(`${entry.id}: ${previewPaths[0]} is not in the managed file allow-list`); + } + await validatePng(entry, resolve(root, entry.source, previewPaths[0]), errors); + return errors; +} + +export async function validateSocialPreviews({ root = repositoryRoot } = {}) { + const errors = []; + const manifest = loadCatalog({ root }); + let generatedChecked = 0; + let curatedChecked = 0; + + for (const template of manifest.templates) { + generatedChecked += 1; + errors.push(...(await validateGeneratedSocialPreview(template, { root }))); + } + + // Product artwork is deliberately bespoke. Validate its packaging constraints, + // but never overwrite it with the framework-template generator. + for (const product of manifest.products ?? []) { + curatedChecked += 1; + errors.push(...(await validateCuratedSocialPreview(product, { root }))); + } + + return { checked: generatedChecked + curatedChecked, generatedChecked, curatedChecked, errors }; +} + +if (fileURLToPath(import.meta.url) === resolve(process.argv[1] ?? "")) { + const result = await validateSocialPreviews(); + if (result.errors.length) { + console.error(result.errors.map((error) => `- ${error}`).join("\n")); + process.exit(1); + } + console.log( + `Validated ${result.generatedChecked} generated social previews and ${result.curatedChecked} curated product preview.`, + ); +} diff --git a/distribution/scripts/validate-social-previews.test.mjs b/distribution/scripts/validate-social-previews.test.mjs new file mode 100644 index 0000000..ef705d3 --- /dev/null +++ b/distribution/scripts/validate-social-previews.test.mjs @@ -0,0 +1,99 @@ +import assert from "node:assert/strict"; +import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import test from "node:test"; +import sharp from "sharp"; +import { distributables, loadCatalog } from "./catalog.mjs"; +import { writeSocialPreview } from "./generate-social-previews.mjs"; +import { + validateCuratedSocialPreview, + validateGeneratedSocialPreview, + validateSocialPreviews, +} from "./validate-social-previews.mjs"; + +const template = { + id: "test-react", + displayName: "Test React Starter", + framework: "react", + kind: "browser", + source: "sample", + files: [".github/social-preview.png", ".github/social-preview.svg"], +}; + +test("validates every checked-in framework social preview", async () => { + const manifest = loadCatalog(); + const result = await validateSocialPreviews(); + assert.equal(result.checked, distributables(manifest).length); + assert.equal(result.generatedChecked, manifest.templates.length); + assert.equal(result.curatedChecked, manifest.products.length); + assert.deepEqual(result.errors, []); +}); + +test("reports missing files and unmanaged preview paths", async () => { + const root = mkdtempSync(resolve(tmpdir(), "tuurio-preview-validation-")); + try { + const errors = await validateGeneratedSocialPreview({ ...template, files: [] }, { root }); + assert.ok(errors.some((error) => error.includes("social-preview.png is not in the managed file allow-list"))); + assert.ok(errors.some((error) => error.includes("social-preview.svg is not in the managed file allow-list"))); + assert.ok(errors.some((error) => error.includes("missing .github/social-preview.svg"))); + assert.ok(errors.some((error) => error.includes("missing .github/social-preview.png"))); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("detects stale SVG and PNG content", async () => { + const root = mkdtempSync(resolve(tmpdir(), "tuurio-preview-validation-")); + try { + const { svgPath, pngPath } = await writeSocialPreview(template, { root }); + writeFileSync(svgPath, ""); + await sharp({ + create: { width: 1280, height: 640, channels: 3, background: "#ffffff" }, + }) + .png() + .toFile(pngPath); + + const errors = await validateGeneratedSocialPreview(template, { root }); + assert.ok(errors.some((error) => error.includes("SVG is stale"))); + assert.ok(errors.some((error) => error.includes("PNG is stale"))); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("detects invalid dimensions, alpha, and oversized PNG files", async () => { + const root = mkdtempSync(resolve(tmpdir(), "tuurio-preview-validation-")); + try { + const { pngPath } = await writeSocialPreview(template, { root }); + await sharp({ + create: { width: 640, height: 320, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0.5 } }, + }) + .png() + .toFile(pngPath); + appendFileSync(pngPath, Buffer.alloc(1_000_000)); + + const errors = await validateGeneratedSocialPreview(template, { root }); + assert.ok(errors.some((error) => error.includes("PNG must be 1280x640"))); + assert.ok(errors.some((error) => error.includes("PNG must not have an alpha channel"))); + assert.ok(errors.some((error) => error.includes("PNG exceeds 1 MB"))); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("rejects corrupt curated product artwork and an unmanaged path", async () => { + const root = mkdtempSync(resolve(tmpdir(), "tuurio-preview-validation-")); + const product = { id: "test-product", source: "product", files: [] }; + try { + const directory = resolve(root, product.source, ".github"); + mkdirSync(directory, { recursive: true }); + writeFileSync(resolve(directory, "social-preview.png"), "not a PNG"); + + const errors = await validateCuratedSocialPreview(product, { root }); + assert.ok(errors.some((error) => error.includes("social-preview.png is not in the managed file allow-list"))); + assert.ok(errors.some((error) => error.includes("PNG cannot be decoded"))); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/distribution/social-previews/README.md b/distribution/social-previews/README.md index 74a4182..f673ad2 100644 --- a/distribution/social-previews/README.md +++ b/distribution/social-previews/README.md @@ -1,6 +1,25 @@ # Social preview specification -Every public satellite repository needs a 1280 × 640 social preview generated from the same reviewed design system. +Every public satellite repository needs a reviewed 1280 × 640 social preview. + +Generate all 20 framework previews from the reviewed catalog metadata: + +```bash +cd distribution +npm ci +npm run previews +``` + +Use `npm run previews -- --id react-vite` to regenerate one asset. Each source +directory receives a canonical `.github/social-preview.svg` and the upload-ready +`.github/social-preview.png`. Both files are synchronized to the corresponding +satellite. The AI SaaS Starter keeps its intentionally product-specific +`.github/social-preview.png`; validation checks its allow-list entry, dimensions, +alpha channel, file size, and decodability without replacing the artwork with a +framework card. GitHub does not currently expose a supported REST endpoint for +setting a repository's social preview, so upload each generated PNG once under +**Settings → General → Social preview**. Subsequent visual changes use the same +versioned source and generator. Required content: @@ -11,3 +30,12 @@ Required content: - High-contrast background and text that remains legible in GitHub's cropped previews. Do not place tenant identifiers, callback URLs, credentials, tokens, user data, or transient version numbers in social previews. + +The framework cards use the pinned DejaVu Sans font files and `resvg` renderer +from `distribution/package-lock.json`. The validator regenerates each expected +PNG and compares decoded pixels, so output remains independent of fonts +installed on the CI runner or contributor machine. A narrow raster tolerance +allows insignificant anti-aliasing differences between CPU architectures while +still rejecting visible or widespread drift. The SVG source declares +Arial, Helvetica, and the browser generic sans-serif as viewing fallbacks; the +committed PNG always uses the pinned DejaVu files. diff --git a/distribution/templates.yml b/distribution/templates.yml index 6189716..09cfb4e 100644 --- a/distribution/templates.yml +++ b/distribution/templates.yml @@ -20,7 +20,7 @@ templates: start: npm run dev -- --host 0.0.0.0 kind: browser stackblitz: true - files: [.env.example, .gitignore, eslint.config.js, index.html, package.json, package-lock.json, src, tsconfig.app.json, tsconfig.json, tsconfig.node.json, vite.config.ts] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, eslint.config.js, index.html, package.json, package-lock.json, src, tsconfig.app.json, tsconfig.json, tsconfig.node.json, vite.config.ts] topics: [react, vite, authentication, oidc, oauth2, pkce, typescript, tuurio, starter-template, eu-hosted] verify: [npm ci, npm run lint, npm run build] - id: lovable @@ -37,7 +37,7 @@ templates: start: npm run dev -- --host 0.0.0.0 kind: browser stackblitz: false - files: [.gitignore, index.html, package.json, package-lock.json, src, tsconfig.json, vite.config.ts] + files: [.gitignore, .github/social-preview.png, .github/social-preview.svg, index.html, package.json, package-lock.json, src, tsconfig.json, vite.config.ts] topics: [lovable, react, vite, authentication, oidc, oauth2, pkce, typescript, tuurio, eu-hosted] verify: [npm ci, npm run typecheck, npm run build] - id: vue @@ -54,7 +54,7 @@ templates: start: npm run dev -- --host 0.0.0.0 kind: browser stackblitz: true - files: [.env.example, .gitignore, index.html, package.json, package-lock.json, public, src, tsconfig.app.json, tsconfig.json, tsconfig.node.json, vite.config.ts] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, index.html, package.json, package-lock.json, public, src, tsconfig.app.json, tsconfig.json, tsconfig.node.json, vite.config.ts] topics: [vue, vue3, vite, authentication, oidc, oauth2, pkce, typescript, tuurio, starter-template] verify: [npm ci, npm run build] - id: angular @@ -72,7 +72,7 @@ templates: start: npm start -- --host 0.0.0.0 kind: browser stackblitz: true - files: [.env.example, .gitignore, angular.json, package.json, package-lock.json, scripts, src, tsconfig.app.json, tsconfig.json] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, angular.json, package.json, package-lock.json, scripts, src, tsconfig.app.json, tsconfig.json] topics: [angular, authentication, oidc, oauth2, pkce, typescript, tuurio, starter-template, eu-hosted] verify: [npm ci, npm run build] - id: nextjs @@ -89,7 +89,7 @@ templates: start: npm run dev kind: server stackblitz: true - files: [.env.example, app, components, lib, next-env.d.ts, package.json, package-lock.json, providers, tsconfig.json] + files: [.env.example, .github/social-preview.png, .github/social-preview.svg, app, components, lib, next-env.d.ts, package.json, package-lock.json, providers, tsconfig.json] topics: [nextjs, react, authentication, oidc, oauth2, typescript, tuurio, saas-starter, starter-template, eu-hosted] verify: [npm ci, npm run build] - id: android @@ -105,7 +105,7 @@ templates: packageManager: Gradle Wrapper kind: native stackblitz: false - files: [.gitignore, app, build.gradle.kts, gradle, gradle.properties, gradlew, gradlew.bat, settings.gradle.kts] + files: [.gitignore, .github/social-preview.png, .github/social-preview.svg, app, build.gradle.kts, gradle, gradle.properties, gradlew, gradlew.bat, settings.gradle.kts] topics: [android, kotlin, jetpack-compose, authentication, oidc, oauth2, pkce, appauth, tuurio, starter-template] verify: [./gradlew assembleDebug] - id: ios @@ -122,7 +122,7 @@ templates: runner: macos-15 kind: native stackblitz: false - files: [TuurioAuthSample] + files: [.github/social-preview.png, .github/social-preview.svg, TuurioAuthSample] topics: [ios, swift, swiftui, authentication, oidc, oauth2, pkce, appauth, tuurio, starter-template] verify: [swiftc -parse TuurioAuthSample/*.swift] - id: flutter @@ -138,7 +138,7 @@ templates: packageManager: Flutter pub kind: native stackblitz: false - files: [.gitignore, lib, pubspec.yaml, pubspec.lock] + files: [.gitignore, .github/social-preview.png, .github/social-preview.svg, lib, pubspec.yaml, pubspec.lock] topics: [flutter, dart, authentication, oidc, oauth2, pkce, appauth, mobile, tuurio, starter-template] verify: [flutter pub get, flutter analyze] - id: express @@ -155,7 +155,7 @@ templates: start: npm start kind: server stackblitz: true - files: [.env.example, .gitignore, package.json, package-lock.json, server.js, src] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, package.json, package-lock.json, server.js, src] topics: [nodejs, express, javascript, authentication, oidc, oauth2, openid-connect, tuurio, starter-template, eu-hosted] verify: [npm ci, node --check server.js, node --check src/oauth.js] - id: flask @@ -172,7 +172,7 @@ templates: start: python3 app.py kind: server stackblitz: false - files: [.env.example, .gitignore, app.py, config.py, requirements.txt, static] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, app.py, config.py, requirements.txt, static] topics: [python, flask, authentication, oidc, oauth2, openid-connect, tuurio, starter-template, eu-hosted] verify: [python3 -m pip install -r requirements.txt, python3 -m compileall -q .] - id: go @@ -189,7 +189,7 @@ templates: start: go run . kind: server stackblitz: false - files: [.env.example, .gitignore, go.mod, go.sum, main.go, public] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, go.mod, go.sum, main.go, public] topics: [go, golang, authentication, oidc, oauth2, openid-connect, tuurio, starter-template, eu-hosted] verify: [go test ./..., go build ./...] - id: spring-boot @@ -205,7 +205,7 @@ templates: packageManager: Gradle Wrapper kind: server stackblitz: false - files: [.env.example, build.gradle, gradle, gradlew, gradlew.bat, settings.gradle, src] + files: [.env.example, .github/social-preview.png, .github/social-preview.svg, build.gradle, gradle, gradlew, gradlew.bat, settings.gradle, src] topics: [java, spring-boot, spring-security, authentication, oidc, oauth2, openid-connect, tuurio, starter-template] verify: [./gradlew --no-daemon test, ./gradlew --no-daemon bootJar] - id: php @@ -222,7 +222,7 @@ templates: start: php -S 127.0.0.1:8080 router.php kind: server stackblitz: false - files: [.env.example, .gitignore, .htaccess, public, router.php, src] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, .htaccess, public, router.php, src] topics: [php, authentication, oidc, oauth2, openid-connect, tuurio, starter-template, eu-hosted] verify: [find . -name '*.php' -print0 | xargs -0 -n1 php -l] - id: laravel @@ -239,7 +239,7 @@ templates: start: php artisan serve --host=127.0.0.1 --port=8000 kind: server stackblitz: false - files: [.editorconfig, .env.example, .gitattributes, .gitignore, .styleci.yml, app, artisan, bootstrap, composer.json, composer.lock, config, database, phpunit.xml, public, resources, routes, storage, tests] + files: [.editorconfig, .env.example, .gitattributes, .gitignore, .github/social-preview.png, .github/social-preview.svg, .styleci.yml, app, artisan, bootstrap, composer.json, composer.lock, config, database, phpunit.xml, public, resources, routes, storage, tests] topics: [laravel, php, authentication, oidc, oauth2, openid-connect, tuurio, starter-template, eu-hosted] verify: [composer install --no-interaction, cp .env.example .env, php artisan key:generate, php artisan test] - id: sveltekit @@ -257,7 +257,7 @@ templates: start: npm run dev -- --host 0.0.0.0 kind: server stackblitz: true - files: [.env.example, .gitignore, package.json, package-lock.json, src, svelte.config.js, tsconfig.json, vite.config.ts] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, package.json, package-lock.json, src, svelte.config.js, tsconfig.json, vite.config.ts] topics: [svelte, sveltekit, authentication, oidc, oauth2, typescript, tuurio, saas-starter, starter-template] verify: [npm ci, npm run check, npm run build] - id: nuxt @@ -275,7 +275,7 @@ templates: start: npm run dev -- --host 0.0.0.0 kind: server stackblitz: true - files: [.env.example, .gitignore, app, nuxt.config.ts, package.json, package-lock.json, server, tsconfig.json] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, app, nuxt.config.ts, package.json, package-lock.json, server, tsconfig.json] topics: [nuxt, vue, authentication, oidc, oauth2, typescript, tuurio, saas-starter, starter-template] verify: [npm ci, npm run typecheck, npm run build] - id: astro @@ -293,7 +293,7 @@ templates: start: npm run dev -- --host 0.0.0.0 kind: server stackblitz: true - files: [.env.example, .gitignore, astro.config.mjs, package.json, package-lock.json, src, tsconfig.json] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, astro.config.mjs, package.json, package-lock.json, src, tsconfig.json] topics: [astro, authentication, oidc, oauth2, typescript, tuurio, saas-starter, starter-template, eu-hosted] verify: [npm ci, npm run check, npm run build] - id: react-router @@ -311,7 +311,7 @@ templates: start: npm run dev kind: server stackblitz: true - files: [.env.example, .gitignore, index.html, package.json, package-lock.json, server, src, tsconfig.json, tsconfig.server.json, vite.config.ts] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, index.html, package.json, package-lock.json, server, src, tsconfig.json, tsconfig.server.json, vite.config.ts] topics: [react, react-router, authentication, oidc, oauth2, typescript, tuurio, saas-starter, starter-template] verify: [npm ci, npm run typecheck, npm run build] - id: django @@ -328,7 +328,7 @@ templates: start: python3 manage.py runserver 0.0.0.0:8000 kind: server stackblitz: false - files: [.env.example, .gitignore, authapp, manage.py, requirements.txt, templates, tuurio_starter] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, authapp, manage.py, requirements.txt, templates, tuurio_starter] topics: [python, django, authentication, oidc, oauth2, openid-connect, tuurio, saas-starter, starter-template] verify: [python3 -m pip install -r requirements.txt, python3 manage.py check, python3 manage.py test] - id: fastapi @@ -345,7 +345,7 @@ templates: start: uvicorn app.main:app --host 0.0.0.0 --port 8000 kind: server stackblitz: false - files: [.env.example, .gitignore, app, pyproject.toml, requirements.txt, tests] + files: [.env.example, .gitignore, .github/social-preview.png, .github/social-preview.svg, app, pyproject.toml, requirements.txt, tests] topics: [python, fastapi, authentication, oidc, oauth2, openid-connect, tuurio, saas-starter, starter-template] verify: [python3 -m pip install -r requirements.txt, python3 -m compileall -q app tests, python3 -m pytest -q] products: