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 +164 -7
- package/dist/auth-manager.d.ts +3 -2
- package/dist/auth-manager.js +15 -7
- package/dist/cli/commands/get.js +15 -13
- package/dist/cli/commands/login.js +14 -2
- package/dist/cli/commands/logout.js +4 -2
- package/dist/cli/commands/status.js +2 -1
- package/dist/cli/main.js +1 -1
- package/dist/core/interfaces/provider.d.ts +2 -0
- package/dist/providers/provider-registry.d.ts +1 -0
- package/dist/providers/provider-registry.js +15 -0
- package/package.json +1 -1
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
|
-
|
|
11
|
+
### Local machine (has a browser)
|
|
13
12
|
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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)
|
package/dist/auth-manager.d.ts
CHANGED
|
@@ -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
|
|
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(
|
|
40
|
+
resolveProvider(input: string): ProviderConfig;
|
|
40
41
|
/**
|
|
41
42
|
* Get credentials for a specific provider, resolving by URL.
|
|
42
43
|
*/
|
package/dist/auth-manager.js
CHANGED
|
@@ -71,16 +71,24 @@ export class AuthManager {
|
|
|
71
71
|
return ok(authResult.value);
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* Resolve a provider by
|
|
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(
|
|
77
|
-
const existing = this.providers.
|
|
77
|
+
resolveProvider(input) {
|
|
78
|
+
const existing = this.providers.resolveFlexible(input);
|
|
78
79
|
if (existing)
|
|
79
80
|
return existing;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
package/dist/cli/commands/get.js
CHANGED
|
@@ -8,40 +8,42 @@ export async function runGet(positionals, flags, deps) {
|
|
|
8
8
|
process.exitCode = 1;
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
-
|
|
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 (
|
|
15
|
-
|
|
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 === '
|
|
23
|
+
process.exitCode = result.error.code === 'CREDENTIAL_NOT_FOUND' ? 3 : 1;
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
credential = result.value.credential;
|
|
26
|
+
credential = result.value;
|
|
26
27
|
}
|
|
27
28
|
else {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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 === '
|
|
42
|
+
process.exitCode = result.error.code === 'PROVIDER_NOT_FOUND' ? 2 : 3;
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
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>
|
|
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
|
}
|
|
@@ -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.
|