saasfunnels 0.4.0 → 0.4.2

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
@@ -51,7 +51,7 @@ approved set is reviewable.
51
51
  - uses: LeadEngine-ai/saasfunnels-cli/action@v0.2.1
52
52
  with:
53
53
  api-key: ${{ secrets.SAASFUNNELS_API_KEY }}
54
- cli-version: 0.2.1 # pin it; a floating version rebaselines drift
54
+ cli-version: 0.2.1 # pin it; a floating version rebaselines drift
55
55
  discovery-roots: app,lib
56
56
  ```
57
57
 
@@ -63,8 +63,8 @@ measured against a comparable scan rather than a wider or narrower one.
63
63
  Plan and pricing upload is opt-in:
64
64
 
65
65
  ```yaml
66
- plan-sources: "true"
67
- integration-id: <stripe integration id>
66
+ plan-sources: "true"
67
+ integration-id: <stripe integration id>
68
68
  ```
69
69
 
70
70
  It runs only on the default branch, requires a committed
@@ -100,7 +100,7 @@ Support: support@saasfunnels.ai
100
100
 
101
101
  Use `saasfunnels setup run` from the application repository after creating app credentials in Setup. Load `SAASFUNNELS_API_KEY` from **Developer setup access** into the local process environment. Its scopes are `developer:read` and `features:write`; it is not a browser or payment-administration key.
102
102
 
103
- The command asks before uploading structured discovery proposals. `--send` provides explicit non-interactive approval. It coordinates features, pricing extraction, and plan branches, reports progress, and waits for the Stripe catalog. Run `saasfunnels setup run --resume` after an interruption. Existing accepted stages and customer review decisions are preserved.
103
+ The command asks before uploading structured discovery proposals. `--send` provides explicit non-interactive approval. It coordinates features, pricing extraction, and plan branches, reports progress, and waits for the Stripe catalog. Run `saasfunnels setup run --resume` after an interruption. Existing accepted stages and customer review decisions are preserved. Coverage questions include a SHA-256 fingerprint of their affected files so changed application behavior can reopen an answer while unrelated file changes keep it. Only the fingerprint and bounded paths are uploaded, never source contents.
104
104
 
105
105
  The scanner reads supported application files one at a time without the old 500-file, 2 MB repository, or 200-candidate cutoffs. Generated code, tests, and symlinks are excluded. Files over 2 MB and uploads over the server limit stop with a specific recovery message.
106
106
 
package/dist/library.js CHANGED
@@ -235858,7 +235858,7 @@ function validateFeatureCatalogManifest(value) {
235858
235858
 
235859
235859
  // src/identity.ts
235860
235860
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
235861
- var SAASFUNNELS_CLI_VERSION = "0.4.0";
235861
+ var SAASFUNNELS_CLI_VERSION = "0.4.2";
235862
235862
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
235863
235863
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
235864
235864
  var SAASFUNNELS_ENV = Object.freeze({
@@ -233702,6 +233702,38 @@ function declarations(source, featuresOnly = false) {
233702
233702
  true,
233703
233703
  import_typescript2.default.ScriptKind.TSX
233704
233704
  );
233705
+ const validatorBindings = /* @__PURE__ */ new Map();
233706
+ for (const statement of file2.statements) {
233707
+ if (!import_typescript2.default.isImportDeclaration(statement) || !import_typescript2.default.isStringLiteral(statement.moduleSpecifier) || !/^(?:zod(?:\/.*)?|yup|joi|valibot|superstruct)$/.test(
233708
+ statement.moduleSpecifier.text
233709
+ ))
233710
+ continue;
233711
+ const clause = statement.importClause;
233712
+ if (clause?.name) validatorBindings.set(clause.name.text, "namespace");
233713
+ const bindings = clause?.namedBindings;
233714
+ if (bindings && import_typescript2.default.isNamespaceImport(bindings))
233715
+ validatorBindings.set(bindings.name.text, "namespace");
233716
+ if (bindings && import_typescript2.default.isNamedImports(bindings))
233717
+ for (const item of bindings.elements)
233718
+ validatorBindings.set(
233719
+ item.name.text,
233720
+ item.propertyName?.text ?? item.name.text
233721
+ );
233722
+ }
233723
+ const schemaConstructors = /^(?:object|strictObject|looseObject|record|array|tuple|union|intersection|discriminatedUnion|lazy|recursive|struct)$/;
233724
+ const schemaModifiers = /^(?:strict|passthrough|strip|optional|nullable|nullish|default|describe|meta|refine|superRefine|transform|pipe|readonly|brand|extend|merge|pick|omit|partial|required|min|max)$/;
233725
+ function validationSchema(node) {
233726
+ if (import_typescript2.default.isAsExpression(node) || import_typescript2.default.isSatisfiesExpression(node) || import_typescript2.default.isParenthesizedExpression(node))
233727
+ return validationSchema(node.expression);
233728
+ if (!import_typescript2.default.isCallExpression(node)) return false;
233729
+ const callee = node.expression;
233730
+ if (import_typescript2.default.isIdentifier(callee))
233731
+ return schemaConstructors.test(validatorBindings.get(callee.text) ?? "");
233732
+ if (!import_typescript2.default.isPropertyAccessExpression(callee)) return false;
233733
+ if (import_typescript2.default.isIdentifier(callee.expression) && validatorBindings.has(callee.expression.text) && schemaConstructors.test(callee.name.text))
233734
+ return true;
233735
+ return schemaModifiers.test(callee.name.text) && validationSchema(callee.expression);
233736
+ }
233705
233737
  const values = /* @__PURE__ */ new Map();
233706
233738
  const roots = [];
233707
233739
  const planName = /^(plans?|pricing|tiers?|billingCatalog|planCatalog|subscriptionPlans)$/i;
@@ -233718,6 +233750,7 @@ function declarations(source, featuresOnly = false) {
233718
233750
  for (const declaration of statement.declarationList.declarations) {
233719
233751
  if (import_typescript2.default.isIdentifier(declaration.name) && declaration.initializer && isProductNode(declaration)) {
233720
233752
  values.set(declaration.name.text, declaration.initializer);
233753
+ if (validationSchema(declaration.initializer)) continue;
233721
233754
  if (planName.test(declaration.name.text) || containsAccess(declaration.initializer) && containsAccess(
233722
233755
  declaration.initializer,
233723
233756
  /^(prices|stripePriceId)$/
@@ -233725,7 +233758,7 @@ function declarations(source, featuresOnly = false) {
233725
233758
  roots.push(declaration.initializer);
233726
233759
  }
233727
233760
  }
233728
- if (import_typescript2.default.isExportAssignment(statement) && containsAccess(statement.expression))
233761
+ if (import_typescript2.default.isExportAssignment(statement) && containsAccess(statement.expression) && !validationSchema(statement.expression))
233729
233762
  roots.push(statement.expression);
233730
233763
  }
233731
233764
  function read(node, depth = 0, locals = /* @__PURE__ */ new Map()) {
@@ -233928,7 +233961,7 @@ function extractSetupPricing(source, kind, catalogPrices = [], options = {}) {
233928
233961
 
233929
233962
  // src/identity.ts
233930
233963
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
233931
- var SAASFUNNELS_CLI_VERSION = "0.4.0";
233964
+ var SAASFUNNELS_CLI_VERSION = "0.4.2";
233932
233965
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
233933
233966
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
233934
233967
  var SAASFUNNELS_ENV = Object.freeze({
@@ -233959,6 +233992,9 @@ function saasFunnelsPublicText(value) {
233959
233992
  ).replaceAll("prevenueFeatureKeys", "saasFunnelsFeatureKeys").replaceAll("PrevenueFeatureKey", "SaaSFunnelsFeatureKey").replaceAll("SaaSFunnels", SAASFUNNELS_PRODUCT_NAME);
233960
233993
  }
233961
233994
 
233995
+ // src/discovery-coverage.ts
233996
+ import { createHash as createHash3 } from "node:crypto";
233997
+
233962
233998
  // src/plan-analysis.ts
233963
233999
  var import_typescript3 = __toESM(require_typescript(), 1);
233964
234000
  function effect(node) {
@@ -237287,9 +237323,10 @@ async function discoverCoverageLimitations(cwd2) {
237287
237323
  );
237288
237324
  const sources = /* @__PURE__ */ new Map();
237289
237325
  const results = /* @__PURE__ */ new Map();
237326
+ const evidence = /* @__PURE__ */ new Map();
237290
237327
  const record3 = (code, file2) => {
237291
237328
  const paths = results.get(code) ?? /* @__PURE__ */ new Set();
237292
- if (paths.size < 12) paths.add(file2);
237329
+ paths.add(file2);
237293
237330
  results.set(code, paths);
237294
237331
  };
237295
237332
  for (const path of files) {
@@ -237302,6 +237339,14 @@ async function discoverCoverageLimitations(cwd2) {
237302
237339
  if (/\.(?:py|rb|go|rs|java|kt|cs|php|ex|exs|scala|swift|vue|svelte)$/.test(
237303
237340
  path
237304
237341
  )) {
237342
+ if (stat.size > 2e6)
237343
+ throw new Error(
237344
+ "Unsupported source exceeds the 2 MB scan limit. Narrow the discovery scope before retrying."
237345
+ );
237346
+ evidence.set(
237347
+ path,
237348
+ createHash3("sha256").update(await readFile3(join2(cwd2, path))).digest("hex")
237349
+ );
237305
237350
  record3("unsupported_language", path);
237306
237351
  continue;
237307
237352
  }
@@ -237309,6 +237354,7 @@ async function discoverCoverageLimitations(cwd2) {
237309
237354
  continue;
237310
237355
  const source = await readFile3(join2(cwd2, path), "utf8");
237311
237356
  sources.set(path, source);
237357
+ evidence.set(path, createHash3("sha256").update(source).digest("hex"));
237312
237358
  const file2 = parseApplicationSource(path, source);
237313
237359
  const visit = (node) => {
237314
237360
  if (!isProductNode(node)) return;
@@ -237351,11 +237397,20 @@ async function discoverCoverageLimitations(cwd2) {
237351
237397
  ))
237352
237398
  record3("dynamic_access", path);
237353
237399
  }
237354
- return [...results].map(([code, paths]) => ({ code, files: [...paths] }));
237400
+ return [...results].map(([code, paths]) => ({
237401
+ code,
237402
+ files: [...paths].sort().slice(0, 12),
237403
+ // Include all affected files in the receipt, even when the UI path list is capped.
237404
+ sourceFingerprint: createHash3("sha256").update(
237405
+ JSON.stringify(
237406
+ [...paths].sort().map((path) => [path, evidence.get(path)])
237407
+ )
237408
+ ).digest("hex")
237409
+ }));
237355
237410
  }
237356
237411
 
237357
237412
  // src/setup-run.ts
237358
- import { createHash as createHash4 } from "node:crypto";
237413
+ import { createHash as createHash5 } from "node:crypto";
237359
237414
  import { execFile as execFile4 } from "node:child_process";
237360
237415
  import { promisify as promisify4 } from "node:util";
237361
237416
  import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
@@ -237540,7 +237595,7 @@ function planMappingRequestKey(input) {
237540
237595
  }
237541
237596
 
237542
237597
  // src/plan-branches.ts
237543
- import { createHash as createHash3 } from "node:crypto";
237598
+ import { createHash as createHash4 } from "node:crypto";
237544
237599
  import { readdir as readdir3, readFile as readFile5, lstat as lstat4 } from "node:fs/promises";
237545
237600
  import { extname as extname3, join as join4, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
237546
237601
  var maxPlanBranchFileCharacters = 2e6;
@@ -237562,7 +237617,7 @@ function clusterPlanBranches(branches) {
237562
237617
  const clusters = /* @__PURE__ */ new Map();
237563
237618
  for (const branch of branches) {
237564
237619
  const fullLocation = `${branch.repositoryPath}::${branch.symbol}`;
237565
- const location = fullLocation.length <= 400 ? fullLocation : `${fullLocation.slice(0, 367)}~${createHash3("sha256").update(fullLocation).digest("hex").slice(0, 32)}`;
237620
+ const location = fullLocation.length <= 400 ? fullLocation : `${fullLocation.slice(0, 367)}~${createHash4("sha256").update(fullLocation).digest("hex").slice(0, 32)}`;
237566
237621
  const key3 = `${branch.shape}|${location}`;
237567
237622
  const existing = clusters.get(key3);
237568
237623
  if (existing) existing.push(branch);
@@ -237652,7 +237707,7 @@ async function discoverPlanBranches(input) {
237652
237707
 
237653
237708
  // src/setup-run.ts
237654
237709
  var exec4 = promisify4(execFile4);
237655
- var hash2 = (text) => createHash4("sha256").update(text).digest("hex");
237710
+ var hash2 = (text) => createHash5("sha256").update(text).digest("hex");
237656
237711
  var contextSchema = external_exports.object({
237657
237712
  contractVersion: external_exports.literal(3),
237658
237713
  minimumCliVersion: external_exports.string(),
@@ -237704,6 +237759,10 @@ async function runGuidedSetup(options) {
237704
237759
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
237705
237760
  signal: AbortSignal.timeout(3e4)
237706
237761
  });
237762
+ if (response.status === 426)
237763
+ throw new Error(
237764
+ "Setup request failed: update the installer with npx --yes saasfunnels@latest setup run"
237765
+ );
237707
237766
  if (!response.ok)
237708
237767
  throw new Error(
237709
237768
  `Setup request failed (${response.status}). Check installation access and retry.`
@@ -237782,15 +237841,15 @@ async function runGuidedSetup(options) {
237782
237841
  }
237783
237842
  })
237784
237843
  );
237785
- const revision = `${head}:${hash2(JSON.stringify(["configuration-v3", diff, sourceHashes, reviewedInputs]))}`;
237844
+ const revision = `${head}:${hash2(JSON.stringify(["configuration-v3", SAASFUNNELS_CLI_VERSION, diff, sourceHashes, reviewedInputs]))}`;
237786
237845
  const remote = (await exec4("git", ["remote", "get-url", "origin"], { cwd: options.cwd })).stdout.trim();
237787
237846
  const repositoryKey = safeRepositoryKey(remote);
237788
237847
  const statePath = join5(options.cwd, ".saasfunnels", "setup-run.json");
237789
237848
  if (options.resume) {
237790
237849
  const saved = JSON.parse(await readFile6(statePath, "utf8"));
237791
- if (saved.generation !== context.generation || saved.repositoryKey !== repositoryKey || saved.revision !== revision)
237850
+ if (saved.cliVersion !== SAASFUNNELS_CLI_VERSION || saved.generation !== context.generation || saved.repositoryKey !== repositoryKey || saved.revision !== revision)
237792
237851
  throw new Error(
237793
- "The app connection or revision changed. Run setup again without --resume."
237852
+ "The app connection, installer, or source changed. Start a fresh scan: npx --yes saasfunnels@latest setup run"
237794
237853
  );
237795
237854
  }
237796
237855
  const approved = options.send || options.prompt && /^y(es)?$/i.test(
@@ -237815,6 +237874,7 @@ async function runGuidedSetup(options) {
237815
237874
  statePath,
237816
237875
  JSON.stringify(
237817
237876
  {
237877
+ cliVersion: SAASFUNNELS_CLI_VERSION,
237818
237878
  generation: context.generation,
237819
237879
  repositoryKey,
237820
237880
  revision,
@@ -1,5 +1,5 @@
1
1
  export declare const SAASFUNNELS_CLI_NAME = "saasfunnels";
2
- export declare const SAASFUNNELS_CLI_VERSION = "0.4.0";
2
+ export declare const SAASFUNNELS_CLI_VERSION = "0.4.2";
3
3
  export declare const SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
4
4
  export declare const SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
5
5
  export declare const SAASFUNNELS_ENV: Readonly<{
@@ -77,7 +77,7 @@ export type SaaSFunnelsMcpOptions = {
77
77
  output?: Pick<NodeJS.WritableStream, "write">;
78
78
  };
79
79
  declare const mcpProtocolVersion = "2025-06-18";
80
- declare const mcpServerVersion = "0.4.0";
80
+ declare const mcpServerVersion = "0.4.2";
81
81
  export declare function saasFunnelsMcpResources(): McpResourceDefinition[];
82
82
  export declare function readSaaSFunnelsMcpResource(uri: string, options?: SaaSFunnelsMcpOptions): string;
83
83
  export declare function saasFunnelsMcpToolDefinitions({ allowFunnelTests, allowFunnelWrites, allowTestWrites, includeHostedAccountTools, }?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasfunnels",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "description": "SaaSFunnels CLI and local stdio MCP server",
6
6
  "author": "SaaSFunnels",