@almyty/credentials 1.5.1 → 1.5.3

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,10 +6,11 @@ registries then use the credential. Model providers, deployment clouds,
6
6
  buckets, chat platforms: all of them are services in the same catalog.
7
7
 
8
8
  ```sh
9
- npx @almyty/auth login
10
- npx @almyty/credentials services --kind inference
11
- npx @almyty/credentials add openrouter --open
12
- npx @almyty/credentials list
9
+ npm i -g @almyty/cli
10
+ almyty login
11
+ almyty credentials services --kind inference
12
+ almyty credentials add openrouter --open
13
+ almyty credentials list
13
14
  ```
14
15
 
15
16
  ## What a credential is
@@ -43,7 +44,7 @@ Every read command takes `--json` and writes undecorated JSON to stdout.
43
44
 
44
45
  | Command | What it does |
45
46
  |---|---|
46
- | `add <service> [--method m] [--owner org\|user\|private] [--name n] [--headless] [--open]` | Add a credential |
47
+ | `add <service> [--method m] [--owner org\|team\|user\|private] [--team id\|slug] [--name n] [--headless] [--open]` | Add a credential |
47
48
  | `complete <key> --state s --code c` | Finish a headless sign-in by pasting the code |
48
49
  | `validate <id>` | Re-check against the provider; refreshes health and the account label |
49
50
  | `rotate <id> [--headless] [--open]` | Replace the secret in place, so everything using the credential keeps working |
@@ -55,14 +56,14 @@ unless `--method` names another. `services` lists them.
55
56
  **Sign-in** (OpenRouter and Slack today) is the best path: nothing to paste.
56
57
 
57
58
  ```sh
58
- npx @almyty/credentials add openrouter --open
59
+ almyty credentials add openrouter --open
59
60
  ```
60
61
 
61
62
  On a machine with no browser, ask for the headless flow and the provider shows
62
63
  a code:
63
64
 
64
65
  ```sh
65
- npx @almyty/credentials add openrouter --headless
66
+ almyty credentials add openrouter --headless
66
67
  # -> Open this URL to continue: https://…
67
68
  # The provider will show you a code. Paste it with:
68
69
  # almyty credentials complete openrouter --state st-… --code <code>
@@ -76,7 +77,7 @@ whose state the redirect has already consumed.
76
77
  the page where the key is created:
77
78
 
78
79
  ```sh
79
- npx @almyty/credentials add huggingface --owner user
80
+ almyty credentials add huggingface --owner user
80
81
  # Get a key at: https://huggingface.co/settings/tokens
81
82
  # Access token: ······
82
83
  ```
@@ -106,6 +107,10 @@ narrows that to a principal.
106
107
  `--owner org` (the default) makes the credential the organization's: this is
107
108
  what agents and deployments use, and it needs `connections:manage` (admin or
108
109
  owner). `--owner user` makes it yours, and you can grant it to others.
110
+ `--owner team --team <id|slug>` makes it the organization's for one team:
111
+ only that team's members (and admins) see or use it. The slug is the team's
112
+ name in lowercase with dashes (`Customer Support` is `customer-support`); a
113
+ member can pick only a team they are on.
109
114
  `--owner private` makes it yours alone: nobody else sees or uses it, org admins
110
115
  included, and it cannot be granted. Free and personal organizations allow
111
116
  personal and private credentials by default; paid organizations start with them off until
@@ -125,9 +130,9 @@ In order of preference:
125
130
  4. **`--input-stdin`** — the same object on stdin.
126
131
 
127
132
  ```sh
128
- npx @almyty/credentials add channel-telegram --input-file bot.json
133
+ almyty credentials add channel-telegram --input-file bot.json
129
134
  pass show telegram/bot | jq -Rn '{bot_token: input}' \
130
- | npx @almyty/credentials add channel-telegram --input-stdin
135
+ | almyty credentials add channel-telegram --input-stdin
131
136
  ```
132
137
 
133
138
  `--input '<json>'` still works for the fields a service does **not** mark
@@ -161,9 +166,9 @@ Every chat channel is a service like any other, so a Slack, Discord or
161
166
  Telegram token is added here instead of pasted into a gateway form:
162
167
 
163
168
  ```sh
