rakomi 0.2.0 → 0.3.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 +7 -4
- package/dist/commands/tenants.js +75 -1
- package/dist/commands/use.js +23 -7
- package/dist/env.js +5 -1
- package/dist/index.js +40 -4
- package/dist/lease-client.js +64 -0
- package/dist/tenants-client.js +43 -0
- package/dist/usage.js +9 -2
- package/package.json +1 -1
- package/sbom.cdx.json +3 -3
package/README.md
CHANGED
|
@@ -15,9 +15,11 @@ session to get a short code to enter on another device instead. If you don't alr
|
|
|
15
15
|
Rakomi administrator for the right value.
|
|
16
16
|
|
|
17
17
|
`rakomi whoami` shows the tenant you signed in against ("Home tenant") and the tenant your other
|
|
18
|
-
commands should treat as active ("Active tenant"). `rakomi
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
commands should treat as active ("Active tenant"). `rakomi tenants memberships` lists every tenant
|
|
19
|
+
your account is a verified member of, with your role in each. `rakomi use <slug>` looks one up from
|
|
20
|
+
that list and remembers it — a slug you don't belong to fails with a plain "not found" rather than
|
|
21
|
+
confirming the tenant exists; `rakomi use <tenant-id>` (a full id) remembers it directly without a
|
|
22
|
+
lookup, and `--tenant <tenant-id>` overrides it for one command.
|
|
21
23
|
|
|
22
24
|
## Connect an AI agent
|
|
23
25
|
|
|
@@ -54,10 +56,11 @@ token), sends a real invitation e-mail, and is refused under `--ci` — see
|
|
|
54
56
|
| `login` | sign in (browser by default; `--no-browser` for a device code) |
|
|
55
57
|
| `logout` | clear the local session |
|
|
56
58
|
| `whoami` | show the signed-in account, home tenant, and active tenant |
|
|
57
|
-
| `use <tenant-id>` | remember a tenant
|
|
59
|
+
| `use <tenant-id-or-slug>` | remember a tenant locally for `whoami`/future commands; a slug is server-verified |
|
|
58
60
|
| `connect` | connect Claude Code / Claude Desktop to your tenant (read access) |
|
|
59
61
|
| `tenants create <name>` | create a tenant (`--owner me\|<email>`, `--slug <slug>`) |
|
|
60
62
|
| `tenants list` | list tenants you provisioned |
|
|
63
|
+
| `tenants memberships` | list tenants you're a verified member of |
|
|
61
64
|
|
|
62
65
|
## Global options
|
|
63
66
|
|
package/dist/commands/tenants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InteractiveRequiredError, NotLoggedInError, UsageError } from '../errors.js';
|
|
2
2
|
import { resolveDpopKey } from '../install-key.js';
|
|
3
|
-
import {
|
|
3
|
+
import { claimEphemeralTenant, EPHEMERAL_LEASE_LABEL_MAX_LENGTH, EPHEMERAL_TENANT_TTL_SECONDS_MAX, EPHEMERAL_TENANT_TTL_SECONDS_MIN, releaseEphemeralTenant, } from '../lease-client.js';
|
|
4
|
+
import { createTenant, listTenantMemberships, listTenants } from '../tenants-client.js';
|
|
4
5
|
export async function runTenantsCreate(deps, args) {
|
|
5
6
|
const session = deps.session.read();
|
|
6
7
|
if (!session)
|
|
@@ -59,3 +60,76 @@ export async function runTenantsList(deps) {
|
|
|
59
60
|
}
|
|
60
61
|
deps.stdout.write(result.data.map((t) => `${t.slug}\t${t.id}\t${t.status}`).join('\n') + '\n');
|
|
61
62
|
}
|
|
63
|
+
export async function runTenantsMemberships(deps) {
|
|
64
|
+
const session = deps.session.read();
|
|
65
|
+
if (!session)
|
|
66
|
+
throw new NotLoggedInError();
|
|
67
|
+
const result = await listTenantMemberships(deps, {
|
|
68
|
+
apiBaseUrl: session.api_base_url,
|
|
69
|
+
accessToken: session.access_token,
|
|
70
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
71
|
+
});
|
|
72
|
+
if (deps.json) {
|
|
73
|
+
deps.stdout.write(JSON.stringify(result) + '\n');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (result.data.length === 0) {
|
|
77
|
+
deps.stdout.write('No tenant memberships found for your account.\n');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
deps.stdout.write(result.data.map((m) => `${m.slug}\t${m.id}\t${m.role}`).join('\n') + '\n');
|
|
81
|
+
}
|
|
82
|
+
function resolveParentTenantId(explicit, tenantConfig) {
|
|
83
|
+
return explicit ?? tenantConfig.read() ?? undefined;
|
|
84
|
+
}
|
|
85
|
+
const NO_PARENT_TENANT_MESSAGE = 'No tenant to operate on — pass --tenant <tenant-id>, or run `rakomi use <tenant-id>` first.';
|
|
86
|
+
export async function runTenantsClaim(deps, args) {
|
|
87
|
+
const session = deps.session.read();
|
|
88
|
+
if (!session)
|
|
89
|
+
throw new NotLoggedInError('Not logged in. Run `rakomi login --ci` first.');
|
|
90
|
+
const parentTenantId = resolveParentTenantId(args.parentTenantId, deps.tenantConfig);
|
|
91
|
+
if (!parentTenantId)
|
|
92
|
+
throw new UsageError(NO_PARENT_TENANT_MESSAGE);
|
|
93
|
+
if (args.ttlSeconds !== undefined &&
|
|
94
|
+
(args.ttlSeconds < EPHEMERAL_TENANT_TTL_SECONDS_MIN || args.ttlSeconds > EPHEMERAL_TENANT_TTL_SECONDS_MAX)) {
|
|
95
|
+
throw new UsageError(`--ttl-seconds must be between ${EPHEMERAL_TENANT_TTL_SECONDS_MIN} and ${EPHEMERAL_TENANT_TTL_SECONDS_MAX}, got ${args.ttlSeconds}`);
|
|
96
|
+
}
|
|
97
|
+
const label = args.label?.trim();
|
|
98
|
+
if (args.label !== undefined && (label === '' || (label?.length ?? 0) > EPHEMERAL_LEASE_LABEL_MAX_LENGTH)) {
|
|
99
|
+
throw new UsageError(`--label must be 1-${EPHEMERAL_LEASE_LABEL_MAX_LENGTH} characters after trimming, got "${args.label}"`);
|
|
100
|
+
}
|
|
101
|
+
const result = await claimEphemeralTenant(deps, {
|
|
102
|
+
apiBaseUrl: session.api_base_url,
|
|
103
|
+
accessToken: session.access_token,
|
|
104
|
+
parentTenantId,
|
|
105
|
+
ttlSeconds: args.ttlSeconds,
|
|
106
|
+
label,
|
|
107
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
108
|
+
});
|
|
109
|
+
if (deps.json) {
|
|
110
|
+
deps.stdout.write(JSON.stringify(result) + '\n');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
deps.stdout.write(`Claimed ephemeral tenant "${result.slug}" (${result.tenant_id}) — expires ${result.expires_at} (source: ${result.source})\n` +
|
|
114
|
+
`Lease id: ${result.lease_id} — pass this to \`rakomi tenants release --ci\` when you're done with it.\n`);
|
|
115
|
+
}
|
|
116
|
+
export async function runTenantsRelease(deps, args) {
|
|
117
|
+
const session = deps.session.read();
|
|
118
|
+
if (!session)
|
|
119
|
+
throw new NotLoggedInError('Not logged in. Run `rakomi login --ci` first.');
|
|
120
|
+
const parentTenantId = resolveParentTenantId(args.parentTenantId, deps.tenantConfig);
|
|
121
|
+
if (!parentTenantId)
|
|
122
|
+
throw new UsageError(NO_PARENT_TENANT_MESSAGE);
|
|
123
|
+
await releaseEphemeralTenant(deps, {
|
|
124
|
+
apiBaseUrl: session.api_base_url,
|
|
125
|
+
accessToken: session.access_token,
|
|
126
|
+
parentTenantId,
|
|
127
|
+
tenantId: args.tenantId,
|
|
128
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
129
|
+
});
|
|
130
|
+
if (deps.json) {
|
|
131
|
+
deps.stdout.write(JSON.stringify({ released: true, tenant_id: args.tenantId }) + '\n');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
deps.stdout.write(`Released ephemeral tenant ${args.tenantId} — deletion scheduled.\n`);
|
|
135
|
+
}
|
package/dist/commands/use.js
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { isValidTenantId } from '../env.js';
|
|
2
|
-
import { UsageError } from '../errors.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { NotLoggedInError, UsageError } from '../errors.js';
|
|
3
|
+
import { resolveDpopKey } from '../install-key.js';
|
|
4
|
+
import { getTenantMembershipBySlug } from '../tenants-client.js';
|
|
5
|
+
export async function runUse(deps, args) {
|
|
6
|
+
if (isValidTenantId(args.tenantId)) {
|
|
7
|
+
deps.tenantConfig.write(args.tenantId);
|
|
8
|
+
deps.stdout.write(`Active tenant set to ${args.tenantId}.\n`);
|
|
9
|
+
return;
|
|
7
10
|
}
|
|
8
|
-
deps.
|
|
9
|
-
|
|
11
|
+
const session = deps.session.read();
|
|
12
|
+
if (!session)
|
|
13
|
+
throw new NotLoggedInError();
|
|
14
|
+
const membership = await getTenantMembershipBySlug(deps, {
|
|
15
|
+
apiBaseUrl: session.api_base_url,
|
|
16
|
+
accessToken: session.access_token,
|
|
17
|
+
slug: args.tenantId,
|
|
18
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
19
|
+
});
|
|
20
|
+
if (!membership) {
|
|
21
|
+
throw new UsageError(`No tenant membership found for "${args.tenantId}" — check the spelling, or ask a tenant ` +
|
|
22
|
+
'admin to invite you. (Run `rakomi tenants memberships` to see the tenants you already belong to.)');
|
|
23
|
+
}
|
|
24
|
+
deps.tenantConfig.write(membership.id);
|
|
25
|
+
deps.stdout.write(`Active tenant set to ${membership.slug} (${membership.id}) — role: ${membership.role}.\n`);
|
|
10
26
|
}
|
package/dist/env.js
CHANGED
|
@@ -3,6 +3,7 @@ export const DEFAULT_API_BASE_URL = 'https://api.rakomi.com';
|
|
|
3
3
|
export const DEFAULT_ACCOUNTS_BASE_URL = 'https://accounts.rakomi.com';
|
|
4
4
|
export const DEFAULT_MCP_URL = 'https://mcp.rakomi.com/mcp';
|
|
5
5
|
export const DEFAULT_CIMD_CLIENT_ID_URL = 'https://api.rakomi.com/.well-known/rakomi-cli/client-metadata.json';
|
|
6
|
+
export const DEFAULT_PLATFORM_TENANT_ID = '01ef847e-4c40-4960-af05-11112c4764b2';
|
|
6
7
|
export const CI_FEDERATION_SESSION_CLIENT_ID = 'oidc-federation';
|
|
7
8
|
export function isCimdClientId(id) {
|
|
8
9
|
let url;
|
|
@@ -36,7 +37,10 @@ export function isValidTenantId(id) {
|
|
|
36
37
|
}
|
|
37
38
|
export function platformTenantId(env) {
|
|
38
39
|
const raw = env.RAKOMI_PLATFORM_TENANT_ID;
|
|
39
|
-
|
|
40
|
+
if (raw)
|
|
41
|
+
return isValidTenantId(raw) ? raw : undefined;
|
|
42
|
+
const isUnoverriddenProdHost = apiBaseUrl(env) === DEFAULT_API_BASE_URL && accountsBaseUrl(env) === DEFAULT_ACCOUNTS_BASE_URL;
|
|
43
|
+
return isUnoverriddenProdHost ? DEFAULT_PLATFORM_TENANT_ID : undefined;
|
|
40
44
|
}
|
|
41
45
|
const CLIENT_ID_METADATA_DOCUMENT_SUPPORTED_FIELD = 'client_id_metadata_document_supported';
|
|
42
46
|
export async function isCimdPlatformSupported(deps, apiUrl) {
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { runConnect } from './commands/connect.js';
|
|
|
9
9
|
import { runLogin, runLoginCi } from './commands/login.js';
|
|
10
10
|
import { runLogout } from './commands/logout.js';
|
|
11
11
|
import { runMcpToolsCi } from './commands/mcp.js';
|
|
12
|
-
import { runTenantsCreate, runTenantsList } from './commands/tenants.js';
|
|
12
|
+
import { runTenantsClaim, runTenantsCreate, runTenantsList, runTenantsMemberships, runTenantsRelease } from './commands/tenants.js';
|
|
13
13
|
import { runUse } from './commands/use.js';
|
|
14
14
|
import { runWhoami } from './commands/whoami.js';
|
|
15
15
|
import { apiBaseUrl, DEFAULT_MCP_URL } from './env.js';
|
|
@@ -33,6 +33,8 @@ const GLOBAL_OPTIONS = {
|
|
|
33
33
|
slug: { type: 'string' },
|
|
34
34
|
'tenant-id': { type: 'string' },
|
|
35
35
|
tenant: { type: 'string' },
|
|
36
|
+
'ttl-seconds': { type: 'string' },
|
|
37
|
+
label: { type: 'string' },
|
|
36
38
|
'cimd-url': { type: 'string' },
|
|
37
39
|
status: { type: 'boolean' },
|
|
38
40
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -55,6 +57,15 @@ export async function run(args, deps) {
|
|
|
55
57
|
return EXIT.FAIL;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
60
|
+
function parseTtlSeconds(raw) {
|
|
61
|
+
if (typeof raw !== 'string')
|
|
62
|
+
return undefined;
|
|
63
|
+
const n = Number(raw);
|
|
64
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
65
|
+
throw new UsageError(`--ttl-seconds must be a positive integer, got "${raw}"`);
|
|
66
|
+
}
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
58
69
|
async function dispatch(args, deps) {
|
|
59
70
|
let values;
|
|
60
71
|
let positionals;
|
|
@@ -127,8 +138,8 @@ async function dispatch(args, deps) {
|
|
|
127
138
|
case 'use': {
|
|
128
139
|
const tenantId = rest[0];
|
|
129
140
|
if (!tenantId)
|
|
130
|
-
throw new UsageError('Usage: rakomi use <tenant-id>');
|
|
131
|
-
runUse({ tenantConfig: deps.tenantConfig, stdout: deps.stdout }, { tenantId });
|
|
141
|
+
throw new UsageError('Usage: rakomi use <tenant-id-or-slug>');
|
|
142
|
+
await runUse({ ...httpDeps, session: deps.session, keys: deps.keys, tenantConfig: deps.tenantConfig, stdout: deps.stdout }, { tenantId });
|
|
132
143
|
return EXIT.OK;
|
|
133
144
|
}
|
|
134
145
|
case 'connect':
|
|
@@ -183,7 +194,32 @@ async function dispatch(args, deps) {
|
|
|
183
194
|
await runTenantsList({ ...httpDeps, session: deps.session, keys: deps.keys, json, stdout: deps.stdout });
|
|
184
195
|
return EXIT.OK;
|
|
185
196
|
}
|
|
186
|
-
|
|
197
|
+
if (sub === 'memberships') {
|
|
198
|
+
await runTenantsMemberships({ ...httpDeps, session: deps.session, keys: deps.keys, json, stdout: deps.stdout });
|
|
199
|
+
return EXIT.OK;
|
|
200
|
+
}
|
|
201
|
+
if (sub === 'claim') {
|
|
202
|
+
if (values.ci !== true) {
|
|
203
|
+
throw new UsageError('rakomi tenants claim requires --ci (no interactive form exists yet).');
|
|
204
|
+
}
|
|
205
|
+
await runTenantsClaim({ ...httpDeps, session: deps.session, keys: deps.keys, tenantConfig: deps.tenantConfig, json, stdout: deps.stdout }, {
|
|
206
|
+
parentTenantId: typeof values.tenant === 'string' ? values.tenant : undefined,
|
|
207
|
+
ttlSeconds: parseTtlSeconds(values['ttl-seconds']),
|
|
208
|
+
label: typeof values.label === 'string' ? values.label : undefined,
|
|
209
|
+
});
|
|
210
|
+
return EXIT.OK;
|
|
211
|
+
}
|
|
212
|
+
if (sub === 'release') {
|
|
213
|
+
if (values.ci !== true) {
|
|
214
|
+
throw new UsageError('rakomi tenants release requires --ci (no interactive form exists yet).');
|
|
215
|
+
}
|
|
216
|
+
const tenantId = tenantArgs[0];
|
|
217
|
+
if (!tenantId)
|
|
218
|
+
throw new UsageError('Usage: rakomi tenants release --ci <tenant-id> [--tenant <parent-tenant-id>]');
|
|
219
|
+
await runTenantsRelease({ ...httpDeps, session: deps.session, keys: deps.keys, tenantConfig: deps.tenantConfig, json, stdout: deps.stdout }, { tenantId, parentTenantId: typeof values.tenant === 'string' ? values.tenant : undefined });
|
|
220
|
+
return EXIT.OK;
|
|
221
|
+
}
|
|
222
|
+
throw new UsageError('Usage: rakomi tenants <create|list|claim|release> ...');
|
|
187
223
|
}
|
|
188
224
|
default:
|
|
189
225
|
throw new UsageError(`Unknown command "${command ?? ''}".`);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { CliError, EXIT } from './errors.js';
|
|
3
|
+
import { describeError, errorCode, request } from './http.js';
|
|
4
|
+
export const EPHEMERAL_TENANT_TTL_SECONDS_MIN = 600;
|
|
5
|
+
export const EPHEMERAL_TENANT_TTL_SECONDS_MAX = 1_209_600;
|
|
6
|
+
export const EPHEMERAL_LEASE_LABEL_MAX_LENGTH = 200;
|
|
7
|
+
function generateIdempotencyKey() {
|
|
8
|
+
return randomBytes(16).toString('hex');
|
|
9
|
+
}
|
|
10
|
+
const CHILD_TENANT_LIMIT_REACHED_CODE = 'tenant/child_tenant_limit_reached';
|
|
11
|
+
export async function claimEphemeralTenant(deps, opts) {
|
|
12
|
+
const result = await request(deps, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
url: `${opts.apiBaseUrl}/v1/tenants/${encodeURIComponent(opts.parentTenantId)}/ephemeral/claims`,
|
|
15
|
+
headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
|
|
16
|
+
body: {
|
|
17
|
+
...(opts.ttlSeconds !== undefined ? { ttl_seconds: opts.ttlSeconds } : {}),
|
|
18
|
+
...(opts.label ? { label: opts.label } : {}),
|
|
19
|
+
},
|
|
20
|
+
idempotencyKey: opts.idempotencyKey ?? generateIdempotencyKey(),
|
|
21
|
+
dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
|
|
22
|
+
});
|
|
23
|
+
if (result.status === 401) {
|
|
24
|
+
throw new CliError('Your session has expired. Run `rakomi login --ci` again.', EXIT.NOT_LOGGED_IN);
|
|
25
|
+
}
|
|
26
|
+
if (result.status === 403) {
|
|
27
|
+
throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)} ` +
|
|
28
|
+
"(missing the `tenants:lease` scope — check this CI identity's trust policy).", EXIT.FAIL);
|
|
29
|
+
}
|
|
30
|
+
if (result.status === 404) {
|
|
31
|
+
throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)} ` +
|
|
32
|
+
"(the given tenant is not the caller's own tenant, or the caller's own tenant is itself " +
|
|
33
|
+
'ephemeral — ephemeral tenants cannot nest).', EXIT.FAIL);
|
|
34
|
+
}
|
|
35
|
+
if (result.status === 409 && errorCode(result.body) === CHILD_TENANT_LIMIT_REACHED_CODE) {
|
|
36
|
+
throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)}.`, EXIT.FAIL);
|
|
37
|
+
}
|
|
38
|
+
if (result.status !== 201) {
|
|
39
|
+
throw new CliError(`Could not claim an ephemeral tenant: ${describeError(result.body, result.status)}`, EXIT.FAIL);
|
|
40
|
+
}
|
|
41
|
+
return result.body;
|
|
42
|
+
}
|
|
43
|
+
export async function releaseEphemeralTenant(deps, opts) {
|
|
44
|
+
const result = await request(deps, {
|
|
45
|
+
method: 'DELETE',
|
|
46
|
+
url: `${opts.apiBaseUrl}/v1/tenants/${encodeURIComponent(opts.parentTenantId)}/ephemeral/claims/${encodeURIComponent(opts.tenantId)}`,
|
|
47
|
+
headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
|
|
48
|
+
dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
|
|
49
|
+
});
|
|
50
|
+
if (result.status === 401) {
|
|
51
|
+
throw new CliError('Your session has expired. Run `rakomi login --ci` again.', EXIT.NOT_LOGGED_IN);
|
|
52
|
+
}
|
|
53
|
+
if (result.status === 403) {
|
|
54
|
+
throw new CliError(`Could not release the ephemeral tenant: ${describeError(result.body, result.status)} ` +
|
|
55
|
+
"(missing the `tenants:lease` scope — check this CI identity's trust policy).", EXIT.FAIL);
|
|
56
|
+
}
|
|
57
|
+
if (result.status === 404) {
|
|
58
|
+
throw new CliError(`Could not release the ephemeral tenant: ${describeError(result.body, result.status)} ` +
|
|
59
|
+
'(the given tenant id does not exist under the given parent, or is not an ephemeral tenant).', EXIT.FAIL);
|
|
60
|
+
}
|
|
61
|
+
if (result.status !== 204) {
|
|
62
|
+
throw new CliError(`Could not release the ephemeral tenant: ${describeError(result.body, result.status)}`, EXIT.FAIL);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/tenants-client.js
CHANGED
|
@@ -64,3 +64,46 @@ export async function listTenants(deps, opts) {
|
|
|
64
64
|
}
|
|
65
65
|
return result.body;
|
|
66
66
|
}
|
|
67
|
+
function describeCallerUnverified(action) {
|
|
68
|
+
return (`Could not ${action}: your account e-mail is not verified (or is disabled). Verify your e-mail ` +
|
|
69
|
+
'and sign in again.');
|
|
70
|
+
}
|
|
71
|
+
export async function listTenantMemberships(deps, opts) {
|
|
72
|
+
const result = await request(deps, {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
url: `${opts.apiBaseUrl}/v1/tenants/memberships`,
|
|
75
|
+
headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
|
|
76
|
+
dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
|
|
77
|
+
});
|
|
78
|
+
if (result.status === 401) {
|
|
79
|
+
throw new CliError('Your session has expired. Run `rakomi login` again.', EXIT.NOT_LOGGED_IN);
|
|
80
|
+
}
|
|
81
|
+
if (result.status === 403) {
|
|
82
|
+
throw new CliError(describeCallerUnverified('list your tenant memberships'), EXIT.FAIL);
|
|
83
|
+
}
|
|
84
|
+
if (result.status !== 200) {
|
|
85
|
+
throw new CliError(`Could not list your tenant memberships: ${describeError(result.body, result.status)}`, EXIT.FAIL);
|
|
86
|
+
}
|
|
87
|
+
return result.body;
|
|
88
|
+
}
|
|
89
|
+
export async function getTenantMembershipBySlug(deps, opts) {
|
|
90
|
+
const result = await request(deps, {
|
|
91
|
+
method: 'GET',
|
|
92
|
+
url: `${opts.apiBaseUrl}/v1/tenants/memberships/${encodeURIComponent(opts.slug)}`,
|
|
93
|
+
headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
|
|
94
|
+
dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
|
|
95
|
+
});
|
|
96
|
+
if (result.status === 401) {
|
|
97
|
+
throw new CliError('Your session has expired. Run `rakomi login` again.', EXIT.NOT_LOGGED_IN);
|
|
98
|
+
}
|
|
99
|
+
if (result.status === 404) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (result.status === 403) {
|
|
103
|
+
throw new CliError(describeCallerUnverified('look up this tenant membership'), EXIT.FAIL);
|
|
104
|
+
}
|
|
105
|
+
if (result.status !== 200) {
|
|
106
|
+
throw new CliError(`Could not look up tenant "${opts.slug}": ${describeError(result.body, result.status)}`, EXIT.FAIL);
|
|
107
|
+
}
|
|
108
|
+
return result.body;
|
|
109
|
+
}
|
package/dist/usage.js
CHANGED
|
@@ -12,7 +12,7 @@ export function helpText() {
|
|
|
12
12
|
' --tenant-id <uuid> the tenant to authenticate against (or set RAKOMI_PLATFORM_TENANT_ID)',
|
|
13
13
|
' logout clear the local session',
|
|
14
14
|
' whoami show the signed-in account, home tenant, and active tenant',
|
|
15
|
-
' use <tenant-id>
|
|
15
|
+
' use <tenant-id-or-slug> remember a tenant locally (whoami/future commands); a UUID is stored as-is, a slug is server-verified',
|
|
16
16
|
' connect connect Claude Code / Claude Desktop to your tenant (read access)',
|
|
17
17
|
' --write also request write access for the connected client',
|
|
18
18
|
' --client <name> claude-code | claude-desktop (required if more than one applies)',
|
|
@@ -23,6 +23,13 @@ export function helpText() {
|
|
|
23
23
|
' --owner <me|email> who becomes the new tenant\'s owner (default: me)',
|
|
24
24
|
' --slug <slug> optional; auto-derived + suffixed if omitted',
|
|
25
25
|
' tenants list list tenants you provisioned',
|
|
26
|
+
' tenants memberships list tenants your account is a verified member of',
|
|
27
|
+
' tenants claim --ci claim a temporary, self-expiring tenant under your own tenant',
|
|
28
|
+
' --tenant <id> your own tenant to claim under (or run `rakomi use <tenant-id>` first)',
|
|
29
|
+
' --ttl-seconds <n> how long the claim should live, in seconds (only for a freshly-created tenant)',
|
|
30
|
+
' --label <text> optional label, stored and echoed back, never interpreted',
|
|
31
|
+
' tenants release --ci <tenant-id> release a tenant claimed by `tenants claim`',
|
|
32
|
+
' --tenant <id> the tenant it was claimed under (or run `rakomi use <tenant-id>` first)',
|
|
26
33
|
' mcp tools --ci list MCP read tools your CI identity can invoke (no login needed first)',
|
|
27
34
|
'',
|
|
28
35
|
'Global options:',
|
|
@@ -31,7 +38,7 @@ export function helpText() {
|
|
|
31
38
|
' --dry-run print what would happen, make no writes or mutating calls',
|
|
32
39
|
' --no-browser use the RFC 8628 device-code flow instead of opening a browser',
|
|
33
40
|
' --no-keychain store the session in a 0600 file instead of the OS keychain',
|
|
34
|
-
' --tenant <id>
|
|
41
|
+
' --tenant <id> the tenant for this invocation (whoami, tenants claim/release)',
|
|
35
42
|
' -h, --help show this help and exit',
|
|
36
43
|
' -V, --version print the version and exit',
|
|
37
44
|
'',
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
},
|
|
27
27
|
"component": {
|
|
28
28
|
"type": "library",
|
|
29
|
-
"bom-ref": "pkg:npm/rakomi@0.
|
|
29
|
+
"bom-ref": "pkg:npm/rakomi@0.3.0",
|
|
30
30
|
"name": "rakomi",
|
|
31
|
-
"version": "0.
|
|
32
|
-
"purl": "pkg:npm/rakomi@0.
|
|
31
|
+
"version": "0.3.0",
|
|
32
|
+
"purl": "pkg:npm/rakomi@0.3.0",
|
|
33
33
|
"licenses": [
|
|
34
34
|
{
|
|
35
35
|
"license": {
|