Add device-grant dev sign-in (CRD-driven) for local/agent development
AuthClient dev-console + AuthServer deviceFlow via k8s/auth-dev.yaml; otel server accepts its own URL as an extra audience (OTEL_OIDC_AUDIENCE). Vite dev-only device-flow sign-in, scripts/dev-auth.mjs helper, AGENTS.md notes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0fde8a0f92
commit
bd17837663
10 files changed
+305
-3
No files matched your search
@@ -171,3 +171,24 @@ Initial pass is done. Take **one view at a time** to the full screen pattern
|
|||||||
(FilterBar + card/aside layout, real buttons, fold or restyle the shared
|
(FilterBar + card/aside layout, real buttons, fold or restyle the shared
|
||||||
chrome for that screen), verify it in the browser in both themes, commit, then
|
chrome for that screen), verify it in the browser in both themes, commit, then
|
||||||
move to the next. Suggested order: Traces, Logs, Metrics, Search, Trends, Setup.
|
move to the next. Suggested order: Traces, Logs, Metrics, Search, Trends, Setup.
|
||||||
|
|
||||||
|
## Local / agent development without login blocking
|
||||||
|
|
||||||
|
Uses the go-auth device grant (RFC 8628); reference: `https://users.k6n.net/dev-auth.md`.
|
||||||
|
Config is CRD-driven (`https://users.k6n.net/crds.md`), see `k8s/auth-dev.yaml`:
|
||||||
|
an `AuthClient` `dev-console` with `spec.audience: [https://otel.k6n.net]`
|
||||||
|
(audience entries must be absolute URLs - `otel-debug` was rejected and briefly
|
||||||
|
made the AuthServer `READY False`), plus `AuthServer.spec.deviceFlow`. The otel
|
||||||
|
server accepts that audience via `OTEL_OIDC_AUDIENCE` (`k8s/deployment.yaml`).
|
||||||
|
|
||||||
|
- **Browser (`npm run dev`)**: `src/dev/device-auth.ts` is imported only under
|
||||||
|
`import.meta.env.DEV`. With no session it shows a "Dev sign-in" link + code,
|
||||||
|
polls, then reloads. `/dev-auth/*` is proxied by `vite.config.ts` to the
|
||||||
|
issuer's `/api/auth/*` (override the issuer with `AUTH_DEV_BASE`).
|
||||||
|
- **Terminal / agents**: `node scripts/dev-auth.mjs login|token|whoami|forget`.
|
||||||
|
Refresh token lives in `~/.go-auth/dev/` (0700/0600). Use
|
||||||
|
`curl -H "Authorization: Bearer $(node scripts/dev-auth.mjs token)" ...`.
|
||||||
|
`GO_AUTH_TOKEN` is an explicit-token escape hatch; never store it.
|
||||||
|
- Never commit tokens, never persist them in a production build, never log them.
|
||||||
|
- Apply with `kubectl apply --dry-run=server --server-side --field-manager=otel-dev-auth -f k8s/auth-dev.yaml`
|
||||||
|
first; check `kubectl get authserver -n ecommerce` stays `READY True`.
|
||||||
@@ -78,6 +78,10 @@ type OidcAuth struct {
|
|||||||
disabled bool
|
disabled bool
|
||||||
ingestKey string
|
ingestKey string
|
||||||
agentToken string
|
agentToken string
|
||||||
|
// audience is an optional extra accepted token audience: this service's
|
||||||
|
// own URL, which the issuer emits via AuthClient.spec.audience (e.g. for
|
||||||
|
// device-grant dev tokens minted for another client).
|
||||||
|
audience string
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
keys map[string]*rsa.PublicKey
|
keys map[string]*rsa.PublicKey
|
||||||
@@ -102,6 +106,7 @@ func NewOidcAuth(ingestKey string) *OidcAuth {
|
|||||||
disabled: disabled,
|
disabled: disabled,
|
||||||
ingestKey: ingestKey,
|
ingestKey: ingestKey,
|
||||||
agentToken: os.Getenv("OTEL_AGENT_TOKEN"),
|
agentToken: os.Getenv("OTEL_AGENT_TOKEN"),
|
||||||
|
audience: os.Getenv("OTEL_OIDC_AUDIENCE"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +244,7 @@ func (a *OidcAuth) VerifyToken(ctx context.Context, token string) (*IDClaims, er
|
|||||||
if claims.Iss != a.issuer {
|
if claims.Iss != a.issuer {
|
||||||
return nil, fmt.Errorf("unexpected issuer %q", claims.Iss)
|
return nil, fmt.Errorf("unexpected issuer %q", claims.Iss)
|
||||||
}
|
}
|
||||||
if !claims.Aud.Contains(a.client) {
|
if !claims.Aud.Contains(a.client) && (a.audience == "" || !claims.Aud.Contains(a.audience)) {
|
||||||
return nil, errors.New("token not issued for this client")
|
return nil, errors.New("token not issued for this client")
|
||||||
}
|
}
|
||||||
if claims.Exp != 0 && time.Now().Unix() > claims.Exp {
|
if claims.Exp != 0 && time.Now().Unix() > claims.Exp {
|
||||||
|
|||||||
@@ -103,6 +103,25 @@ func TestVerifyToken(t *testing.T) {
|
|||||||
t.Fatalf("expected wrong audience to fail")
|
t.Fatalf("expected wrong audience to fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extra audience: rejected unless configured, accepted once it is.
|
||||||
|
urlAudToken := mintToken("key-1", IDClaims{
|
||||||
|
Iss: "https://test.issuer",
|
||||||
|
Sub: "dev-user",
|
||||||
|
Aud: Audience{"dev-console", "https://svc.example"},
|
||||||
|
Exp: time.Now().Add(time.Hour).Unix(),
|
||||||
|
})
|
||||||
|
if _, err := auth.VerifyToken(context.Background(), urlAudToken); err == nil {
|
||||||
|
t.Fatalf("expected unconfigured extra audience to fail")
|
||||||
|
}
|
||||||
|
auth.audience = "https://svc.example"
|
||||||
|
if _, err := auth.VerifyToken(context.Background(), urlAudToken); err != nil {
|
||||||
|
t.Fatalf("expected configured audience to pass, got: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := auth.VerifyToken(context.Background(), wrongAudToken); err == nil {
|
||||||
|
t.Fatalf("configured audience must not admit other audiences")
|
||||||
|
}
|
||||||
|
auth.audience = ""
|
||||||
|
|
||||||
// Wrong issuer
|
// Wrong issuer
|
||||||
wrongIssToken := mintToken("key-1", IDClaims{
|
wrongIssToken := mintToken("key-1", IDClaims{
|
||||||
Iss: "https://wrong.issuer",
|
Iss: "https://wrong.issuer",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Local/agent development sign-in via the RFC 8628 device grant.
|
||||||
|
# Tokens minted for dev-console carry aud=[dev-console, https://otel.k6n.net], so the
|
||||||
|
# otel server (which requires aud to contain its own URL (OTEL_OIDC_AUDIENCE)) accepts them with no
|
||||||
|
# server-side change. Approval is still gated by the issuer's normal policy.
|
||||||
|
apiVersion: auth.k6n.net/v1alpha1
|
||||||
|
kind: AuthClient
|
||||||
|
metadata:
|
||||||
|
name: dev-console
|
||||||
|
namespace: ecommerce
|
||||||
|
labels:
|
||||||
|
auth.k6n.net/server: auth
|
||||||
|
spec:
|
||||||
|
clientID: dev-console
|
||||||
|
displayName: Dev console (device grant)
|
||||||
|
description: Public dev client for local and agent development via the device grant
|
||||||
|
redirectURIs: []
|
||||||
|
audience:
|
||||||
|
- https://otel.k6n.net
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
- offline_access
|
||||||
|
---
|
||||||
|
# Partial apply: only deviceFlow is owned by this field manager.
|
||||||
|
apiVersion: auth.k6n.net/v1alpha1
|
||||||
|
kind: AuthServer
|
||||||
|
metadata:
|
||||||
|
name: auth
|
||||||
|
namespace: ecommerce
|
||||||
|
spec:
|
||||||
|
deviceFlow:
|
||||||
|
clientID: dev-console
|
||||||
|
codeTTL: 10m
|
||||||
|
pollInterval: 5s
|
||||||
@@ -51,6 +51,10 @@ spec:
|
|||||||
value: "https://users.k6n.net"
|
value: "https://users.k6n.net"
|
||||||
- name: OTEL_OIDC_CLIENT_ID
|
- name: OTEL_OIDC_CLIENT_ID
|
||||||
value: "otel-debug"
|
value: "otel-debug"
|
||||||
|
# Also accept tokens whose aud carries this service's URL (device-grant
|
||||||
|
# dev tokens from the dev-console client, see k8s/auth-dev.yaml).
|
||||||
|
- name: OTEL_OIDC_AUDIENCE
|
||||||
|
value: "https://otel.k6n.net"
|
||||||
# Service credential for non-interactive debugging agents. It grants
|
# Service credential for non-interactive debugging agents. It grants
|
||||||
# access to protected /api debug endpoints, not OTLP ingest.
|
# access to protected /api debug endpoints, not OTLP ingest.
|
||||||
- name: OTEL_AGENT_TOKEN
|
- name: OTEL_AGENT_TOKEN
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Development-only sign-in via the issuer's device grant (RFC 8628), so local
|
||||||
|
// runs are not blocked by the redirect login. Imported only when
|
||||||
|
// import.meta.env.DEV; never ships in a production build.
|
||||||
|
//
|
||||||
|
// Flow: POST /dev-auth/device -> show verification link + code -> poll
|
||||||
|
// /dev-auth/device/token. /dev-auth is proxied to the issuer's /api/auth by
|
||||||
|
// vite.config.ts. Tokens land in the normal store so authFetch just works;
|
||||||
|
// refresh goes through /dev-auth/device/refresh.
|
||||||
|
import { hasSession, saveTokens, setRefreshOverride, type TokenResponse } from "../lib/auth";
|
||||||
|
|
||||||
|
const CLIENT_ID =
|
||||||
|
(import.meta.env.VITE_DEV_AUTH_CLIENT_ID as string | undefined) ?? "dev-console";
|
||||||
|
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
||||||
|
|
||||||
|
interface DeviceStart {
|
||||||
|
device_code: string;
|
||||||
|
user_code: string;
|
||||||
|
verification_uri: string;
|
||||||
|
verification_uri_complete?: string;
|
||||||
|
expires_in: number;
|
||||||
|
interval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = (path: string, body: Record<string, string>) =>
|
||||||
|
fetch(`/dev-auth${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refresh(refreshToken: string): Promise<TokenResponse | null> {
|
||||||
|
const res = await post("/device/refresh", { client_id: CLIENT_ID, refresh_token: refreshToken });
|
||||||
|
return res.ok ? ((await res.json()) as TokenResponse) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esc = (v: string) => v.replace(/[<>&"]/g, "");
|
||||||
|
|
||||||
|
function banner() {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.setAttribute("role", "status");
|
||||||
|
el.style.cssText =
|
||||||
|
"position:fixed;right:16px;bottom:16px;z-index:9999;max-width:340px;padding:12px 14px;border-radius:12px;" +
|
||||||
|
"border:1px solid var(--ds-line-strong);background:var(--ds-surface);color:var(--ds-text);" +
|
||||||
|
"font:13px/1.4 var(--font-ui);";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return {
|
||||||
|
set(html: string | null) {
|
||||||
|
if (html === null) el.remove();
|
||||||
|
else el.innerHTML = html;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startDevAuth(): Promise<void> {
|
||||||
|
setRefreshOverride(refresh);
|
||||||
|
if (hasSession()) return;
|
||||||
|
|
||||||
|
const ui = banner();
|
||||||
|
try {
|
||||||
|
const res = await post("/device", { client_id: CLIENT_ID });
|
||||||
|
if (!res.ok) throw new Error(`device start failed: HTTP ${res.status}`);
|
||||||
|
const d = (await res.json()) as DeviceStart;
|
||||||
|
const link = esc(d.verification_uri_complete ?? d.verification_uri);
|
||||||
|
ui.set(
|
||||||
|
`<strong>Dev sign-in</strong><br>Approve in your browser: ` +
|
||||||
|
`<a href="${link}" target="_blank" rel="noreferrer" style="color:var(--ds-accent)">${link}</a>` +
|
||||||
|
`<br>Code <code style="font-family:var(--font-data)">${esc(d.user_code)}</code>`,
|
||||||
|
);
|
||||||
|
console.info(`[dev-auth] approve at ${link} (code ${d.user_code})`);
|
||||||
|
|
||||||
|
let interval = (d.interval ?? 5) * 1000;
|
||||||
|
const deadline = Date.now() + d.expires_in * 1000;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await new Promise((r) => setTimeout(r, interval));
|
||||||
|
const t = await post("/device/token", {
|
||||||
|
grant_type: DEVICE_GRANT,
|
||||||
|
device_code: d.device_code,
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
});
|
||||||
|
if (t.ok) {
|
||||||
|
saveTokens((await t.json()) as TokenResponse);
|
||||||
|
ui.set(null);
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const err = ((await t.json().catch(() => ({}))) as { error?: string }).error;
|
||||||
|
if (err === "slow_down") interval += 5000;
|
||||||
|
else if (err !== "authorization_pending") throw new Error(err ?? `HTTP ${t.status}`);
|
||||||
|
}
|
||||||
|
throw new Error("device code expired");
|
||||||
|
} catch (e) {
|
||||||
|
ui.set(`<strong>Dev sign-in failed</strong><br>${esc(String(e instanceof Error ? e.message : e))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-2
@@ -74,8 +74,25 @@ export async function ensureTokens(): Promise<Tokens | null> {
|
|||||||
return refreshing;
|
return refreshing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dev-only hook (see src/dev/device-auth.ts): tokens obtained through the
|
||||||
|
// device grant must be refreshed through the device refresh endpoint.
|
||||||
|
type RefreshOverride = (refreshToken: string) => Promise<TokenResponse | null>;
|
||||||
|
let refreshOverride: RefreshOverride | null = null;
|
||||||
|
export function setRefreshOverride(fn: RefreshOverride | null) {
|
||||||
|
refreshOverride = fn;
|
||||||
|
}
|
||||||
|
|
||||||
async function doRefresh(t: Tokens): Promise<Tokens | null> {
|
async function doRefresh(t: Tokens): Promise<Tokens | null> {
|
||||||
try {
|
try {
|
||||||
|
if (refreshOverride) {
|
||||||
|
const body = await refreshOverride(t.refreshToken!);
|
||||||
|
if (!body) {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
saveTokens(body, t.refreshToken);
|
||||||
|
return readTokens();
|
||||||
|
}
|
||||||
const res = await fetch("/api/auth/token", {
|
const res = await fetch("/api/auth/token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
@@ -97,14 +114,14 @@ async function doRefresh(t: Tokens): Promise<Tokens | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TokenResponse {
|
export interface TokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
id_token: string;
|
id_token: string;
|
||||||
refresh_token?: string;
|
refresh_token?: string;
|
||||||
expires_in: number;
|
expires_in: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTokens(body: TokenResponse, fallbackRefresh?: string) {
|
export function saveTokens(body: TokenResponse, fallbackRefresh?: string) {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
TOKEN_KEY,
|
TOKEN_KEY,
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ rum?.init({
|
|||||||
endpoint: "/v1/logs",
|
endpoint: "/v1/logs",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Local development only: sign in through the issuer's device grant instead
|
||||||
|
// of the redirect flow. Tree-shaken out of production builds.
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
void import("./dev/device-auth").then((m) => m.startDevAuth());
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Router>
|
<Router>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import react from "@vitejs/plugin-react";
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
const target = "https://otel.k6n.net";
|
const target = "https://otel.k6n.net";
|
||||||
|
// Device-grant issuer for local/agent development (see src/dev/device-auth.ts).
|
||||||
|
// Proxied same-origin so the browser needs no CORS and no issuer URL.
|
||||||
|
const authIssuer = process.env.AUTH_DEV_BASE ?? "https://users.k6n.net";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -18,6 +21,11 @@ export default defineConfig({
|
|||||||
target: target,
|
target: target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
"/dev-auth": {
|
||||||
|
target: authIssuer,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/dev-auth/, "/api/auth"),
|
||||||
|
},
|
||||||
"/auth_callback": {
|
"/auth_callback": {
|
||||||
target: target,
|
target: target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Terminal helper for agent/local development against the protected API.
|
||||||
|
// Device grant (RFC 8628) against the go-auth issuer. The refresh token is
|
||||||
|
// kept in ~/.go-auth/dev/<client>.json (0700 dir, 0600 file). Never commit it.
|
||||||
|
//
|
||||||
|
// node scripts/dev-auth.mjs login approve once in a browser
|
||||||
|
// node scripts/dev-auth.mjs token print a fresh access token
|
||||||
|
// node scripts/dev-auth.mjs whoami print the token claims
|
||||||
|
// node scripts/dev-auth.mjs forget delete the stored refresh token
|
||||||
|
//
|
||||||
|
// Env: AUTH_DEV_BASE (issuer, default https://users.k6n.net),
|
||||||
|
// AUTH_DEVICE_CLIENT_ID (default dev-console),
|
||||||
|
// GO_AUTH_TOKEN (explicit token escape hatch; `token` prints it as-is).
|
||||||
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const BASE = (process.env.AUTH_DEV_BASE ?? "https://users.k6n.net").replace(/\/$/, "");
|
||||||
|
const CLIENT = process.env.AUTH_DEVICE_CLIENT_ID ?? "dev-console";
|
||||||
|
const DIR = join(homedir(), ".go-auth", "dev");
|
||||||
|
const FILE = join(DIR, `${CLIENT}.json`);
|
||||||
|
const GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
||||||
|
|
||||||
|
const post = (path, body) =>
|
||||||
|
fetch(`${BASE}/api/auth${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save(refresh) {
|
||||||
|
await mkdir(DIR, { recursive: true, mode: 0o700 });
|
||||||
|
await chmod(DIR, 0o700);
|
||||||
|
const tmp = `${FILE}.tmp`;
|
||||||
|
await writeFile(tmp, JSON.stringify({ refresh_token: refresh }), { mode: 0o600 });
|
||||||
|
await rename(tmp, FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const start = await post("/device", { client_id: CLIENT });
|
||||||
|
if (!start.ok) throw new Error(`device start failed: HTTP ${start.status}`);
|
||||||
|
const d = await start.json();
|
||||||
|
console.error(`Approve in a browser: ${d.verification_uri_complete ?? d.verification_uri}\nCode: ${d.user_code}`);
|
||||||
|
let interval = (d.interval ?? 5) * 1000;
|
||||||
|
const deadline = Date.now() + d.expires_in * 1000;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await new Promise((r) => setTimeout(r, interval));
|
||||||
|
const t = await post("/device/token", { grant_type: GRANT, device_code: d.device_code, client_id: CLIENT });
|
||||||
|
const body = await t.json().catch(() => ({}));
|
||||||
|
if (t.ok) {
|
||||||
|
await save(body.refresh_token);
|
||||||
|
console.error("Signed in; refresh token stored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body.error === "slow_down") interval += 5000;
|
||||||
|
else if (body.error !== "authorization_pending") throw new Error(body.error ?? `HTTP ${t.status}`);
|
||||||
|
}
|
||||||
|
throw new Error("device code expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function token() {
|
||||||
|
if (process.env.GO_AUTH_TOKEN) return process.env.GO_AUTH_TOKEN;
|
||||||
|
let stored;
|
||||||
|
try {
|
||||||
|
stored = JSON.parse(await readFile(FILE, "utf8"));
|
||||||
|
} catch {
|
||||||
|
throw new Error("not signed in: run `node scripts/dev-auth.mjs login`");
|
||||||
|
}
|
||||||
|
const res = await post("/device/refresh", { client_id: CLIENT, refresh_token: stored.refresh_token });
|
||||||
|
if (!res.ok) throw new Error(`refresh failed (HTTP ${res.status}); run login again`);
|
||||||
|
const body = await res.json();
|
||||||
|
if (body.refresh_token) await save(body.refresh_token);
|
||||||
|
return body.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmds = {
|
||||||
|
login,
|
||||||
|
token: async () => console.log(await token()),
|
||||||
|
whoami: async () => {
|
||||||
|
const t = await token();
|
||||||
|
console.log(JSON.stringify(JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString()), null, 2));
|
||||||
|
},
|
||||||
|
forget: () => rm(FILE, { force: true }),
|
||||||
|
};
|
||||||
|
const cmd = cmds[process.argv[2]];
|
||||||
|
if (!cmd) {
|
||||||
|
console.error("usage: dev-auth.mjs login|token|whoami|forget");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
cmd().catch((e) => {
|
||||||
|
console.error(e.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user