signet-auth 1.0.0 → 1.1.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
@@ -6,20 +6,39 @@ General-purpose authentication CLI. Manages credentials for any web service -- a
6
6
 
7
7
  ```bash
8
8
  npm install -g signet-auth
9
- sig init
10
9
  ```
11
10
 
12
- That's it. `sig init` auto-detects your browser and creates `~/.signet/config.yaml` with sensible defaults.
11
+ ### Local machine (has a browser)
13
12
 
14
- ### From source
13
+ ```bash
14
+ sig init # Auto-detects your browser, creates ~/.signet/config.yaml
15
+ sig login <url> # Opens browser for SSO — done
16
+ ```
17
+
18
+ ### Remote / headless machine (no browser)
19
+
20
+ ```bash
21
+ sig init --remote # Sets up browserless mode
22
+ ```
23
+
24
+ Then get credentials from your local machine or set them manually:
15
25
 
16
26
  ```bash
17
- git clone <repo-url> && cd signet
18
- npm install
19
- npm run build
27
+ # On your local machine (has browser):
28
+ sig remote add <name> <remote-host> # Point to the remote machine
29
+ sig sync push <name> # Push credentials over SSH
30
+
31
+ # Or set credentials manually on the remote:
32
+ sig login <url> --cookie "session=abc; id=xyz" # Paste from browser DevTools
33
+ sig login <url> --token <token> # API key or PAT
20
34
  ```
21
35
 
22
- The CLI is available as `sig` (or `./bin/sig.js`).
36
+ ### From source
37
+
38
+ ```bash
39
+ git clone https://github.com/signet-auth/signet.git && cd signet
40
+ npm install && npm run build
41
+ ```
23
42
 
24
43
  ## Quick Start
25
44
 
@@ -437,6 +456,144 @@ sig login https://api.example.com --token ghp_xxxxxxxxxxxxx
437
456
 
438
457
  Cookie-based auth is the default -- just `sig login <url>` and complete SSO in the browser window.
439
458
 
