@qulib/core 0.2.1 → 0.2.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
@@ -55,6 +55,20 @@ qulib analyze --url https://app.example.com --auth-storage-state ./qulib-storage
55
55
 
56
56
  The storage state is just a JSON file of cookies and localStorage — keep it private, treat it like a credential.
57
57
 
58
+ ### Multi-path auth exploration (`explore-auth`)
59
+
60
+ For unfamiliar apps (especially enterprise SSO with several buttons), run **`qulib explore-auth --url <url>`** before `analyze`. The JSON lists every detected path (built-in OAuth names like Google/Clever, **heuristic** unknown buttons such as tenant-specific SSO labels, password forms, and magic-link copy) plus **`suggestedAgentBehavior`** for the agent.
61
+
62
+ Unknown SSO buttons include **`unrecognizedButtons`** with a hint. Teach this machine to recognize a label next time:
63
+
64
+ ```bash
65
+ qulib auth providers add --id scholastic-sync --label "Scholastic Sync" --pattern "scholastic sync"
66
+ qulib auth providers list
67
+ qulib auth providers remove --id scholastic-sync
68
+ ```
69
+
70
+ Patterns live in **`~/.qulib/providers.json`** (per user, not in the repo). Built-in public platforms stay in qulib’s curated list; tenant-specific names are never shipped as built-ins.
71
+
58
72
  ### Auth detection
59
73
 
60
74
  To check what auth pattern a site uses before configuring anything:
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { z } from 'zod';
6
6
  import { HarnessConfigSchema } from '../schemas/config.schema.js';
7
7
  import { analyzeApp } from '../analyze.js';
8
8
  import { detectAuth } from '../tools/auth-detector.js';
9
+ import { exploreAuth } from '../tools/auth-explorer.js';
9
10
  const program = new Command();
10
11
  const AnalyzeUrlSchema = z.string().url();
11
12
  const FormLoginCliSchema = z.object({
@@ -166,6 +167,15 @@ program
166
167
  submitSelector: options.submitSelector,
167
168
  });
168
169
  });
170
+ program
171
+ .command('explore-auth')
172
+ .description('Explore all sign-in paths (OAuth, forms, magic link) for agent-driven setup before analyze')
173
+ .requiredOption('--url <url>', 'URL of the app or login page')
174
+ .option('--timeout <ms>', 'Navigation timeout in ms', '20000')
175
+ .action(async (options) => {
176
+ const result = await exploreAuth(options.url, parseInt(options.timeout, 10));
177
+ console.log(JSON.stringify(result, null, 2));
178
+ });
169
179
  program
170
180
  .command('detect-auth')
171
181
  .description('Detect the authentication pattern used by a deployed web app')
@@ -176,6 +186,43 @@ program
176
186
  console.log(JSON.stringify(result, null, 2));
177
187
  });
178
188
  const authCmd = program.command('auth').description('Authentication helpers for scans');
189
+ const providersCmd = authCmd
190
+ .command('providers')
191
+ .description('User-local OAuth/SSO button patterns (~/.qulib/providers.json)');
192
+ providersCmd
193
+ .command('list')
194
+ .description('List user-local providers registered on this machine')
195
+ .action(async () => {
196
+ const { listUserProviders } = await import('../tools/user-providers.js');
197
+ const providers = listUserProviders();
198
+ console.log(JSON.stringify(providers, null, 2));
199
+ });
200
+ providersCmd
201
+ .command('add')
202
+ .description('Register a custom provider pattern (case-insensitive regex source)')
203
+ .requiredOption('--id <id>', 'Stable id (kebab-case), e.g. scholastic-sync')
204
+ .requiredOption('--label <label>', 'Human-readable label')
205
+ .requiredOption('--pattern <regex>', 'Regex source, e.g. scholastic sync')
206
+ .action(async (opts) => {
207
+ try {
208
+ new RegExp(opts.pattern, 'i');
209
+ }
210
+ catch {
211
+ throw new Error(`Invalid regex pattern: ${opts.pattern}`);
212
+ }
213
+ const { addUserProvider } = await import('../tools/user-providers.js');
214
+ addUserProvider({ id: opts.id, label: opts.label, pattern: opts.pattern });
215
+ console.log(`[qulib] Added provider "${opts.label}" (id: ${opts.id}) to ~/.qulib/providers.json`);
216
+ });
217
+ providersCmd
218
+ .command('remove')
219
+ .description('Remove a user-local provider by id')
220
+ .requiredOption('--id <id>', 'Provider id to remove')
221
+ .action(async (opts) => {
222
+ const { removeUserProvider } = await import('../tools/user-providers.js');
223
+ const removed = removeUserProvider(opts.id);
224
+ console.log(removed ? `[qulib] Removed "${opts.id}"` : `[qulib] No provider with id "${opts.id}" found`);
225
+ });
179
226
  authCmd
180
227
  .command('init')
181
228
  .description('Open a browser, let the user log in manually, save the storage state to a file for reuse')
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { analyzeApp } from './analyze.js';
2
2
  export { detectAuth } from './tools/auth-detector.js';
3
+ export { exploreAuth } from './tools/auth-explorer.js';
4
+ export { addUserProvider, removeUserProvider, listUserProviders } from './tools/user-providers.js';
3
5
  export type { AnalyzeOptions, AnalyzeResult } from './analyze.js';
4
- export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, DetectedAuth, } from './schemas/index.js';
6
+ export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, DetectedAuth, AuthExploration, AuthPath, AuthPathRequirements, } from './schemas/index.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,GACb,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnG,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,oBAAoB,GACrB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export { analyzeApp } from './analyze.js';
2
2
  export { detectAuth } from './tools/auth-detector.js';
3
+ export { exploreAuth } from './tools/auth-explorer.js';
4
+ export { addUserProvider, removeUserProvider, listUserProviders } from './tools/user-providers.js';
@@ -384,5 +384,362 @@ export declare const DetectedAuthSchema: z.ZodObject<{
384
384
  recommendation: string;
385
385
  }>;
386
386
  export type DetectedAuth = z.infer<typeof DetectedAuthSchema>;
