@produtype/core 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -191,7 +191,7 @@ prodkit plan ../my-app --output prodkit-plan.md
191
191
 
192
192
  _Generated from the analyzer itself — run `npm run docs:stacks` after changing a detector._
193
193
 
194
- - **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, aiohttp, Litestar, Sanic, Tornado, Starlette, Streamlit, Gradio, Dash, Chainlit, Gin, Echo, Fiber, chi, Gorilla, Beego, Go, Axum, Actix Web, Rocket, Warp, Tide, Poem, Salvo, Tower HTTP, Hyper, Spring Boot, Quarkus, Micronaut, Ktor, Javalin, Vert.x, Dropwizard, Helidon, Rails, Sinatra, Hanami, Roda, Grape, Ruby, Laravel, Symfony, Slim, CodeIgniter, CakePHP, Yii, PHP, ASP.NET Core, .NET
194
+ - **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, aiohttp, Litestar, Sanic, Tornado, Starlette, Streamlit, Gradio, Dash, Chainlit, Gin, Echo, Fiber, chi, Gorilla, Beego, Go, Axum, Actix Web, Rocket, Warp, Tide, Poem, Salvo, Tower HTTP, Hyper, Spring Boot, Quarkus, Micronaut, Ktor, Javalin, Vert.x, Dropwizard, Helidon, Rails, Sinatra, Hanami, Roda, Grape, Ruby, Laravel, Symfony, Slim, CodeIgniter, CakePHP, Yii, PHP, ASP.NET Core, Cloudflare Workers
195
195
  - **Frontend:** React, Vite, Vue, Nuxt, Svelte, Angular, Astro, Solid, Qwik, Preact, Remix, htmx, Tailwind CSS, Electron
196
196
  - **Mobile:** Flutter, React Native, iOS (native), Android (native), SwiftUI, UIKit, Jetpack Compose, Android views
197
197
  - **Databases:** Postgres, MySQL, SQLite, SQL Server, MongoDB, Redis, Firestore, DynamoDB, Convex
@@ -210,7 +210,7 @@ How some of these are decided:
210
210
  - **Ruby** — a Gemfile with no web framework in it
211
211
  - **PHP** — PHP sources with no framework in composer.json
212
212
  - **ASP.NET Core** — the Microsoft.NET.Sdk.Web SDK attribute
213
- - **.NET** — a .csproj with no web SDK
213
+ - **Cloudflare Workers** — a wrangler manifest beside a module exporting a fetch handler
214
214
  - **Flutter** — pubspec.yaml — classified as a client application, not a backend
215
215
  - **React Native** — the react-native or expo dependency
216
216
  - **iOS (native)** — Info.plist, Package.swift, a Podfile or an .xcodeproj in the tree
@@ -245,6 +245,7 @@ const LABELS = {
245
245
  electron: 'Electron',
246
246
  flutter: 'Flutter',
247
247
  'react-native': 'React Native',
248
+ 'cloudflare workers': 'Cloudflare Workers',
248
249
  ios: 'iOS (native)',
249
250
  android: 'Android (native)',
250
251
  swiftui: 'SwiftUI',
@@ -348,7 +349,11 @@ function supportedStacks() {
348
349
  ...entries(exports.PHP_BACKEND_FRAMEWORKS.map(([id]) => id)),
349
350
  { id: 'php', label: 'PHP', detectedFrom: 'PHP sources with no framework in composer.json' },
350
351
  { id: 'aspnet-core', label: 'ASP.NET Core', detectedFrom: 'the Microsoft.NET.Sdk.Web SDK attribute' },
351
- { id: 'dotnet', label: '.NET', detectedFrom: 'a .csproj with no web SDK' },
352
+ {
353
+ id: 'cloudflare workers',
354
+ label: 'Cloudflare Workers',
355
+ detectedFrom: 'a wrangler manifest beside a module exporting a fetch handler',
356
+ },
352
357
  ],
353
358
  frontend: [
354
359
  { id: 'react', label: 'React' },
@@ -141,6 +141,35 @@ async function detectAuth(ctx) {
141
141
  /createServerClient/,
142
142
  /\[\.\.\.nextauth\]/i,
143
143
  ], 30);
144
+ /**
145
+ * Identity the platform supplies, under a name the author did not choose.
146
+ *
147
+ * Cloudflare Access sits in front of Orange Meets and puts a signed JWT on every
148
+ * request as the `CF_Authorization` cookie; `app/root.tsx` decodes it, checks its
149
+ * expiry and redirects when it is about to lapse. There is no auth package in the
150
+ * manifest and no `/login` route, because signing in happens before the request
151
+ * reaches the application at all — so the report told a product behind an identity
152
+ * proxy that "anyone who finds a URL can use the product and read whatever it
153
+ * exposes".
154
+ *
155
+ * The same arrangement has a name on every platform: Google IAP, an AWS ALB with
156
+ * OIDC, Azure App Service's Easy Auth. Each one publishes a fixed header or cookie,
157
+ * and reading it is how an application asks who is calling. Those names are the
158
+ * anchor — nobody in the repository invented `x-amzn-oidc-data`.
159
+ *
160
+ * It says the request is authenticated, not that the application authorizes
161
+ * anything: `hasAuthz` is decided separately and stays untouched.
162
+ */
163
+ const PLATFORM_IDENTITY = [
164
+ /\bCF_Authorization\b/,
165
+ /\bCf-Access-Jwt-Assertion\b/i,
166
+ /\bcf-access-authenticated-user-email\b/i,
167
+ /\bx-goog-iap-jwt-assertion\b/i,
168
+ /\bx-goog-authenticated-user-email\b/i,
169
+ /\bx-amzn-oidc-(?:data|identity|accesstoken)\b/i,
170
+ /\bX-MS-CLIENT-PRINCIPAL(?:-NAME|-ID)?\b/i,
171
+ ];
172
+ const platformIdentity = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, PLATFORM_IDENTITY, 10);
144
173
  /**
145
174
  * `otp` as a word, not as three letters inside another one.
146
175
  *
@@ -394,7 +423,7 @@ async function detectAuth(ctx) {
394
423
  */