459
+ ## AI Agent Integration
460
+
461
+ Signet works as an auth layer for AI coding agents (Claude Code, Cursor, Windsurf, etc.) that need to interact with authenticated web services. The agent shells out to `sig` via bash -- no SDK or MCP server needed.
462
+
463
+ ### How it works
464
+
465
+ 1. **Human authenticates once** via browser SSO: `sig login <url>`
466
+ 2. **Agent reuses stored credentials** via CLI: `sig get`, `sig request`
467
+ 3. **On auth failure**, agent re-triggers login: `sig login <url>` (opens browser for human)
468
+
469
+ The human handles the browser SSO flow; the agent handles everything else.
470
+
471
+ ### Pattern 1: Direct authenticated requests
472
+
473
+ For APIs where the agent makes HTTP calls directly. Signet injects credentials automatically.
474
+
475
+ ```bash
476
+ # GET — read data
477
+ sig request "https://jira.example.com/rest/api/2/issue/PROJ-123" --format body
478
+
479
+ # POST — create/update (add CSRF headers for cookie-based APIs)
480
+ sig request "https://jira.example.com/rest/api/2/issue" \
481
+ --method POST \
482
+ --header "Content-Type: application/json" \
483
+ --header "X-Requested-With: XMLHttpRequest" \
484
+ --body '{"fields": {"summary": "New issue"}}' \
485
+ --format body
486
+ ```
487
+
488
+ Best for: REST APIs where the agent constructs requests directly (Jira, Confluence, etc.)
489
+
490
+ ### Pattern 2: Credential pass-through to scripts
491
+
492
+ For agents that call helper scripts (Python, Node, etc.) which handle HTTP internally. The agent extracts the credential and passes it as a CLI argument.
493
+
494
+ ```bash
495
+ # Cookie-based services
496
+ CRED=$(sig get https://wiki.example.com/ --format value)
497
+ python scripts/wiki_search.py --cookie "$CRED" --keyword "deployment guide"
498
+
499
+ # Bearer token services
500
+ TOKEN=$(sig get https://graph.microsoft.com/ --format value | sed 's/^Bearer //')
501
+ python scripts/calendar.py --token "$TOKEN" --range today
502
+ ```
503
+
504
+ **Note on bearer tokens:** `sig get` returns `Bearer eyJ...` (with prefix). If your scripts add the `Bearer` prefix themselves, strip it with `sed 's/^Bearer //'`.
505
+
506
+ Best for: Complex workflows wrapped in Python/Node scripts that accept credentials via CLI args.
507
+
508
+ ### Pattern 3: Curl with sig credentials
509
+
510
+ For multipart uploads or requests that `sig request` can't handle (e.g., file attachments).
511
+
512
+ ```bash
513
+ CRED=$(sig get https://jira.example.com/ --format value)
514
+ curl -X POST "https://jira.example.com/rest/api/2/issue/PROJ-123/attachments" \
515
+ -H "Cookie: $CRED" \
516
+ -H "X-Atlassian-Token: no-check" \
517
+ -F "file=@/path/to/file.png"
518
+ ```
519
+
520
+ ### Error handling for agents
521
+
522
+ Teach agents to detect auth failures and re-authenticate:
523
+
524
+ | Signal | Meaning | Agent action |
525
+ |--------|---------|-------------|
526
+ | HTTP 401/403 | Session expired | Run `sig login <url>`, retry |
527
+ | HTML login page in response | SSO redirect | Run `sig login <url>`, retry |
528
+ | `sig get` returns empty | No stored credential | Run `sig login <url>` |
529
+
530
+ ### Skill-based setup (Claude Code)
531
+
532
+ For [Claude Code](https://docs.anthropic.com/en/docs/claude-code), create a [skill](https://docs.anthropic.com/en/docs/claude-code/skills) with a `SKILL.md` that documents the auth pattern and API endpoints. The skill triggers automatically based on its description.
533
+
534
+ Example `SKILL.md` structure:
535
+
536
+ ```markdown
537
+ ---
538
+ name: my-api
539
+ description: "Interact with My API. Trigger on: my-api, tickets, issues..."
540
+ ---
541
+ # My API
542
+
543
+ ## Authentication
544
+ Get credential: `CRED=$(sig get https://api.example.com/ --format value)`
545
+ Re-auth: `sig login https://api.example.com/`
546
+
547
+ ## Endpoints
548
+ | Operation | Command |
549
+ |-----------|---------|
550
+ | List items | `sig request "https://api.example.com/items" --format body` |
551
+ | Create item | `sig request "https://api.example.com/items" --method POST --body '...' --format body` |
552
+ ```
553
+
554
+ ### Multi-service configuration
555
+
556
+ Agents often need access to multiple services. Configure all providers in a single `~/.signet/config.yaml`:
557
+
558
+ ```yaml
559
+ providers:
560
+ jira:
561
+ domains: ["jira.example.com"]
562
+ strategy: cookie
563
+ config:
564
+ ttl: "10d"
565
+
566
+ wiki:
567
+ domains: ["wiki.example.com"]
568
+ strategy: cookie
569
+ config:
570
+ ttl: "12h"
571
+
572
+ ms-teams:
573
+ domains: ["teams.cloud.microsoft"]
574
+ entryUrl: https://teams.cloud.microsoft/v2/
575
+ strategy: oauth2
576
+ config:
577
+ audiences: ["https://ic3.teams.office.com"]
578
+
579
+ ms-graph:
580
+ domains: ["graph.microsoft.com"]
581
+ entryUrl: https://teams.cloud.microsoft/v2/
582
+ strategy: oauth2
583
+ config:
584
+ audiences: ["https://graph.microsoft.com"]
585
+ ```
586
+
587
+ Then authenticate all at once:
588
+
589
+ ```bash
590
+ sig login https://jira.example.com/
591
+ sig login https://wiki.example.com/
592
+ sig login https://teams.cloud.microsoft/v2/
593
+ ```
594
+
595
+ The agent resolves providers by URL automatically -- no provider IDs needed in agent code.
596
+
440
597
  ## License
441
598
 
442
599
  [MIT](LICENSE)
@@ -34,9 +34,10 @@ export declare class AuthManager {
34
34
  */
35
35
  getCredentials(providerId: string): Promise<Result<Credential, AuthError>>;
36
36
  /**
37
- * Resolve a provider by URL, auto-provisioning a default cookie provider if none matches.
37
+ * Resolve a provider by ID, name, URL, or domain.
38
+ * Auto-provisions a default cookie provider only for URL-like inputs that don't match.
38
39
  */
39
- resolveProvider(url: string): ProviderConfig;
40
+ resolveProvider(input: string): ProviderConfig;
40
41
  /**
41
42
  * Get credentials for a specific provider, resolving by URL.
42
43
  */
@@ -71,16 +71,24 @@ export class AuthManager {
71
71
  return ok(authResult.value);
72
72
  }
73
73
  /**
74
- * Resolve a provider by URL, auto-provisioning a default cookie provider if none matches.
74
+ * Resolve a provider by ID, name, URL, or domain.
75
+ * Auto-provisions a default cookie provider only for URL-like inputs that don't match.
75
76
  */
76
- resolveProvider(url) {
77
- const existing = this.providers.resolve(url);
77
+ resolveProvider(input) {
78
+ const existing = this.providers.resolveFlexible(input);
78
79
  if (existing)
79
80
  return existing;
80
- const provider = createDefaultProvider(url);
81
- this.providers.register(provider);
82
- this.logger?.info(`Auto-provisioned provider "${provider.id}" for ${url}`);
83
- return provider;
81
+ // Only auto-provision for URL-like inputs (contains '.' or starts with 'http')
82
+ const isUrlLike = input.startsWith('http://') || input.startsWith('https://') || input.includes('.');
83
+ if (isUrlLike) {
84
+ const provider = createDefaultProvider(input);
85
+ this.providers.register(provider);
86
+ this.logger?.info(`Auto-provisioned provider "${provider.id}" for ${input}`);
87
+ return provider;
88
+ }
89
+ // For non-URL inputs that don't resolve, return a not-found error via a sentinel
90
+ // that will be caught by callers. We throw here since the method returns ProviderConfig.
91
+ throw new ProviderNotFoundError(input);
84
92
  }
85
93
  /**
86
94
  * Get credentials for a specific provider, resolving by URL.
@@ -8,40 +8,42 @@ export async function runGet(positionals, flags, deps) {
8
8
  process.exitCode = 1;
9
9
  return;
10
10
  }
11
- const isUrl = target.startsWith('http://') || target.startsWith('https://');
11
+ // Unified resolution: try ID → name → URL/domain
12
+ const resolved = deps.authManager.providerRegistry.resolveFlexible(target);
12
13
  let providerId;
13
14
  let credential;
14
- if (isUrl) {
15
- const result = await deps.authManager.getCredentialsByUrl(target);
15
+ if (resolved) {
16
+ providerId = resolved.id;
17
+ const result = await deps.authManager.getCredentials(providerId);
16
18
  if (!isOk(result)) {
17
19
  process.stderr.write(`Error: ${result.error.message}\n`);
18
20
  if (result.error.code === 'BROWSER_UNAVAILABLE') {
19
21
  process.stderr.write(`Hint: Run "sig login ${target} --token <token>" or "sig sync pull" to get credentials.\n`);
20
22
  }
21
- process.exitCode = result.error.code === 'PROVIDER_NOT_FOUND' ? 2 : 3;
23
+ process.exitCode = result.error.code === 'CREDENTIAL_NOT_FOUND' ? 3 : 1;
22
24
  return;
23
25
  }
24
- providerId = result.value.provider.id;
25
- credential = result.value.credential;
26
+ credential = result.value;
26
27
  }
27
28
  else {
28
- const provider = deps.authManager.providerRegistry.get(target);
29
- if (!provider) {
30
- process.stderr.write(`Error: No provider found with ID "${target}".\n`);
29
+ // Fall through to URL-based resolution (with auto-provisioning) for URL-like inputs
30
+ const isUrl = target.includes('.') || target.startsWith('http');
31
+ if (!isUrl) {
32
+ process.stderr.write(`Error: No provider found matching "${target}".\n`);
31
33
  process.exitCode = 2;
32
34
  return;
33
35
  }
34
- providerId = provider.id;
35
- const result = await deps.authManager.getCredentials(providerId);
36
+ const result = await deps.authManager.getCredentialsByUrl(target);
36
37
  if (!isOk(result)) {
37
38
  process.stderr.write(`Error: ${result.error.message}\n`);
38
39
  if (result.error.code === 'BROWSER_UNAVAILABLE') {
39
40
  process.stderr.write(`Hint: Run "sig login ${target} --token <token>" or "sig sync pull" to get credentials.\n`);
40
41
  }
41
- process.exitCode = result.error.code === 'CREDENTIAL_NOT_FOUND' ? 3 : 1;
42
+ process.exitCode = result.error.code === 'PROVIDER_NOT_FOUND' ? 2 : 3;
42
43
  return;
43
44
  }
44
- credential = result.value;
45
+ providerId = result.value.provider.id;
46
+ credential = result.value.credential;
45
47
  }
46
48
  const headers = deps.authManager.applyToRequest(providerId, credential);
47
49
  const entries = Object.entries(headers);
@@ -2,6 +2,7 @@ import { buildStrategyConfig } from '../../config/validator.js';
2
2
  import { addProviderToConfig } from '../../config/loader.js';
3
3
  import { isOk } from '../../core/result.js';
4
4
  import { formatJson } from '../formatters.js';
5
+ import { ProviderNotFoundError } from '../../core/errors.js';
5
6
  /** Convert runtime ProviderConfig to the YAML ProviderEntry format. */
6
7
  function toProviderEntry(pc) {
7
8
  const { strategy: _s, ...strategyRest } = pc.strategyConfig;
@@ -35,11 +36,22 @@ function parseCookieString(raw, domain) {
35
36
  export async function runLogin(positionals, flags, deps) {
36
37
  const url = positionals[0];
37
38
  if (!url) {
38
- process.stderr.write('Usage: sig login <url>\n');
39
+ process.stderr.write('Usage: sig login <provider|url>\n');
39
40
  process.exitCode = 1;
40
41
  return;
41
42
  }
42
- const baseProvider = deps.authManager.resolveProvider(url);
43
+ let baseProvider;
44
+ try {
45
+ baseProvider = deps.authManager.resolveProvider(url);
46
+ }
47
+ catch (e) {
48
+ if (e instanceof ProviderNotFoundError) {
49
+ process.stderr.write(`Error: No provider found matching "${url}". Run "sig providers" to see configured providers.\n`);
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+ throw e;
54
+ }
43
55
  const hasOverrides = flags.strategy !== undefined;
44
56
  const provider = hasOverrides
45
57
  ? { ...baseProvider }
@@ -1,8 +1,10 @@
1
1
  export async function runLogout(positionals, flags, deps) {
2
2
  const providerId = positionals[0];
3
3
  if (providerId) {
4
- await deps.authManager.clearCredentials(providerId);
5
- process.stderr.write(`Credentials cleared for "${providerId}".\n`);
4
+ const resolved = deps.authManager.providerRegistry.resolveFlexible(providerId);
5
+ const resolvedId = resolved?.id ?? providerId;
6
+ await deps.authManager.clearCredentials(resolvedId);
7
+ process.stderr.write(`Credentials cleared for "${resolvedId}".\n`);
6
8
  }
7
9
  else {
8
10
  await deps.authManager.clearAll();
@@ -3,7 +3,8 @@ export async function runStatus(positionals, flags, deps) {
3
3
  const providerId = flags.provider ?? positionals[0];
4
4
  const format = flags.format ?? (process.stdout.isTTY ? 'table' : 'json');
5
5
  if (providerId) {
6
- const status = await deps.authManager.getStatus(providerId);
6
+ const resolved = deps.authManager.providerRegistry.resolveFlexible(providerId);
7
+ const status = await deps.authManager.getStatus(resolved?.id ?? providerId);
7
8
  if (format === 'json') {
8
9
  process.stdout.write(formatJson(status) + '\n');
9
10
  }
package/dist/cli/main.js CHANGED
@@ -44,7 +44,7 @@ const HELP = `Usage: sig <command> [options]
44
44
  Commands:
45
45
  init Set up Signet configuration (interactive)
46
46
  get <provider|url> Get credential headers for a provider or URL
47
- login <url> Authenticate with a system (browser or token)
47
+ login <provider|url> Authenticate with a system (browser or token)
48
48
  request <url> Make an authenticated HTTP request
49
49
  status [provider] Show authentication status
50
50
  logout [provider] Clear stored credentials
@@ -12,4 +12,6 @@ export interface IProviderRegistry {
12
12
  list(): ProviderConfig[];
13
13
  /** Register a new provider at runtime. Overwrites if ID already exists. */
14
14
  register(provider: ProviderConfig): void;
15
+ /** Resolve a provider by ID, name (case-insensitive), or URL/domain, in that order. */
16
+ resolveFlexible(input: string): ProviderConfig | null;
15
17
  }
@@ -16,4 +16,5 @@ export declare class ProviderRegistry implements IProviderRegistry {
16
16
  get(id: string): ProviderConfig | null;
17
17
  list(): ProviderConfig[];
18
18
  register(provider: ProviderConfig): void;
19
+ resolveFlexible(input: string): ProviderConfig | null;
19
20
  }
@@ -50,6 +50,21 @@ export class ProviderRegistry {
50
50
  register(provider) {
51
51
  this.providers.set(provider.id, provider);
52
52
  }
53
+ resolveFlexible(input) {
54
+ // 1. Exact ID match
55
+ const byId = this.providers.get(input);
56
+ if (byId)
57
+ return byId;
58
+ // 2. Case-insensitive name match
59
+ const inputLower = input.toLowerCase();
60
+ for (const provider of this.providers.values()) {
61
+ if (provider.name.toLowerCase() === inputLower) {
62
+ return provider;
63
+ }
64
+ }
65
+ // 3. URL/domain match (existing behavior)
66
+ return this.resolve(input);
67
+ }
53
68
  }
54
69
  /**
55
70
  * Simple glob matching for domain patterns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signet-auth",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "General-purpose authentication CLI with pluggable strategies and browser adapters",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",