387
+ export declare const AuthPathRequirementsSchema: z.ZodDiscriminatedUnion<"method", [z.ZodObject<{
388
+ method: z.ZodLiteral<"storage-state">;
389
+ instruction: z.ZodString;
390
+ }, "strip", z.ZodTypeAny, {
391
+ method: "storage-state";
392
+ instruction: string;
393
+ }, {
394
+ method: "storage-state";
395
+ instruction: string;
396
+ }>, z.ZodObject<{
397
+ method: z.ZodLiteral<"credentials">;
398
+ fields: z.ZodArray<z.ZodObject<{
399
+ name: z.ZodString;
400
+ label: z.ZodString;
401
+ type: z.ZodEnum<["text", "password", "email", "select", "checkbox"]>;
402
+ observedOptions: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
403
+ }, "strip", z.ZodTypeAny, {
404
+ type: "password" | "text" | "email" | "select" | "checkbox";
405
+ name: string;
406
+ label: string;
407
+ observedOptions: string[];
408
+ }, {
409
+ type: "password" | "text" | "email" | "select" | "checkbox";
410
+ name: string;
411
+ label: string;
412
+ observedOptions?: string[] | undefined;
413
+ }>, "many">;
414
+ }, "strip", z.ZodTypeAny, {
415
+ method: "credentials";
416
+ fields: {
417
+ type: "password" | "text" | "email" | "select" | "checkbox";
418
+ name: string;
419
+ label: string;
420
+ observedOptions: string[];
421
+ }[];
422
+ }, {
423
+ method: "credentials";
424
+ fields: {
425
+ type: "password" | "text" | "email" | "select" | "checkbox";
426
+ name: string;
427
+ label: string;
428
+ observedOptions?: string[] | undefined;
429
+ }[];
430
+ }>, z.ZodObject<{
431
+ method: z.ZodLiteral<"unknown">;
432
+ instruction: z.ZodString;
433
+ }, "strip", z.ZodTypeAny, {
434
+ method: "unknown";
435
+ instruction: string;
436
+ }, {
437
+ method: "unknown";
438
+ instruction: string;
439
+ }>]>;
440
+ export declare const AuthPathSchema: z.ZodObject<{
441
+ id: z.ZodString;
442
+ label: z.ZodString;
443
+ type: z.ZodEnum<["oauth", "oauth-unknown", "form-login", "form-multi", "magic-link", "unknown"]>;
444
+ provider: z.ZodNullable<z.ZodString>;
445
+ source: z.ZodEnum<["built-in", "user-local", "heuristic"]>;
446
+ automatable: z.ZodBoolean;
447
+ confidence: z.ZodEnum<["high", "medium", "low"]>;
448
+ requirements: z.ZodDiscriminatedUnion<"method", [z.ZodObject<{
449
+ method: z.ZodLiteral<"storage-state">;
450
+ instruction: z.ZodString;
451
+ }, "strip", z.ZodTypeAny, {
452
+ method: "storage-state";
453
+ instruction: string;
454
+ }, {
455
+ method: "storage-state";
456
+ instruction: string;
457
+ }>, z.ZodObject<{
458
+ method: z.ZodLiteral<"credentials">;
459
+ fields: z.ZodArray<z.ZodObject<{
460
+ name: z.ZodString;
461
+ label: z.ZodString;
462
+ type: z.ZodEnum<["text", "password", "email", "select", "checkbox"]>;
463
+ observedOptions: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
464
+ }, "strip", z.ZodTypeAny, {
465
+ type: "password" | "text" | "email" | "select" | "checkbox";
466
+ name: string;
467
+ label: string;
468
+ observedOptions: string[];
469
+ }, {
470
+ type: "password" | "text" | "email" | "select" | "checkbox";
471
+ name: string;
472
+ label: string;
473
+ observedOptions?: string[] | undefined;
474
+ }>, "many">;
475
+ }, "strip", z.ZodTypeAny, {
476
+ method: "credentials";
477
+ fields: {
478
+ type: "password" | "text" | "email" | "select" | "checkbox";
479
+ name: string;
480
+ label: string;
481
+ observedOptions: string[];
482
+ }[];
483
+ }, {
484
+ method: "credentials";
485
+ fields: {
486
+ type: "password" | "text" | "email" | "select" | "checkbox";
487
+ name: string;
488
+ label: string;
489
+ observedOptions?: string[] | undefined;
490
+ }[];
491
+ }>, z.ZodObject<{
492
+ method: z.ZodLiteral<"unknown">;
493
+ instruction: z.ZodString;
494
+ }, "strip", z.ZodTypeAny, {
495
+ method: "unknown";
496
+ instruction: string;
497
+ }, {
498
+ method: "unknown";
499
+ instruction: string;
500
+ }>]>;
501
+ }, "strip", z.ZodTypeAny, {
502
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "oauth-unknown" | "form-multi";
503
+ provider: string | null;
504
+ label: string;
505
+ id: string;
506
+ source: "built-in" | "user-local" | "heuristic";
507
+ automatable: boolean;
508
+ confidence: "high" | "medium" | "low";
509
+ requirements: {
510
+ method: "storage-state";
511
+ instruction: string;
512
+ } | {
513
+ method: "credentials";
514
+ fields: {
515
+ type: "password" | "text" | "email" | "select" | "checkbox";
516
+ name: string;
517
+ label: string;
518
+ observedOptions: string[];
519
+ }[];
520
+ } | {
521
+ method: "unknown";
522
+ instruction: string;
523
+ };
524
+ }, {
525
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "oauth-unknown" | "form-multi";
526
+ provider: string | null;
527
+ label: string;
528
+ id: string;
529
+ source: "built-in" | "user-local" | "heuristic";
530
+ automatable: boolean;
531
+ confidence: "high" | "medium" | "low";
532
+ requirements: {
533
+ method: "storage-state";
534
+ instruction: string;
535
+ } | {
536
+ method: "credentials";
537
+ fields: {
538
+ type: "password" | "text" | "email" | "select" | "checkbox";
539
+ name: string;
540
+ label: string;
541
+ observedOptions?: string[] | undefined;
542
+ }[];
543
+ } | {
544
+ method: "unknown";
545
+ instruction: string;
546
+ };
547
+ }>;
548
+ export declare const AuthExplorationSchema: z.ZodObject<{
549
+ url: z.ZodString;
550
+ authRequired: z.ZodBoolean;
551
+ authScope: z.ZodEnum<["site-wide", "section-only", "optional", "none"]>;
552
+ authPaths: z.ZodArray<z.ZodObject<{
553
+ id: z.ZodString;
554
+ label: z.ZodString;
555
+ type: z.ZodEnum<["oauth", "oauth-unknown", "form-login", "form-multi", "magic-link", "unknown"]>;
556
+ provider: z.ZodNullable<z.ZodString>;
557
+ source: z.ZodEnum<["built-in", "user-local", "heuristic"]>;
558
+ automatable: z.ZodBoolean;
559
+ confidence: z.ZodEnum<["high", "medium", "low"]>;
560
+ requirements: z.ZodDiscriminatedUnion<"method", [z.ZodObject<{
561
+ method: z.ZodLiteral<"storage-state">;
562
+ instruction: z.ZodString;
563
+ }, "strip", z.ZodTypeAny, {
564
+ method: "storage-state";
565
+ instruction: string;
566
+ }, {
567
+ method: "storage-state";
568
+ instruction: string;
569
+ }>, z.ZodObject<{
570
+ method: z.ZodLiteral<"credentials">;
571
+ fields: z.ZodArray<z.ZodObject<{
572
+ name: z.ZodString;
573
+ label: z.ZodString;
574
+ type: z.ZodEnum<["text", "password", "email", "select", "checkbox"]>;
575
+ observedOptions: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
576
+ }, "strip", z.ZodTypeAny, {
577
+ type: "password" | "text" | "email" | "select" | "checkbox";
578
+ name: string;
579
+ label: string;
580
+ observedOptions: string[];
581
+ }, {
582
+ type: "password" | "text" | "email" | "select" | "checkbox";
583
+ name: string;
584
+ label: string;
585
+ observedOptions?: string[] | undefined;
586
+ }>, "many">;
587
+ }, "strip", z.ZodTypeAny, {
588
+ method: "credentials";
589
+ fields: {
590
+ type: "password" | "text" | "email" | "select" | "checkbox";
591
+ name: string;
592
+ label: string;
593
+ observedOptions: string[];
594
+ }[];
595
+ }, {
596
+ method: "credentials";
597
+ fields: {
598
+ type: "password" | "text" | "email" | "select" | "checkbox";
599
+ name: string;
600
+ label: string;
601
+ observedOptions?: string[] | undefined;
602
+ }[];
603
+ }>, z.ZodObject<{
604
+ method: z.ZodLiteral<"unknown">;
605
+ instruction: z.ZodString;
606
+ }, "strip", z.ZodTypeAny, {
607
+ method: "unknown";
608
+ instruction: string;
609
+ }, {
610
+ method: "unknown";
611
+ instruction: string;
612
+ }>]>;
613
+ }, "strip", z.ZodTypeAny, {
614
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "oauth-unknown" | "form-multi";
615
+ provider: string | null;
616
+ label: string;
617
+ id: string;
618
+ source: "built-in" | "user-local" | "heuristic";
619
+ automatable: boolean;
620
+ confidence: "high" | "medium" | "low";
621
+ requirements: {
622
+ method: "storage-state";
623
+ instruction: string;
624
+ } | {
625
+ method: "credentials";
626
+ fields: {
627
+ type: "password" | "text" | "email" | "select" | "checkbox";
628
+ name: string;
629
+ label: string;
630
+ observedOptions: string[];
631
+ }[];
632
+ } | {
633
+ method: "unknown";
634
+ instruction: string;
635
+ };
636
+ }, {
637
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "oauth-unknown" | "form-multi";
638
+ provider: string | null;
639
+ label: string;
640
+ id: string;
641
+ source: "built-in" | "user-local" | "heuristic";
642
+ automatable: boolean;
643
+ confidence: "high" | "medium" | "low";
644
+ requirements: {
645
+ method: "storage-state";
646
+ instruction: string;
647
+ } | {
648
+ method: "credentials";
649
+ fields: {
650
+ type: "password" | "text" | "email" | "select" | "checkbox";
651
+ name: string;
652
+ label: string;
653
+ observedOptions?: string[] | undefined;
654
+ }[];
655
+ } | {
656
+ method: "unknown";
657
+ instruction: string;
658
+ };
659
+ }>, "many">;
660
+ observedAt: z.ZodString;
661
+ suggestedAgentBehavior: z.ZodString;
662
+ unrecognizedButtons: z.ZodDefault<z.ZodArray<z.ZodObject<{
663
+ label: z.ZodString;
664
+ hint: z.ZodString;
665
+ }, "strip", z.ZodTypeAny, {
666
+ label: string;
667
+ hint: string;
668
+ }, {
669
+ label: string;
670
+ hint: string;
671
+ }>, "many">>;
672
+ }, "strip", z.ZodTypeAny, {
673
+ url: string;
674
+ authRequired: boolean;
675
+ authScope: "none" | "site-wide" | "section-only" | "optional";
676
+ authPaths: {
677
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "oauth-unknown" | "form-multi";
678
+ provider: string | null;
679
+ label: string;
680
+ id: string;
681
+ source: "built-in" | "user-local" | "heuristic";
682
+ automatable: boolean;
683
+ confidence: "high" | "medium" | "low";
684
+ requirements: {
685
+ method: "storage-state";
686
+ instruction: string;
687
+ } | {
688
+ method: "credentials";
689
+ fields: {
690
+ type: "password" | "text" | "email" | "select" | "checkbox";
691
+ name: string;
692
+ label: string;
693
+ observedOptions: string[];
694
+ }[];
695
+ } | {
696
+ method: "unknown";
697
+ instruction: string;
698
+ };
699
+ }[];
700
+ observedAt: string;
701
+ suggestedAgentBehavior: string;
702
+ unrecognizedButtons: {
703
+ label: string;
704
+ hint: string;
705
+ }[];
706
+ }, {
707
+ url: string;
708
+ authRequired: boolean;
709
+ authScope: "none" | "site-wide" | "section-only" | "optional";
710
+ authPaths: {
711
+ type: "unknown" | "form-login" | "oauth" | "magic-link" | "oauth-unknown" | "form-multi";
712
+ provider: string | null;
713
+ label: string;
714
+ id: string;
715
+ source: "built-in" | "user-local" | "heuristic";
716
+ automatable: boolean;
717
+ confidence: "high" | "medium" | "low";
718
+ requirements: {
719
+ method: "storage-state";
720
+ instruction: string;
721
+ } | {
722
+ method: "credentials";
723
+ fields: {
724
+ type: "password" | "text" | "email" | "select" | "checkbox";
725
+ name: string;
726
+ label: string;
727
+ observedOptions?: string[] | undefined;
728
+ }[];
729
+ } | {
730
+ method: "unknown";
731
+ instruction: string;
732
+ };
733
+ }[];
734
+ observedAt: string;
735
+ suggestedAgentBehavior: string;
736
+ unrecognizedButtons?: {
737
+ label: string;
738
+ hint: string;
739
+ }[] | undefined;
740
+ }>;
741
+ export type AuthPathRequirements = z.infer<typeof AuthPathRequirementsSchema>;
742
+ export type AuthPath = z.infer<typeof AuthPathSchema>;
743
+ export type AuthExploration = z.infer<typeof AuthExplorationSchema>;
387
744
  export {};
