@almyty/credentials 1.5.0 → 1.5.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 +5 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +75 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Every read command takes `--json` and writes undecorated JSON to stdout.
|
|
|
43
43
|
|
|
44
44
|
| Command | What it does |
|
|
45
45
|
|---|---|
|
|
46
|
-
| `add <service> [--method m] [--owner org\|user\|private] [--name n] [--headless] [--open]` | Add a credential |
|
|
46
|
+
| `add <service> [--method m] [--owner org\|team\|user\|private] [--team id\|slug] [--name n] [--headless] [--open]` | Add a credential |
|
|
47
47
|
| `complete <key> --state s --code c` | Finish a headless sign-in by pasting the code |
|
|
48
48
|
| `validate <id>` | Re-check against the provider; refreshes health and the account label |
|
|
49
49
|
| `rotate <id> [--headless] [--open]` | Replace the secret in place, so everything using the credential keeps working |
|
|
@@ -106,6 +106,10 @@ narrows that to a principal.
|
|
|
106
106
|
`--owner org` (the default) makes the credential the organization's: this is
|
|
107
107
|
what agents and deployments use, and it needs `connections:manage` (admin or
|
|
108
108
|
owner). `--owner user` makes it yours, and you can grant it to others.
|
|
109
|
+
`--owner team --team <id|slug>` makes it the organization's for one team:
|
|
110
|
+
only that team's members (and admins) see or use it. The slug is the team's
|
|
111
|
+
name in lowercase with dashes (`Customer Support` is `customer-support`); a
|
|
112
|
+
member can pick only a team they are on.
|
|
109
113
|
`--owner private` makes it yours alone: nobody else sees or uses it, org admins
|
|
110
114
|
included, and it cannot be granted. Free and personal organizations allow
|
|
111
115
|
personal and private credentials by default; paid organizations start with them off until
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,23 @@ export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
|
10
10
|
* which came back as an opaque 400. Say what is missing instead.
|
|
11
11
|
*/
|
|
12
12
|
export declare function needArg(positional: string[], index: number, name: string, usage: string): string;
|
|
13
|
-
export declare function connectBody(flags: ParsedArgs['flags'], input?: Record<string, unknown
|
|
13
|
+
export declare function connectBody(flags: ParsedArgs['flags'], input?: Record<string, unknown>, teamId?: string): Record<string, unknown>;
|
|
14
|
+
/** A team's name as a slug: "Customer Support" is customer-support. */
|
|
15
|
+
export declare function teamSlug(name: string): string;
|
|
16
|
+
/** Whether --team is an id already (sent as is; the API checks it). */
|
|
17
|
+
export declare function isTeamId(ref: string): boolean;
|
|
18
|
+
/** The team --team <slug> names, out of the organization's teams. */
|
|
19
|
+
export declare function pickTeam(teams: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}>, ref: string): string;
|
|
23
|
+
/** The organization a slug is looked up in: the one the key acts in, or the only one. */
|
|
24
|
+
export declare function organizationOf(profile: {
|
|
25
|
+
currentOrganizationId?: string;
|
|
26
|
+
organizations?: Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
}>;
|
|
29
|
+
}): string;
|
|
14
30
|
export declare function grantBody(flags: ParsedArgs['flags']): Record<string, unknown>;
|
|
15
31
|
/** A JSON object, or a message naming which flag was wrong. */
|
|
16
32
|
export declare function parseInputObject(raw: string, flag: string): Record<string, unknown>;
|
|
@@ -62,3 +78,10 @@ export declare function unscreenableInputMessage(): string;
|
|
|
62
78
|
* provider then rejected for a reason that had nothing to do with the key.
|
|
63
79
|
*/
|
|
64
80
|
export declare function requireTty(what: string, isTty?: boolean): void;
|
|
81
|
+
/**
|
|
82
|
+
* True when `argv1` is this module, directly or through the npm bin
|
|
83
|
+
* symlink. The old check matched the path against a regex and bailed out
|
|
84
|
+
* when VITEST was set, so a smoke test that spawned the built CLI from
|
|
85
|
+
* vitest inherited VITEST and got no output at all.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isEntrypoint(argv1: string | undefined, moduleUrl: string): boolean;
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* `--input` stays for non-secret fields and is refused for secret ones.
|
|
16
16
|
*/
|
|
17
17
|
import { createInterface } from 'readline';
|
|
18
|
-
import { readFileSync } from 'fs';
|
|
18
|
+
import { readFileSync, realpathSync } from 'fs';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
19
20
|
import { AlmytyClient, resolveCredentialsOrExit } from '@almyty/client';
|
|
20
21
|
import { EXIT, EXIT_CODE_HELP, UsageError, describeError, exitCodeFor } from './exit-codes.js';
|
|
21
22
|
import { VERSION } from './version.js';
|
|
@@ -91,7 +92,7 @@ Read:
|
|
|
91
92
|
grants <id> Who may use this credential
|
|
92
93
|
|
|
93
94
|
Add:
|
|
94
|
-
add <service> [--method m] [--owner org|user|private] [--name n] [--headless] [--open]
|
|
95
|
+
add <service> [--method m] [--owner org|team|user|private] [--team id|slug] [--name n] [--headless] [--open]
|
|
95
96
|
A sign-in prints an authorize URL (--open launches a
|
|
96
97
|
browser, --headless asks the service for a code to paste).
|
|
97
98
|
A key form prompts for each field, secrets not echoed.
|
|
@@ -145,10 +146,18 @@ export function needArg(positional, index, name, usage) {
|
|
|
145
146
|
throw new UsageError(`${name} is required\n usage: almyty credentials ${usage}`);
|
|
146
147
|
return v;
|
|
147
148
|
}
|
|
148
|
-
export function connectBody(flags, input) {
|
|
149
|
+
export function connectBody(flags, input, teamId) {
|
|
149
150
|
const body = { owner: str(flags, 'owner') ?? 'org' };
|
|
150
|
-
if (!['org', 'user', 'private'].includes(body.owner))
|
|
151
|
-
throw new UsageError('--owner must be org, user or private');
|
|
151
|
+
if (!['org', 'team', 'user', 'private'].includes(body.owner))
|
|
152
|
+
throw new UsageError('--owner must be org, team, user or private');
|
|
153
|
+
if (body.owner === 'team') {
|
|
154
|
+
if (!teamId)
|
|
155
|
+
throw new UsageError('--owner team needs the team: --team <id|slug>');
|
|
156
|
+
body.teamId = teamId;
|
|
157
|
+
}
|
|
158
|
+
else if (str(flags, 'team')) {
|
|
159
|
+
throw new UsageError('--team goes with --owner team');
|
|
160
|
+
}
|
|
152
161
|
if (str(flags, 'method'))
|
|
153
162
|
body.method = str(flags, 'method');
|
|
154
163
|
if (str(flags, 'name'))
|
|
@@ -157,6 +166,35 @@ export function connectBody(flags, input) {
|
|
|
157
166
|
body.input = input;
|
|
158
167
|
return body;
|
|
159
168
|
}
|
|
169
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
170
|
+
/** A team's name as a slug: "Customer Support" is customer-support. */
|
|
171
|
+
export function teamSlug(name) {
|
|
172
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
173
|
+
}
|
|
174
|
+
/** Whether --team is an id already (sent as is; the API checks it). */
|
|
175
|
+
export function isTeamId(ref) {
|
|
176
|
+
return UUID.test(ref);
|
|
177
|
+
}
|
|
178
|
+
/** The team --team <slug> names, out of the organization's teams. */
|
|
179
|
+
export function pickTeam(teams, ref) {
|
|
180
|
+
const wanted = teamSlug(ref);
|
|
181
|
+
const matches = teams.filter((t) => teamSlug(t.name) === wanted);
|
|
182
|
+
if (matches.length === 1)
|
|
183
|
+
return matches[0].id;
|
|
184
|
+
if (matches.length > 1)
|
|
185
|
+
throw new UsageError(`more than one team is called ${ref}; pass its id with --team`);
|
|
186
|
+
const known = teams.map((t) => teamSlug(t.name)).join(', ') || 'none';
|
|
187
|
+
throw new UsageError(`no team ${ref} in this organization (teams: ${known})`);
|
|
188
|
+
}
|
|
189
|
+
/** The organization a slug is looked up in: the one the key acts in, or the only one. */
|
|
190
|
+
export function organizationOf(profile) {
|
|
191
|
+
if (profile.currentOrganizationId)
|
|
192
|
+
return profile.currentOrganizationId;
|
|
193
|
+
const orgs = profile.organizations ?? [];
|
|
194
|
+
if (orgs.length === 1)
|
|
195
|
+
return orgs[0].id;
|
|
196
|
+
throw new UsageError('this login reaches more than one organization; pass the team id with --team');
|
|
197
|
+
}
|
|
160
198
|
export function grantBody(flags) {
|
|
161
199
|
const principalType = need(flags, 'principal');
|
|
162
200
|
if (!['user', 'team', 'role', 'agent', 'workspace'].includes(principalType))
|
|
@@ -269,7 +307,7 @@ export function formatConnectionDetail(c) {
|
|
|
269
307
|
`${c.name ?? c.connectorKey}`,
|
|
270
308
|
` id ${c.id}`,
|
|
271
309
|
` service ${c.connectorKey}${c.connectorDisplayName ? ` (${c.connectorDisplayName})` : ''}${c.kind ? ` [${c.kind}]` : ''}`,
|
|
272
|
-
` owner ${c.owner}${c.ownerUserId ? ` (${c.ownerUserId})` : ''}`,
|
|
310
|
+
` owner ${c.owner}${c.owner === 'team' && c.teamId ? ` (${c.teamId})` : c.ownerUserId ? ` (${c.ownerUserId})` : ''}`,
|
|
273
311
|
` method ${c.method ?? 'unknown'}`,
|
|
274
312
|
` account ${c.accountLabel ?? '(the provider named none)'}`,
|
|
275
313
|
` health ${c.health?.status ?? 'unknown'}${c.health?.checkedAt ? `, checked ${c.health.checkedAt}` : ', never checked'}`,
|
|
@@ -480,7 +518,20 @@ async function main() {
|
|
|
480
518
|
requireTty(`connect ${key} via ${method.type}`);
|
|
481
519
|
input = await promptForSchema(method.schema);
|
|
482
520
|
}
|
|
483
|
-
const
|
|
521
|
+
const teamRef = str(args.flags, 'team');
|
|
522
|
+
let teamId;
|
|
523
|
+
if (teamRef && str(args.flags, 'owner') === 'team') {
|
|
524
|
+
if (isTeamId(teamRef)) {
|
|
525
|
+
teamId = teamRef;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const profile = await q('/auth/profile');
|
|
529
|
+
const orgId = organizationOf(profile.data ?? {});
|
|
530
|
+
const teams = await q(`/organizations/${orgId}/teams`);
|
|
531
|
+
teamId = pickTeam(teams.data ?? [], teamRef);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const body = connectBody({ ...args.flags, method: method.type }, input, teamId);
|
|
484
535
|
if (isRedirect)
|
|
485
536
|
body.mode = connectMode(args.flags);
|
|
486
537
|
const res = await post(`/credentials/connect/${key}`, body);
|
|
@@ -580,8 +631,23 @@ async function main() {
|
|
|
580
631
|
process.exit(EXIT.USAGE);
|
|
581
632
|
}
|
|
582
633
|
}
|
|
583
|
-
|
|
584
|
-
|
|
634
|
+
/**
|
|
635
|
+
* True when `argv1` is this module, directly or through the npm bin
|
|
636
|
+
* symlink. The old check matched the path against a regex and bailed out
|
|
637
|
+
* when VITEST was set, so a smoke test that spawned the built CLI from
|
|
638
|
+
* vitest inherited VITEST and got no output at all.
|
|
639
|
+
*/
|
|
640
|
+
export function isEntrypoint(argv1, moduleUrl) {
|
|
641
|
+
if (!argv1)
|
|
642
|
+
return false;
|
|
643
|
+
try {
|
|
644
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (isEntrypoint(process.argv[1], import.meta.url)) {
|
|
585
651
|
main().catch((err) => {
|
|
586
652
|
console.error(describeError(err, process.env.ALMYTY_URL));
|
|
587
653
|
process.exit(exitCodeFor(err));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almyty/credentials",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"author": "almyty",
|
|
29
29
|
"license": "Apache-2.0",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@almyty/client": "^1.
|
|
31
|
+
"@almyty/client": "^1.5.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^25.4.0",
|