164
- npx @almyty/credentials services --kind channel
165
- npx @almyty/credentials add channel-slack --open
166
- npx @almyty/credentials add channel-telegram
169
+ almyty credentials services --kind channel
170
+ almyty credentials add channel-slack --open
171
+ almyty credentials add channel-telegram
167
172
  ```
168
173
 
169
174
  The service key is `channel-<gateway type>` with underscores dasherized
@@ -187,7 +192,7 @@ to get it, and how it is validated.
187
192
  | 0 | success |
188
193
  | 1 | unexpected error |
189
194
  | 2 | usage error (bad flags, missing argument, unknown command, a secret on argv) |
190
- | 3 | not authenticated — run `npx @almyty/auth login` |
195
+ | 3 | not authenticated — run `almyty login` |
191
196
  | 4 | not found |
192
197
  | 5 | the operation ran and failed (a `validate` whose health is not `valid`) |
193
198
 
@@ -202,6 +207,6 @@ turned into tools, served over MCP, A2A, UTCP and Agent Skills.
202
207
  - Design notes: `docs/connections.md` in the almyty repository
203
208
  - Source: https://github.com/almyty-inc/almyty
204
209
 
205
- Run `npx @almyty/credentials --help` for the full surface.
210
+ Run `almyty credentials --help` for the full surface.
206
211
 
207
212
  Apache-2.0 © Almyty Inc.
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>): 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>;
package/dist/index.js CHANGED
@@ -92,7 +92,7 @@ Read:
92
92
  grants <id> Who may use this credential
93
93
 
94
94
  Add:
95
- 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]
96
96
  A sign-in prints an authorize URL (--open launches a
97
97
  browser, --headless asks the service for a code to paste).
98
98
  A key form prompts for each field, secrets not echoed.
@@ -146,10 +146,18 @@ export function needArg(positional, index, name, usage) {
146
146
  throw new UsageError(`${name} is required\n usage: almyty credentials ${usage}`);
147
147
  return v;
148
148
  }
149
- export function connectBody(flags, input) {
149
+ export function connectBody(flags, input, teamId) {
150
150
  const body = { owner: str(flags, 'owner') ?? 'org' };
151
- if (!['org', 'user', 'private'].includes(body.owner))
152
- 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
+ }
153
161
  if (str(flags, 'method'))
154
162
  body.method = str(flags, 'method');
155
163
  if (str(flags, 'name'))
@@ -158,6 +166,35 @@ export function connectBody(flags, input) {
158
166
  body.input = input;
159
167
  return body;
160
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
+ }
161
198
  export function grantBody(flags) {
162
199
  const principalType = need(flags, 'principal');
163
200
  if (!['user', 'team', 'role', 'agent', 'workspace'].includes(principalType))
@@ -270,7 +307,7 @@ export function formatConnectionDetail(c) {
270
307
  `${c.name ?? c.connectorKey}`,
271
308
  ` id ${c.id}`,
272
309
  ` service ${c.connectorKey}${c.connectorDisplayName ? ` (${c.connectorDisplayName})` : ''}${c.kind ? ` [${c.kind}]` : ''}`,
273
- ` owner ${c.owner}${c.ownerUserId ? ` (${c.ownerUserId})` : ''}`,
310
+ ` owner ${c.owner}${c.owner === 'team' && c.teamId ? ` (${c.teamId})` : c.ownerUserId ? ` (${c.ownerUserId})` : ''}`,
274
311
  ` method ${c.method ?? 'unknown'}`,
275
312
  ` account ${c.accountLabel ?? '(the provider named none)'}`,
276
313
  ` health ${c.health?.status ?? 'unknown'}${c.health?.checkedAt ? `, checked ${c.health.checkedAt}` : ', never checked'}`,
@@ -481,7 +518,20 @@ async function main() {
481
518
  requireTty(`connect ${key} via ${method.type}`);
482
519
  input = await promptForSchema(method.schema);
483
520
  }
484
- const body = connectBody({ ...args.flags, method: method.type }, input);
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);
485
535
  if (isRedirect)
486
536
  body.mode = connectMode(args.flags);
487
537
  const res = await post(`/credentials/connect/${key}`, body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almyty/credentials",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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.5.0"
31
+ "@almyty/client": "^1.5.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^25.4.0",