@di-framework/tenant-cli 0.0.0-stage → 6.0.10
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 +143 -2
- package/api/v1/openapi.yaml +1 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +419 -0
- package/dist/client/index.d.ts +135 -0
- package/dist/client/index.js +131 -0
- package/dist/client/schema.d.ts +1213 -0
- package/dist/client/sse.d.ts +12 -0
- package/dist/client/sse.js +61 -0
- package/dist/credentials.d.ts +42 -0
- package/dist/credentials.js +65 -0
- package/dist/init.d.ts +17 -0
- package/dist/init.js +41 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +25 -0
- package/dist/oidc.d.ts +54 -0
- package/dist/oidc.js +110 -0
- package/package.json +46 -4
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** One server-sent event, as the `text/event-stream` format frames it. */
|
|
2
|
+
export interface SseEvent {
|
|
3
|
+
event?: string;
|
|
4
|
+
data: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parses a `text/event-stream` body into events. Comment lines (`:`) are skipped, `data:` lines
|
|
9
|
+
* of one event join with newlines, and a blank line dispatches. The hand-written part of the
|
|
10
|
+
* client: OpenAPI describes the streaming operations but not their framing.
|
|
11
|
+
*/
|
|
12
|
+
export declare function events(body: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a `text/event-stream` body into events. Comment lines (`:`) are skipped, `data:` lines
|
|
3
|
+
* of one event join with newlines, and a blank line dispatches. The hand-written part of the
|
|
4
|
+
* client: OpenAPI describes the streaming operations but not their framing.
|
|
5
|
+
*/
|
|
6
|
+
export async function* events(body) {
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
let buffer = '';
|
|
9
|
+
let pending = { data: [] };
|
|
10
|
+
const flush = () => {
|
|
11
|
+
if (pending.data.length === 0 && pending.event === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const done = { data: pending.data.join('\n') };
|
|
14
|
+
if (pending.event !== undefined)
|
|
15
|
+
done.event = pending.event;
|
|
16
|
+
if (pending.id !== undefined)
|
|
17
|
+
done.id = pending.id;
|
|
18
|
+
pending = { data: [] };
|
|
19
|
+
return done;
|
|
20
|
+
};
|
|
21
|
+
const take = (line) => {
|
|
22
|
+
if (line === '')
|
|
23
|
+
return flush();
|
|
24
|
+
if (line.startsWith(':'))
|
|
25
|
+
return undefined;
|
|
26
|
+
const colon = line.indexOf(':');
|
|
27
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
28
|
+
let value = colon === -1 ? '' : line.slice(colon + 1);
|
|
29
|
+
if (value.startsWith(' '))
|
|
30
|
+
value = value.slice(1);
|
|
31
|
+
if (field === 'event')
|
|
32
|
+
pending.event = value;
|
|
33
|
+
else if (field === 'data')
|
|
34
|
+
pending.data.push(value);
|
|
35
|
+
else if (field === 'id')
|
|
36
|
+
pending.id = value;
|
|
37
|
+
return undefined;
|
|
38
|
+
};
|
|
39
|
+
const reader = body.getReader();
|
|
40
|
+
for (;;) {
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done)
|
|
43
|
+
break;
|
|
44
|
+
buffer += decoder.decode(value, { stream: true });
|
|
45
|
+
let newline = buffer.indexOf('\n');
|
|
46
|
+
while (newline !== -1) {
|
|
47
|
+
const line = buffer.slice(0, newline).replace(/\r$/, '');
|
|
48
|
+
buffer = buffer.slice(newline + 1);
|
|
49
|
+
const event = take(line);
|
|
50
|
+
if (event)
|
|
51
|
+
yield event;
|
|
52
|
+
newline = buffer.indexOf('\n');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const tail = take(buffer.replace(/\r$/, ''));
|
|
56
|
+
if (tail)
|
|
57
|
+
yield tail;
|
|
58
|
+
const last = flush();
|
|
59
|
+
if (last)
|
|
60
|
+
yield last;
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One account's credential. The file and shape are the ones `platform/tenant-auth`'s CLI writes
|
|
3
|
+
* (`$DI_FRAMEWORK_HOME/credentials.json`), so a login made there seeds the pilot. The pilot
|
|
4
|
+
* reads `controller.url` and the token; it never writes a kubeconfig.
|
|
5
|
+
*/
|
|
6
|
+
export interface Credential {
|
|
7
|
+
account: string;
|
|
8
|
+
user: string;
|
|
9
|
+
role: string;
|
|
10
|
+
via: 'identity' | 'api-key';
|
|
11
|
+
accessToken?: string;
|
|
12
|
+
refreshToken?: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
/** Access token expiry; absent for API keys. */
|
|
15
|
+
expiresAt?: string;
|
|
16
|
+
/** Where an identity login refreshes and revokes: the issuer and the public client id. */
|
|
17
|
+
issuer?: string;
|
|
18
|
+
clientId?: string;
|
|
19
|
+
controller: {
|
|
20
|
+
url: string;
|
|
21
|
+
certificateAuthorityData?: string;
|
|
22
|
+
};
|
|
23
|
+
console?: string;
|
|
24
|
+
}
|
|
25
|
+
/** True when an identity token is gone or within a minute of expiring, so it must be refreshed. */
|
|
26
|
+
export declare function needsRefresh(credential: Credential, now?: number): boolean;
|
|
27
|
+
export type CredentialStore = Record<string, Credential>;
|
|
28
|
+
export declare class CliError extends Error {
|
|
29
|
+
readonly exitCode: number;
|
|
30
|
+
constructor(message: string, exitCode?: number);
|
|
31
|
+
}
|
|
32
|
+
export declare const home: (env?: NodeJS.ProcessEnv) => string;
|
|
33
|
+
export declare function readStore(env?: NodeJS.ProcessEnv): CredentialStore;
|
|
34
|
+
export declare function writeStore(store: CredentialStore, env?: NodeJS.ProcessEnv): void;
|
|
35
|
+
/** The bearer a credential presents: the API key, or the identity access token while it lasts. */
|
|
36
|
+
export declare function bearer(credential: Credential): string;
|
|
37
|
+
/**
|
|
38
|
+
* The account a command runs against: `--account`, then `DI_TENANT_ACCOUNT`, then the only
|
|
39
|
+
* credential on disk.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveAccount(flag: string | undefined, env: NodeJS.ProcessEnv, store: CredentialStore): string;
|
|
42
|
+
export declare function load(account: string, env?: NodeJS.ProcessEnv): Credential;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** True when an identity token is gone or within a minute of expiring, so it must be refreshed. */
|
|
5
|
+
export function needsRefresh(credential, now = Date.now()) {
|
|
6
|
+
if (credential.via !== 'identity' || !credential.refreshToken)
|
|
7
|
+
return false;
|
|
8
|
+
if (!credential.accessToken)
|
|
9
|
+
return true;
|
|
10
|
+
return credential.expiresAt !== undefined && Date.parse(credential.expiresAt) - now <= 60_000;
|
|
11
|
+
}
|
|
12
|
+
export class CliError extends Error {
|
|
13
|
+
exitCode;
|
|
14
|
+
constructor(message, exitCode = 1) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.exitCode = exitCode;
|
|
17
|
+
this.name = 'CliError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const home = (env = process.env) => env.DI_FRAMEWORK_HOME ?? join(homedir(), '.di-framework');
|
|
21
|
+
const credentialsPath = (env) => join(home(env), 'credentials.json');
|
|
22
|
+
export function readStore(env) {
|
|
23
|
+
const path = credentialsPath(env);
|
|
24
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
|
|
25
|
+
}
|
|
26
|
+
export function writeStore(store, env) {
|
|
27
|
+
const directory = home(env);
|
|
28
|
+
if (!existsSync(directory))
|
|
29
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
30
|
+
const path = credentialsPath(env);
|
|
31
|
+
writeFileSync(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
32
|
+
chmodSync(path, 0o600);
|
|
33
|
+
}
|
|
34
|
+
/** The bearer a credential presents: the API key, or the identity access token while it lasts. */
|
|
35
|
+
export function bearer(credential) {
|
|
36
|
+
if (credential.apiKey)
|
|
37
|
+
return credential.apiKey;
|
|
38
|
+
if (!credential.accessToken)
|
|
39
|
+
throw new CliError('the credential holds no token; log in again');
|
|
40
|
+
if (credential.expiresAt && Date.parse(credential.expiresAt) <= Date.now()) {
|
|
41
|
+
throw new CliError('the login has expired; run login again');
|
|
42
|
+
}
|
|
43
|
+
return credential.accessToken;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The account a command runs against: `--account`, then `DI_TENANT_ACCOUNT`, then the only
|
|
47
|
+
* credential on disk.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveAccount(flag, env, store) {
|
|
50
|
+
const named = flag ?? env.DI_TENANT_ACCOUNT;
|
|
51
|
+
if (named)
|
|
52
|
+
return named;
|
|
53
|
+
const accounts = Object.keys(store);
|
|
54
|
+
if (accounts.length === 1)
|
|
55
|
+
return accounts[0];
|
|
56
|
+
throw new CliError(accounts.length === 0
|
|
57
|
+
? 'not logged in; run: login --controller <url> --account <tenant>'
|
|
58
|
+
: `--account <tenant> is required; logged in to ${accounts.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
export function load(account, env) {
|
|
61
|
+
const credential = readStore(env)[account];
|
|
62
|
+
if (!credential)
|
|
63
|
+
throw new CliError(`not logged in to ${account}; run: login --account ${account}`);
|
|
64
|
+
return credential;
|
|
65
|
+
}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
/** Positional name, also the directory unless `dir` is given. */
|
|
3
|
+
name?: string;
|
|
4
|
+
dir?: string;
|
|
5
|
+
/** `--name`: the project name when it differs from the directory. */
|
|
6
|
+
projectName?: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
cwd: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The minimal di-framework seed: a package manifest, the component config, and one HTTP
|
|
12
|
+
* handler. It creates no platform resources; `services create` and `deploy` do that later.
|
|
13
|
+
*/
|
|
14
|
+
export declare function init(options: InitOptions): {
|
|
15
|
+
directory: string;
|
|
16
|
+
files: string[];
|
|
17
|
+
};
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { CliError } from "./credentials.js";
|
|
4
|
+
/**
|
|
5
|
+
* The minimal di-framework seed: a package manifest, the component config, and one HTTP
|
|
6
|
+
* handler. It creates no platform resources; `services create` and `deploy` do that later.
|
|
7
|
+
*/
|
|
8
|
+
export function init(options) {
|
|
9
|
+
const directory = resolve(options.cwd, options.dir ?? options.name ?? '.');
|
|
10
|
+
const name = options.projectName ?? options.name ?? basename(directory);
|
|
11
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
12
|
+
throw new CliError(`project name must be a lowercase DNS label, got ${JSON.stringify(name)}`);
|
|
13
|
+
}
|
|
14
|
+
const files = {
|
|
15
|
+
'package.json': `${JSON.stringify({
|
|
16
|
+
name,
|
|
17
|
+
private: true,
|
|
18
|
+
type: 'module',
|
|
19
|
+
scripts: { build: 'di-framework platform build', deploy: 'di-tenant deploy apply' },
|
|
20
|
+
dependencies: { '@di-framework/core': '^6.0.3', '@di-framework/http': '^6.0.3' },
|
|
21
|
+
}, null, 2)}\n`,
|
|
22
|
+
'di-framework.config.json': `${JSON.stringify({ name, entry: 'src/app.ts', output: `dist/${name}.wasm` }, null, 2)}\n`,
|
|
23
|
+
'src/app.ts': [
|
|
24
|
+
'/** One HTTP handler; the platform routes requests for this service here. */',
|
|
25
|
+
'export default function fetch(request: Request): Response {',
|
|
26
|
+
' return Response.json({ service: ' +
|
|
27
|
+
JSON.stringify(name) +
|
|
28
|
+
', path: new URL(request.url).pathname });',
|
|
29
|
+
'}',
|
|
30
|
+
'',
|
|
31
|
+
].join('\n'),
|
|
32
|
+
};
|
|
33
|
+
const existing = Object.keys(files).filter((file) => existsSync(join(directory, file)));
|
|
34
|
+
if (existing.length > 0 && !options.force) {
|
|
35
|
+
throw new CliError(`${existing.join(', ')} already exist in ${directory}; pass --force to overwrite`);
|
|
36
|
+
}
|
|
37
|
+
mkdirSync(join(directory, 'src'), { recursive: true });
|
|
38
|
+
for (const [file, content] of Object.entries(files))
|
|
39
|
+
writeFileSync(join(directory, file), content);
|
|
40
|
+
return { directory, files: Object.keys(files) };
|
|
41
|
+
}
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { run } from "./cli.js";
|
|
4
|
+
/** The platform's URL opener; a failure only means the printed URL has to be visited by hand. */
|
|
5
|
+
function openBrowser(url) {
|
|
6
|
+
const command = process.platform === 'darwin'
|
|
7
|
+
? ['open', url]
|
|
8
|
+
: process.platform === 'win32'
|
|
9
|
+
? ['cmd', '/c', 'start', '', url]
|
|
10
|
+
: ['xdg-open', url];
|
|
11
|
+
try {
|
|
12
|
+
Bun.spawn(command, { stdio: ['ignore', 'ignore', 'ignore'] }).unref();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Printed URL stays the fallback.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
process.exit(await run(process.argv.slice(2), {
|
|
19
|
+
stdout: (line) => console.log(line),
|
|
20
|
+
stderr: (line) => console.error(line),
|
|
21
|
+
stdin: () => readFileSync(0, 'utf8'),
|
|
22
|
+
open: openBrowser,
|
|
23
|
+
env: process.env,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
}));
|
package/dist/oidc.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity-server side of a login, for a public native client (RFC 8252): OpenID discovery,
|
|
3
|
+
* PKCE, the authorization URL, and the code, refresh, and revocation calls, all without a
|
|
4
|
+
* client secret. `client_id` alone identifies the CLI; the code verifier is what protects it.
|
|
5
|
+
*/
|
|
6
|
+
export interface ProviderMetadata {
|
|
7
|
+
issuer: string;
|
|
8
|
+
authorization_endpoint: string;
|
|
9
|
+
token_endpoint: string;
|
|
10
|
+
revocation_endpoint?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TokenResponse {
|
|
13
|
+
access_token: string;
|
|
14
|
+
token_type: string;
|
|
15
|
+
expires_in: number;
|
|
16
|
+
refresh_token?: string;
|
|
17
|
+
id_token?: string;
|
|
18
|
+
scope?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class OidcError extends Error {
|
|
21
|
+
readonly status?: number | undefined;
|
|
22
|
+
constructor(message: string, status?: number | undefined);
|
|
23
|
+
}
|
|
24
|
+
type Fetch = typeof fetch;
|
|
25
|
+
export declare function base64url(bytes: Uint8Array): string;
|
|
26
|
+
export declare function randomToken(bytes?: number): string;
|
|
27
|
+
export declare function pkce(verifier?: string): Promise<{
|
|
28
|
+
verifier: string;
|
|
29
|
+
challenge: string;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function discover(issuer: string, doFetch?: Fetch): Promise<ProviderMetadata>;
|
|
32
|
+
export interface AuthorizationRequest {
|
|
33
|
+
clientId: string;
|
|
34
|
+
redirectUri: string;
|
|
35
|
+
scope: string;
|
|
36
|
+
state: string;
|
|
37
|
+
nonce: string;
|
|
38
|
+
codeChallenge: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function authorizationUrl(metadata: ProviderMetadata, request: AuthorizationRequest): string;
|
|
41
|
+
export declare function exchangeCode(metadata: ProviderMetadata, exchange: {
|
|
42
|
+
clientId: string;
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
code: string;
|
|
45
|
+
codeVerifier: string;
|
|
46
|
+
}, doFetch?: Fetch): Promise<TokenResponse>;
|
|
47
|
+
/** Rotates the refresh token; identity-server revokes the family if the old one is replayed. */
|
|
48
|
+
export declare function refreshTokens(metadata: ProviderMetadata, exchange: {
|
|
49
|
+
clientId: string;
|
|
50
|
+
refreshToken: string;
|
|
51
|
+
}, doFetch?: Fetch): Promise<TokenResponse>;
|
|
52
|
+
/** RFC 7009; revoking the refresh token ends the whole authorization. Failures are ignored. */
|
|
53
|
+
export declare function revokeToken(metadata: ProviderMetadata, clientId: string, token: string, doFetch?: Fetch): Promise<void>;
|
|
54
|
+
export {};
|
package/dist/oidc.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity-server side of a login, for a public native client (RFC 8252): OpenID discovery,
|
|
3
|
+
* PKCE, the authorization URL, and the code, refresh, and revocation calls, all without a
|
|
4
|
+
* client secret. `client_id` alone identifies the CLI; the code verifier is what protects it.
|
|
5
|
+
*/
|
|
6
|
+
export class OidcError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(message, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.name = 'OidcError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function base64url(bytes) {
|
|
15
|
+
return Buffer.from(bytes).toString('base64url');
|
|
16
|
+
}
|
|
17
|
+
export function randomToken(bytes = 32) {
|
|
18
|
+
return base64url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
19
|
+
}
|
|
20
|
+
export async function pkce(verifier = randomToken(32)) {
|
|
21
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
22
|
+
return { verifier, challenge: base64url(new Uint8Array(digest)) };
|
|
23
|
+
}
|
|
24
|
+
export async function discover(issuer, doFetch = fetch) {
|
|
25
|
+
const url = `${issuer.replace(/\/$/, '')}/.well-known/openid-configuration`;
|
|
26
|
+
const response = await doFetch(url);
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new OidcError(`discovery at ${issuer} answered ${response.status}`, response.status);
|
|
29
|
+
const metadata = (await response.json());
|
|
30
|
+
for (const field of ['issuer', 'authorization_endpoint', 'token_endpoint']) {
|
|
31
|
+
if (!metadata[field])
|
|
32
|
+
throw new OidcError(`discovery at ${issuer} lacks ${field}`);
|
|
33
|
+
}
|
|
34
|
+
// The CLI only ever talks to the issuer it was pointed at: a document that names endpoints
|
|
35
|
+
// elsewhere would send the authorization code or refresh token to another host.
|
|
36
|
+
const origin = new URL(issuer).origin;
|
|
37
|
+
for (const field of [
|
|
38
|
+
'authorization_endpoint',
|
|
39
|
+
'token_endpoint',
|
|
40
|
+
'revocation_endpoint',
|
|
41
|
+
]) {
|
|
42
|
+
const endpoint = metadata[field];
|
|
43
|
+
if (endpoint && new URL(endpoint).origin !== origin)
|
|
44
|
+
throw new OidcError(`discovery at ${issuer}: ${field} is not under the issuer`);
|
|
45
|
+
}
|
|
46
|
+
return metadata;
|
|
47
|
+
}
|
|
48
|
+
export function authorizationUrl(metadata, request) {
|
|
49
|
+
const url = new URL(metadata.authorization_endpoint);
|
|
50
|
+
url.search = new URLSearchParams({
|
|
51
|
+
response_type: 'code',
|
|
52
|
+
client_id: request.clientId,
|
|
53
|
+
redirect_uri: request.redirectUri,
|
|
54
|
+
scope: request.scope,
|
|
55
|
+
state: request.state,
|
|
56
|
+
nonce: request.nonce,
|
|
57
|
+
code_challenge: request.codeChallenge,
|
|
58
|
+
code_challenge_method: 'S256',
|
|
59
|
+
}).toString();
|
|
60
|
+
return url.toString();
|
|
61
|
+
}
|
|
62
|
+
async function tokenRequest(endpoint, form, doFetch) {
|
|
63
|
+
const response = await doFetch(endpoint, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
66
|
+
body: new URLSearchParams(form),
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
let code = `${response.status}`;
|
|
70
|
+
try {
|
|
71
|
+
code = String((await response.json()).error ?? code);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// No OAuth error body; the status is the message.
|
|
75
|
+
}
|
|
76
|
+
throw new OidcError(`token endpoint refused the ${form.grant_type} grant: ${code}`, response.status);
|
|
77
|
+
}
|
|
78
|
+
return (await response.json());
|
|
79
|
+
}
|
|
80
|
+
export function exchangeCode(metadata, exchange, doFetch = fetch) {
|
|
81
|
+
return tokenRequest(metadata.token_endpoint, {
|
|
82
|
+
grant_type: 'authorization_code',
|
|
83
|
+
client_id: exchange.clientId,
|
|
84
|
+
code: exchange.code,
|
|
85
|
+
redirect_uri: exchange.redirectUri,
|
|
86
|
+
code_verifier: exchange.codeVerifier,
|
|
87
|
+
}, doFetch);
|
|
88
|
+
}
|
|
89
|
+
/** Rotates the refresh token; identity-server revokes the family if the old one is replayed. */
|
|
90
|
+
export function refreshTokens(metadata, exchange, doFetch = fetch) {
|
|
91
|
+
return tokenRequest(metadata.token_endpoint, {
|
|
92
|
+
grant_type: 'refresh_token',
|
|
93
|
+
client_id: exchange.clientId,
|
|
94
|
+
refresh_token: exchange.refreshToken,
|
|
95
|
+
}, doFetch);
|
|
96
|
+
}
|
|
97
|
+
/** RFC 7009; revoking the refresh token ends the whole authorization. Failures are ignored. */
|
|
98
|
+
export async function revokeToken(metadata, clientId, token, doFetch = fetch) {
|
|
99
|
+
const endpoint = metadata.revocation_endpoint ?? `${metadata.issuer}/oauth2/revoke`;
|
|
100
|
+
try {
|
|
101
|
+
await doFetch(endpoint, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
104
|
+
body: new URLSearchParams({ token, client_id: clientId }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// The credential is forgotten locally either way; the token expires on its own.
|
|
109
|
+
}
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@di-framework/tenant-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "6.0.10",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "(MIT OR Apache-2.0)",
|
|
7
|
+
"description": "Pilot tenant CLI: talks only to the tenant controller's OpenAPI contract with an identity-server credential; never to Kubernetes.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/di-framework/platform",
|
|
11
|
+
"directory": "platform/tenant-cli"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/di-framework/platform",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"api/v1/openapi.yaml",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
"./client": {
|
|
21
|
+
"types": "./dist/client/index.d.ts",
|
|
22
|
+
"bun": "./dist/client/index.js",
|
|
23
|
+
"import": "./dist/client/index.js",
|
|
24
|
+
"default": "./dist/client/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./src/api/*": "./src/api/*",
|
|
27
|
+
"./tests/support/*": "./tests/support/*",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"di-tenant": "dist/main.js"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"cli": "bun src/main.ts",
|
|
35
|
+
"generate:api": "di-framework generate && bun scripts/generate-openapi.ts",
|
|
36
|
+
"generate:client": "openapi-typescript api/v1/openapi.yaml -o src/client/schema.d.ts",
|
|
37
|
+
"generate": "bun run generate:api && bun run generate:client",
|
|
38
|
+
"build": "rm -rf dist && bun x tsc -p tsconfig.build.json && cp src/client/schema.d.ts dist/client/schema.d.ts && bun ../../scripts/fix-esm-imports.ts dist",
|
|
39
|
+
"test": "bun test"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@di-framework/core": "^6.0.3",
|
|
43
|
+
"@di-framework/http": "^6.0.3"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"openapi-typescript": "7.13.0"
|
|
47
|
+
}
|
|
48
|
+
}
|