388
745
  //# sourceMappingURL=config.schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/config.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AACpD,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,mBAAmB,GAAG,KAAK,GAAG,eAAe,CAAC;AAEvG,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBvB,CAAC;AAEH,QAAA,MAAM,sBAAsB;;;;;;;;;EAG1B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA8E,CAAC;AAE5G,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC5E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"config.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/config.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AACpD,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,mBAAmB,GAAG,KAAK,GAAG,eAAe,CAAC;AAEvG,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBvB,CAAC;AAEH,QAAA,MAAM,sBAAsB;;;;;;;;;EAG1B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA8E,CAAC;AAE5G,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC5E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAcrC,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASzB,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAehC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
@@ -55,3 +55,40 @@ export const DetectedAuthSchema = z.object({
55
55
  })),
56
56
  recommendation: z.string(),
57
57
  });
58
+ export const AuthPathRequirementsSchema = z.discriminatedUnion('method', [
59
+ z.object({ method: z.literal('storage-state'), instruction: z.string() }),
60
+ z.object({
61
+ method: z.literal('credentials'),
62
+ fields: z.array(z.object({
63
+ name: z.string(),
64
+ label: z.string(),
65
+ type: z.enum(['text', 'password', 'email', 'select', 'checkbox']),
66
+ observedOptions: z.array(z.string()).default([]),
67
+ })),
68
+ }),
69
+ z.object({ method: z.literal('unknown'), instruction: z.string() }),
70
+ ]);
71
+ export const AuthPathSchema = z.object({
72
+ id: z.string(),
73
+ label: z.string(),
74
+ type: z.enum(['oauth', 'oauth-unknown', 'form-login', 'form-multi', 'magic-link', 'unknown']),
75
+ provider: z.string().nullable(),
76
+ source: z.enum(['built-in', 'user-local', 'heuristic']),
77
+ automatable: z.boolean(),
78
+ confidence: z.enum(['high', 'medium', 'low']),
79
+ requirements: AuthPathRequirementsSchema,
80
+ });
81
+ export const AuthExplorationSchema = z.object({
82
+ url: z.string(),
83
+ authRequired: z.boolean(),
84
+ authScope: z.enum(['site-wide', 'section-only', 'optional', 'none']),
85
+ authPaths: z.array(AuthPathSchema),
86
+ observedAt: z.string(),
87
+ suggestedAgentBehavior: z.string(),
88
+ unrecognizedButtons: z
89
+ .array(z.object({
90
+ label: z.string(),
91
+ hint: z.string(),
92
+ }))
93
+ .default([]),
94
+ });
@@ -23,13 +23,13 @@ export declare const FrameworkRecommendationSchema: z.ZodObject<{
23
23
  reason: z.ZodString;
24
24
  confidence: z.ZodEnum<["high", "medium", "low"]>;
25
25
  }, "strip", z.ZodTypeAny, {
26
+ confidence: "high" | "medium" | "low";
26
27
  reason: string;
27
28
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
28
- confidence: "high" | "medium" | "low";
29
29
  }, {
30
+ confidence: "high" | "medium" | "low";
30
31
  reason: string;
31
32
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
32
- confidence: "high" | "medium" | "low";
33
33
  }>;