395
424
  const structuralOwnership = await (0, ownershipChecks_1.readOwnershipChecks)(ctx.root, sourceFiles);
396
425
  const ownershipUnasked = (0, readingDepth_1.wentUnasked)(structuralOwnership, sourceFiles) && (await (0, ownershipChecks_1.anyFileImportsExpress)(ctx.root, sourceFiles));
397
- const hasAuth = authDeps.length > 0 || routeSignals.length > 0;
426
+ const hasAuth = authDeps.length > 0 || routeSignals.length > 0 || platformIdentity.length > 0;
398
427
  const hasAuthz = permissionSignals.length > 0 || roleSignals.length > 0;
399
428
  const b2bHint = b2bSignals.length > 0;
400
429
  const hasOrganization = organizationSignals.length > 0;
@@ -403,7 +432,7 @@ async function detectAuth(ctx) {
403
432
  key: 'auth.core',
404
433
  present: hasAuth,
405
434
  complete: hasAuth && hasAuthz,
406
- evidence: (0, absenceEvidence_1.evidenceOrSearch)([...depEvidence(authDeps), ...authUseEvidence, ...snippetEvidence(routeSignals)], 'a way for somebody to sign in', ['next-auth', 'passport', 'lucia', '@clerk/', '@supabase/auth', 'django.contrib.auth', 'devise', 'jsonwebtoken', 'a /login or /signin route', 'signIn(', 'authenticate(']),
435
+ evidence: (0, absenceEvidence_1.evidenceOrSearch)([...depEvidence(authDeps), ...authUseEvidence, ...snippetEvidence(routeSignals), ...snippetEvidence(platformIdentity)], 'a way for somebody to sign in', ['next-auth', 'passport', 'lucia', '@clerk/', '@supabase/auth', 'django.contrib.auth', 'devise', 'jsonwebtoken', 'a /login or /signin route', 'signIn(', 'authenticate(', 'a Cloudflare Access, Google IAP, AWS ALB or Azure Easy Auth identity header']),
407
436
  details: {
408
437
  hasAuth,
409
438
  hasAuthz,
@@ -227,6 +227,34 @@ async function detectBackend(ctx) {
227
227
  * in the package manager, and "this is a .NET project" was never an answer to
228
228
  * "what serves the requests".
229
229
  */
230
+ /**
231
+ * A Cloudflare Worker, which is a server with no server framework in it.
232
+ *
233
+ * `wrangler.toml` names the entry module and the compatibility date; the module
234
+ * exports an object with a `fetch` handler. That pair is Cloudflare's documented
235
+ * contract and there is no other way to write a Worker — but together they were read
236
+ * as nothing at all: no backend, no stack, and a report that declined to score.
237
+ *
238
+ * Both halves are required. A `wrangler.toml` beside a static site deploys Pages and
239
+ * serves no requests of its own, and an `export default { fetch }` with no manifest
240
+ * is a module somebody may import.
241
+ *
242
+ * Weaker than the rest of this file, and worth saying: the shape was diagnosed on a
243
+ * case built from Cloudflare's documentation rather than found in a repository. Every
244
+ * other rule here has a named project behind it. A real Worker should confirm this
245
+ * one.
246
+ */
247
+ const wranglerManifests = ctx.files.all.filter((f) => /(^|\/)wrangler\.(toml|jsonc?)$/.test(f));
248
+ if (wranglerManifests.length > 0 && !frameworks.includes('hono')) {
249
+ const fetchHandler = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/export\s+default\s*\{[\s\S]{0,120}?\bfetch\s*\(/, /^\s*async\s+fetch\s*\(\s*request/m], 2);
250
+ if (fetchHandler.length > 0) {
251
+ frameworks.push('cloudflare workers');
252
+ evidence.push({ type: 'file', value: wranglerManifests[0], file: wranglerManifests[0] });
253
+ for (const hit of fetchHandler.slice(0, 1)) {
254
+ evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line });
255
+ }
256
+ }
257
+ }
230
258
  /**
231
259
  * Astro, which is a backend only when it is configured to be one.
232
260
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
5
5
  "license": "MIT",
6
6
  "bin": {