34
34
  export declare const TestStepSchema: z.ZodObject<{
35
35
  action: z.ZodEnum<["navigate", "click", "type", "assert-visible", "assert-hidden", "assert-text", "assert-disabled", "assert-count", "wait", "api-call"]>;
@@ -75,13 +75,13 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
75
75
  reason: z.ZodString;
76
76
  confidence: z.ZodEnum<["high", "medium", "low"]>;
77
77
  }, "strip", z.ZodTypeAny, {
78
+ confidence: "high" | "medium" | "low";
78
79
  reason: string;
79
80
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
80
- confidence: "high" | "medium" | "low";
81
81
  }, {
82
+ confidence: "high" | "medium" | "low";
82
83
  reason: string;
83
84
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
84
- confidence: "high" | "medium" | "low";
85
85
  }>, "many">;
86
86
  sourceGapIds: z.ZodArray<z.ZodString, "many">;
87
87
  }, "strip", z.ZodTypeAny, {
@@ -97,9 +97,9 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
97
97
  }[];
98
98
  tags: string[];
99
99
  recommendations: {
100
+ confidence: "high" | "medium" | "low";
100
101
  reason: string;
101
102
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
102
- confidence: "high" | "medium" | "low";
103
103
  }[];
104
104
  sourceGapIds: string[];
105
105
  targetComponent?: string | undefined;
@@ -116,9 +116,9 @@ export declare const NeutralScenarioSchema: z.ZodObject<{
116
116
  }[];
117
117
  tags: string[];
118
118
  recommendations: {
119
+ confidence: "high" | "medium" | "low";
119
120
  reason: string;
120
121
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
121
- confidence: "high" | "medium" | "low";
122
122
  }[];
123
123
  sourceGapIds: string[];
124
124
  targetComponent?: string | undefined;
@@ -132,17 +132,17 @@ export declare const GeneratedTestSchema: z.ZodObject<{
132
132
  outputPath: z.ZodString;
133
133
  }, "strip", z.ZodTypeAny, {
134
134
  code: string;
135
+ source: "llm" | "template";
135
136
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
136
137
  scenarioId: string;
137
138
  filename: string;
138
- source: "llm" | "template";
139
139
  outputPath: string;
140
140
  }, {
141
141
  code: string;
142
+ source: "llm" | "template";
142
143
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
143
144
  scenarioId: string;
144
145
  filename: string;
145
- source: "llm" | "template";
146
146
  outputPath: string;
147
147
  }>;
148
148
  export declare const GapAnalysisSchema: z.ZodObject<{
@@ -199,13 +199,13 @@ export declare const GapAnalysisSchema: z.ZodObject<{
199
199
  reason: z.ZodString;
200
200
  confidence: z.ZodEnum<["high", "medium", "low"]>;
201
201
  }, "strip", z.ZodTypeAny, {
202
+ confidence: "high" | "medium" | "low";
202
203
  reason: string;
203
204
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
204
- confidence: "high" | "medium" | "low";
205
205
  }, {
206
+ confidence: "high" | "medium" | "low";
206
207
  reason: string;
207
208
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
208
- confidence: "high" | "medium" | "low";
209
209
  }>, "many">;
210
210
  sourceGapIds: z.ZodArray<z.ZodString, "many">;
211
211
  }, "strip", z.ZodTypeAny, {
@@ -221,9 +221,9 @@ export declare const GapAnalysisSchema: z.ZodObject<{
221
221
  }[];
222
222
  tags: string[];
223
223
  recommendations: {
224
+ confidence: "high" | "medium" | "low";
224
225
  reason: string;
225
226
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
226
- confidence: "high" | "medium" | "low";
227
227
  }[];
228
228
  sourceGapIds: string[];
229
229
  targetComponent?: string | undefined;
@@ -240,9 +240,9 @@ export declare const GapAnalysisSchema: z.ZodObject<{
240
240
  }[];
241
241
  tags: string[];
242
242
  recommendations: {
243
+ confidence: "high" | "medium" | "low";
243
244
  reason: string;
244
245
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
245
- confidence: "high" | "medium" | "low";
246
246
  }[];
247
247
  sourceGapIds: string[];
248
248
  targetComponent?: string | undefined;
@@ -256,17 +256,17 @@ export declare const GapAnalysisSchema: z.ZodObject<{
256
256
  outputPath: z.ZodString;
257
257
  }, "strip", z.ZodTypeAny, {
258
258
  code: string;
259
+ source: "llm" | "template";
259
260
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
260
261
  scenarioId: string;
261
262
  filename: string;
262
- source: "llm" | "template";
263
263
  outputPath: string;
264
264
  }, {
265
265
  code: string;
266
+ source: "llm" | "template";
266
267
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
267
268
  scenarioId: string;
268
269
  filename: string;
269
- source: "llm" | "template";
270
270
  outputPath: string;
271
271
  }>, "many">;
272
272
  }, "strip", z.ZodTypeAny, {
@@ -295,19 +295,19 @@ export declare const GapAnalysisSchema: z.ZodObject<{
295
295
  }[];
296
296
  tags: string[];
297
297
  recommendations: {
298
+ confidence: "high" | "medium" | "low";
298
299
  reason: string;
299
300
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
300
- confidence: "high" | "medium" | "low";
301
301
  }[];
302
302
  sourceGapIds: string[];
303
303
  targetComponent?: string | undefined;
304
304
  }[];
305
305
  generatedTests: {
306
306
  code: string;
307
+ source: "llm" | "template";
307
308
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
308
309
  scenarioId: string;
309
310
  filename: string;
310
- source: "llm" | "template";
311
311
  outputPath: string;
312
312
  }[];
313
313
  coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
@@ -337,19 +337,19 @@ export declare const GapAnalysisSchema: z.ZodObject<{
337
337
  }[];
338
338
  tags: string[];
339
339
  recommendations: {
340
+ confidence: "high" | "medium" | "low";
340
341
  reason: string;
341
342
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
342
- confidence: "high" | "medium" | "low";
343
343
  }[];
344
344
  sourceGapIds: string[];
345
345
  targetComponent?: string | undefined;
346
346
  }[];
347
347
  generatedTests: {
348
348
  code: string;
349
+ source: "llm" | "template";
349
350
  adapter: "playwright" | "cypress-e2e" | "cypress-component" | "api" | "accessibility";
350
351
  scenarioId: string;
351
352
  filename: string;
352
- source: "llm" | "template";
353
353
  outputPath: string;
354
354
  }[];
355
355
  coverageWarning?: "auth-required" | "budget-exceeded" | "low-coverage" | "navigation-failures" | undefined;
@@ -1,4 +1,4 @@
1
- export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, type DetectedAuth, } from './config.schema.js';
1
+ export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, AuthPathRequirementsSchema, AuthPathSchema, AuthExplorationSchema, type ExplorerType, type AdapterType, type FormLoginAuthConfig, type StorageStateAuthConfig, type AuthConfig, type HarnessConfig, type DetectedAuth, type AuthPathRequirements, type AuthPath, type AuthExploration, } from './config.schema.js';
2
2
  export { DecisionLogEntrySchema, type DecisionLogEntry, } from './decision-log.schema.js';
3
3
  export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchema, type RouteInventory, type Route, } from './route-inventory.schema.js';
4
4
  export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, type GapAnalysis, type Gap, type NeutralScenario, type GeneratedTest, type TestStep, type FrameworkRecommendation, } from './gap-analysis.schema.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,KAAK,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,GAAG,EACR,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EACb,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,KAAK,GACX,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,KAAK,WAAW,EAChB,KAAK,GAAG,EACR,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC"}
@@ -1,4 +1,4 @@
1
- export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, } from './config.schema.js';
1
+ export { HarnessConfigSchema, AuthConfigSchema, DetectedAuthSchema, AuthPathRequirementsSchema, AuthPathSchema, AuthExplorationSchema, } from './config.schema.js';
2
2
  export { DecisionLogEntrySchema, } from './decision-log.schema.js';
3
3
  export { RouteInventorySchema, RouteSchema, A11yViolationSchema, BrokenLinkSchema, } from './route-inventory.schema.js';
4
4
  export { GapAnalysisSchema, GapSchema, NeutralScenarioSchema, GeneratedTestSchema, TestStepSchema, FrameworkRecommendationSchema, } from './gap-analysis.schema.js';
@@ -5,12 +5,12 @@ export declare const RepoRouteSchema: z.ZodObject<{
5
5
  method: z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "unknown"]>;
6
6
  }, "strip", z.ZodTypeAny, {
7
7
  path: string;
8
- file: string;
9
8
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
9
+ file: string;
10
10
  }, {
11
11
  path: string;
12
- file: string;
13
12
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
13
+ file: string;
14
14
  }>;
15
15
  export declare const TestFileSchema: z.ZodObject<{
16
16
  file: z.ZodString;
@@ -62,12 +62,12 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
62
62
  method: z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "unknown"]>;
63
63
  }, "strip", z.ZodTypeAny, {
64
64
  path: string;
65
- file: string;
66
65
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
66
+ file: string;
67
67
  }, {
68
68
  path: string;
69
- file: string;
70
69
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
70
+ file: string;
71
71
  }>, "many">;
72
72
  testFiles: z.ZodArray<z.ZodObject<{
73
73
  file: z.ZodString;
@@ -115,8 +115,8 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
115
115
  scannedAt: string;
116
116
  routes: {
117
117
  path: string;
118
- file: string;
119
118
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
119
+ file: string;
120
120
  }[];
121
121
  repoPath: string;
122
122
  testFiles: {
@@ -139,8 +139,8 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
139
139
  scannedAt: string;
140
140
  routes: {
141
141
  path: string;
142
- file: string;
143
142
  method: "unknown" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
143
+ file: string;
144
144
  }[];
145
145
  repoPath: string;
146
146
  testFiles: {
@@ -0,0 +1,3 @@
1
+ import { type AuthExploration } from '../schemas/config.schema.js';
2
+ export declare function exploreAuth(url: string, timeoutMs?: number): Promise<AuthExploration>;
3
+ //# sourceMappingURL=auth-explorer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-explorer.d.ts","sourceRoot":"","sources":["../../src/tools/auth-explorer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,6BAA6B,CAAC;AA6MrC,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,CA2J1F"}
@@ -0,0 +1,324 @@
1
+ import { AuthExplorationSchema, } from '../schemas/config.schema.js';
2
+ import { launchBrowser } from './browser.js';
3
+ import { BUILT_IN_OAUTH_PROVIDERS } from './oauth-providers.js';
4
+ import { loadUserProviders } from './user-providers.js';
5
+ const MAGIC_LINK_PATTERNS = [
6
+ /email me a (sign[- ]?in )?link/i,
7
+ /sign in with email/i,
8
+ /passwordless/i,
9
+ /we'll send you a link/i,
10
+ ];
11
+ async function waitNetworkIdleBestEffort(page) {
12
+ try {
13
+ await page.waitForLoadState('networkidle', { timeout: 5000 });
14
+ }
15
+ catch {
16
+ // best-effort
17
+ }
18
+ }
19
+ function textLooksLikeOAuthIdpButton(text) {
20
+ const t = text.trim();
21
+ if (t.length === 0 || t.length > 120) {
22
+ return false;
23
+ }
24
+ return (/\b(sign in with|log in with|continue with|sign up with)\b/i.test(t) ||
25
+ /^(github|google|microsoft|apple)$/i.test(t));
26
+ }
27
+ function slugifyLabel(text) {
28
+ const s = text
29
+ .trim()
30
+ .toLowerCase()
31
+ .replace(/\s+/g, '-')
32
+ .replace(/[^a-z0-9-]/g, '')
33
+ .slice(0, 48);
34
+ return s.length > 0 ? s : 'unknown';
35
+ }
36
+ function onLoginishPage(url) {
37
+ return /login|sign[- ]?in|auth|sso|oauth/i.test(new URL(url).pathname + new URL(url).hostname);
38
+ }
39
+ function isHeuristicUnknownSso(text, loginish) {
40
+ const t = text.trim();
41
+ if (!loginish || t.length < 3 || t.length > 80) {
42
+ return false;
43
+ }
44
+ if (/^(submit|cancel|back|close|next|skip|help|faq)$/i.test(t)) {
45
+ return false;
46
+ }
47
+ if (/\b(sign in with|log in with|continue with)\b/i.test(t)) {
48
+ return true;
49
+ }
50
+ if (/\b(sync|sso|portal|workspace|federation)\b/i.test(t)) {
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ function storageRequirement() {
56
+ return {
57
+ method: 'storage-state',
58
+ instruction: 'OAuth and most SSO flows cannot be scripted. Run `qulib auth init --base-url <app-url>` on this machine, then pass the saved storage state JSON to `analyze` or MCP `analyze_app` as `auth: { type: "storage-state", path: "..." }`.',
59
+ };
60
+ }
61
+ async function collectVisibleControlTexts(page) {
62
+ return page.evaluate(() => {
63
+ const seen = new Set();
64
+ const out = [];
65
+ const nodes = document.querySelectorAll('button, a[href], [role="button"]');
66
+ for (const el of nodes) {
67
+ const t = (el.textContent ?? '').trim().replace(/\s+/g, ' ');
68
+ if (!t || t.length > 120) {
69
+ continue;
70
+ }
71
+ const style = window.getComputedStyle(el);
72
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
73
+ continue;
74
+ }
75
+ if (!seen.has(t)) {
76
+ seen.add(t);
77
+ out.push(t);
78
+ }
79
+ }
80
+ return out;
81
+ });
82
+ }
83
+ function buildAllProviders() {
84
+ const builtIn = BUILT_IN_OAUTH_PROVIDERS.map((p) => ({ ...p, source: 'built-in' }));
85
+ const user = loadUserProviders().map((p) => ({ ...p, source: 'user-local' }));
86
+ return [...builtIn, ...user];
87
+ }
88
+ function matchProvider(text, p) {
89
+ return p.patterns.some((re) => re.test(text));
90
+ }
91
+ function oauthConfidence(source, loginish) {
92
+ if (source === 'user-local') {
93
+ return 'high';
94
+ }
95
+ if (source === 'built-in' && loginish) {
96
+ return 'high';
97
+ }
98
+ if (source === 'built-in') {
99
+ return 'medium';
100
+ }
101
+ return 'low';
102
+ }
103
+ async function buildFormPaths(page) {
104
+ const passwordCount = await page.locator('input[type="password"]').count();
105
+ if (passwordCount === 0) {
106
+ return [];
107
+ }
108
+ const formType = passwordCount > 1 ? 'form-multi' : 'form-login';
109
+ const fields = await page.evaluate(() => {
110
+ const pwd = document.querySelector('input[type="password"]');
111
+ if (!pwd) {
112
+ return [];
113
+ }
114
+ const form = pwd.closest('form') ?? document.body;
115
+ const out = [];
116
+ const inputs = form.querySelectorAll('input, select, textarea');
117
+ for (const el of inputs) {
118
+ if (!(el instanceof HTMLElement)) {
119
+ continue;
120
+ }
121
+ const tag = el.tagName.toLowerCase();
122
+ if (tag === 'input') {
123
+ const inp = el;
124
+ const t = (inp.type || 'text').toLowerCase();
125
+ if (['hidden', 'submit', 'button', 'image', 'reset'].includes(t)) {
126
+ continue;
127
+ }
128
+ let fieldType = 'text';
129
+ if (t === 'password') {
130
+ fieldType = 'password';
131
+ }
132
+ else if (t === 'email') {
133
+ fieldType = 'email';
134
+ }
135
+ else if (t === 'checkbox') {
136
+ fieldType = 'checkbox';
137
+ }
138
+ const id = inp.id;
139
+ let label = inp.getAttribute('aria-label') ?? inp.placeholder ?? inp.name ?? fieldType;
140
+ if (id) {
141
+ const lab = document.querySelector(`label[for="${CSS.escape(id)}"]`);
142
+ if (lab?.textContent) {
143
+ label = lab.textContent.trim();
144
+ }
145
+ }
146
+ out.push({
147
+ name: inp.name || inp.id || fieldType,
148
+ label: label.slice(0, 120),
149
+ type: fieldType,
150
+ observedOptions: [],
151
+ });
152
+ }
153
+ else if (tag === 'select') {
154
+ const sel = el;
155
+ const opts = Array.from(sel.options).map((o) => o.text.trim()).filter(Boolean);
156
+ out.push({
157
+ name: sel.name || sel.id || 'select',
158
+ label: (sel.getAttribute('aria-label') ?? sel.name ?? 'select').slice(0, 120),
159
+ type: 'select',
160
+ observedOptions: opts.slice(0, 50),
161
+ });
162
+ }
163
+ }
164
+ return out;
165
+ });
166
+ const requirements = fields.length > 0
167
+ ? { method: 'credentials', fields }
168
+ : {
169
+ method: 'unknown',
170
+ instruction: 'A password field exists but field metadata could not be read. Inspect the page in devtools and configure form-login selectors manually, or use `qulib auth init`.',
171
+ };
172
+ return [
173
+ {
174
+ id: formType === 'form-multi' ? 'form-multi' : 'form-login',
175
+ label: formType === 'form-multi' ? 'Multi-field sign-in form' : 'Username / password form',
176
+ type: formType,
177
+ provider: null,
178
+ source: 'heuristic',
179
+ automatable: requirements.method === 'credentials',
180
+ confidence: requirements.method === 'credentials' ? 'medium' : 'low',
181
+ requirements,
182
+ },
183
+ ];
184
+ }
185
+ export async function exploreAuth(url, timeoutMs = 20000) {
186
+ const browser = await launchBrowser();
187
+ try {
188
+ const context = await browser.newContext();
189
+ const page = await context.newPage();
190
+ await page.goto(url, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
191
+ await waitNetworkIdleBestEffort(page);
192
+ const loginishAfterFirst = /login|sign[- ]?in|auth/i.test(page.url()) || (await page.locator('input[type="password"]').count()) > 0;
193
+ if (!loginishAfterFirst) {
194
+ const loginLink = page.locator('a').filter({ hasText: /^(log ?in|sign ?in|sign in)$/i }).first();
195
+ if ((await loginLink.count()) > 0) {
196
+ const href = await loginLink.getAttribute('href');
197
+ if (href) {
198
+ const next = new URL(href, url).toString();
199
+ await page.goto(next, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
200
+ await waitNetworkIdleBestEffort(page);
201
+ }
202
+ }
203
+ }
204
+ const finalUrl = page.url();
205
+ const loginish = onLoginishPage(finalUrl) || (await page.locator('input[type="password"]').count()) > 0;
206
+ const allProviders = buildAllProviders();
207
+ const texts = await collectVisibleControlTexts(page);
208
+ const consumed = new Set();
209
+ const authPaths = [];
210
+ const unrecognizedButtons = [];
211
+ for (const rawText of texts) {
212
+ const text = rawText.trim();
213
+ if (!text) {
214
+ continue;
215
+ }
216
+ let matched = null;
217
+ for (const p of allProviders) {
218
+ if (!matchProvider(text, p)) {
219
+ continue;
220
+ }
221
+ if (p.source === 'built-in' && !(textLooksLikeOAuthIdpButton(text) || loginish)) {
222
+ continue;
223
+ }
224
+ matched = { p, gate: textLooksLikeOAuthIdpButton(text) || loginish };
225
+ break;
226
+ }
227
+ if (matched) {
228
+ const { p, gate } = matched;
229
+ const id = `oauth:${p.id}`;
230
+ if (consumed.has(id)) {
231
+ continue;
232
+ }
233
+ consumed.add(id);
234
+ authPaths.push({
235
+ id,
236
+ label: p.label,
237
+ type: 'oauth',
238
+ provider: p.id,
239
+ source: p.source,
240
+ automatable: false,
241
+ confidence: oauthConfidence(p.source, loginish || gate),
242
+ requirements: storageRequirement(),
243
+ });
244
+ continue;
245
+ }
246
+ if (isHeuristicUnknownSso(text, loginish)) {
247
+ const slug = slugifyLabel(text);
248
+ const id = `oauth-unknown:${slug}`;
249
+ if (consumed.has(id)) {
250
+ continue;
251
+ }
252
+ consumed.add(id);
253
+ authPaths.push({
254
+ id,
255
+ label: text.slice(0, 100),
256
+ type: 'oauth-unknown',
257
+ provider: null,
258
+ source: 'heuristic',
259
+ automatable: false,
260
+ confidence: 'low',
261
+ requirements: storageRequirement(),
262
+ });
263
+ const safePattern = text.slice(0, 48).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
264
+ unrecognizedButtons.push({
265
+ label: text.slice(0, 100),
266
+ hint: `If this is your org SSO, register it: qulib auth providers add --id "${slug}" --label "${text.replace(/"/g, '\\"').slice(0, 80)}" --pattern "${safePattern}"`,
267
+ });
268
+ }
269
+ }
270
+ const pageText = await page.locator('body').innerText().catch(() => '');
271
+ if (MAGIC_LINK_PATTERNS.some((re) => re.test(pageText))) {
272
+ authPaths.push({
273
+ id: 'magic-link',
274
+ label: 'Magic link / passwordless',
275
+ type: 'magic-link',
276
+ provider: null,
277
+ source: 'heuristic',
278
+ automatable: false,
279
+ confidence: 'medium',
280
+ requirements: {
281
+ method: 'storage-state',
282
+ instruction: 'Magic-link flows need a human in the loop. Use `qulib auth init --base-url <app-url>` and complete email or provider steps in the opened browser, then reuse the saved storage state for scans.',
283
+ },
284
+ });
285
+ }
286
+ authPaths.push(...(await buildFormPaths(page)));
287
+ const authRequired = authPaths.length > 0;
288
+ let authScope = 'none';
289
+ if (authRequired) {
290
+ if (loginish) {
291
+ authScope = 'site-wide';
292
+ }
293
+ else {
294
+ authScope = /login|signin|auth/i.test(new URL(finalUrl).pathname) ? 'site-wide' : 'optional';
295
+ }
296
+ }
297
+ const suggestedParts = [];
298
+ if (authPaths.some((p) => p.type === 'oauth' || p.type === 'oauth-unknown')) {
299
+ suggestedParts.push('For OAuth or unrecognized SSO buttons, collect a Playwright storage state with `qulib auth init` before calling `analyze_app`.');
300
+ }
301
+ if (authPaths.some((p) => p.type === 'form-login' || p.type === 'form-multi')) {
302
+ suggestedParts.push('For password forms, gather username/password and stable selectors (or use storage state if MFA applies).');
303
+ }
304
+ if (authPaths.some((p) => p.type === 'magic-link')) {
305
+ suggestedParts.push('For magic-link, use `qulib auth init` after the user completes email delivery.');
306
+ }
307
+ if (!authRequired) {
308
+ suggestedParts.push('No sign-in surface detected at this URL; you can run `analyze_app` without auth unless gated deeper in the app.');
309
+ }
310
+ const exploration = {
311
+ url: finalUrl,
312
+ authRequired,
313
+ authScope,
314
+ authPaths,
315
+ observedAt: new Date().toISOString(),
316
+ suggestedAgentBehavior: suggestedParts.join(' '),
317
+ unrecognizedButtons,
318
+ };
319
+ return AuthExplorationSchema.parse(exploration);
320
+ }
321
+ finally {
322
+ await browser.close();
323
+ }
324
+ }
@@ -0,0 +1,7 @@
1
+ export interface OAuthProvider {
2
+ id: string;
3
+ label: string;
4
+ patterns: RegExp[];
5
+ }
6
+ export declare const BUILT_IN_OAUTH_PROVIDERS: OAuthProvider[];
7
+ //# sourceMappingURL=oauth-providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-providers.d.ts","sourceRoot":"","sources":["../../src/tools/oauth-providers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,wBAAwB,EAAE,aAAa,EAsBnD,CAAC"}
@@ -0,0 +1,21 @@
1
+ export const BUILT_IN_OAUTH_PROVIDERS = [
2
+ { id: 'github', label: 'GitHub', patterns: [/\bgithub\b/i] },
3
+ { id: 'google', label: 'Google', patterns: [/\bgoogle\b/i, /accounts\.google\.com/i] },
4
+ { id: 'microsoft', label: 'Microsoft', patterns: [/microsoft/i, /login\.microsoftonline\.com/i] },
5
+ { id: 'apple', label: 'Apple', patterns: [/sign in with apple/i, /\bapple id\b/i] },
6
+ { id: 'facebook', label: 'Facebook', patterns: [/facebook/i] },
7
+ { id: 'twitter', label: 'Twitter/X', patterns: [/twitter\.com/i, /\bsign in with x\b/i] },
8
+ { id: 'linkedin', label: 'LinkedIn', patterns: [/linkedin/i] },
9
+ { id: 'auth0', label: 'Auth0', patterns: [/auth0/i] },
10
+ { id: 'okta', label: 'Okta', patterns: [/\bokta\b/i] },
11
+ { id: 'onelogin', label: 'OneLogin', patterns: [/onelogin/i] },
12
+ { id: 'duo', label: 'Duo Security', patterns: [/duo security/i] },
13
+ { id: 'ping', label: 'Ping Identity', patterns: [/pingidentity/i, /pingone/i] },
14
+ { id: 'workday', label: 'Workday', patterns: [/workday/i] },
15
+ { id: 'saml', label: 'SAML SSO', patterns: [/\bsaml\b/i] },
16
+ { id: 'clever', label: 'Clever', patterns: [/\bclever\b/i, /clever\.com/i] },
17
+ { id: 'classlink', label: 'ClassLink', patterns: [/classlink/i] },
18
+ { id: 'schoology', label: 'Schoology', patterns: [/schoology/i] },
19
+ { id: 'canvas', label: 'Canvas (Instructure)', patterns: [/\bcanvas lms\b/i, /instructure/i] },
20
+ { id: 'blackboard', label: 'Blackboard', patterns: [/blackboard/i] },
21
+ ];
@@ -0,0 +1,15 @@
1
+ import type { OAuthProvider } from './oauth-providers.js';
2
+ export interface SerializedProvider {
3
+ id: string;
4
+ label: string;
5
+ patterns: string[];
6
+ }
7
+ export declare function loadUserProviders(): OAuthProvider[];
8
+ export declare function addUserProvider(input: {
9
+ id: string;
10
+ label: string;
11
+ pattern: string;
12
+ }): void;
13
+ export declare function removeUserProvider(id: string): boolean;
14
+ export declare function listUserProviders(): SerializedProvider[];
15
+ //# sourceMappingURL=user-providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-providers.d.ts","sourceRoot":"","sources":["../../src/tools/user-providers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,iBAAiB,IAAI,aAAa,EAAE,CAOnD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAc3F;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAStD;AAED,wBAAgB,iBAAiB,IAAI,kBAAkB,EAAE,CAExD"}
@@ -0,0 +1,62 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const USER_PROVIDERS_PATH = join(homedir(), '.qulib', 'providers.json');
5
+ export function loadUserProviders() {
6
+ const raw = loadSerialized();
7
+ return raw.map((p) => ({
8
+ id: p.id,
9
+ label: p.label,
10
+ patterns: p.patterns.map((src) => new RegExp(src, 'i')),
11
+ }));
12
+ }
13
+ export function addUserProvider(input) {
14
+ const existing = loadSerialized();
15
+ const idx = existing.findIndex((p) => p.id === input.id);
16
+ if (idx >= 0) {
17
+ const p = existing[idx];
18
+ if (!p.patterns.includes(input.pattern)) {
19
+ p.patterns.push(input.pattern);
20
+ }
21
+ p.label = input.label;
22
+ }
23
+ else {
24
+ existing.push({ id: input.id, label: input.label, patterns: [input.pattern] });
25
+ }
26
+ ensureDir();
27
+ writeFileSync(USER_PROVIDERS_PATH, JSON.stringify(existing, null, 2), 'utf-8');
28
+ }
29
+ export function removeUserProvider(id) {
30
+ const existing = loadSerialized();
31
+ const filtered = existing.filter((p) => p.id !== id);
32
+ if (filtered.length === existing.length) {
33
+ return false;
34
+ }
35
+ ensureDir();
36
+ writeFileSync(USER_PROVIDERS_PATH, JSON.stringify(filtered, null, 2), 'utf-8');
37
+ return true;
38
+ }
39
+ export function listUserProviders() {
40
+ return loadSerialized();
41
+ }
42
+ function loadSerialized() {
43
+ if (!existsSync(USER_PROVIDERS_PATH)) {
44
+ return [];
45
+ }
46
+ try {
47
+ const parsed = JSON.parse(readFileSync(USER_PROVIDERS_PATH, 'utf-8'));
48
+ if (!Array.isArray(parsed)) {
49
+ return [];
50
+ }
51
+ return parsed;
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ }
57
+ function ensureDir() {
58
+ const dir = dirname(USER_PROVIDERS_PATH);
59
+ if (!existsSync(dir)) {
60
+ mkdirSync(dir, { recursive: true });
61
+ }
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/core",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",