@drawcall/auth 0.1.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 +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +24 -0
- package/dist/command.d.ts +7 -0
- package/dist/command.js +36 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +119 -0
- package/dist/device-login.d.ts +9 -0
- package/dist/device-login.js +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @drawcall/auth
|
|
2
|
+
|
|
3
|
+
Shared authentication for Drawcall command-line tools.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @drawcall/auth login
|
|
7
|
+
npx @drawcall/auth status
|
|
8
|
+
npx @drawcall/auth logout
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Credentials live in the platform config directory under `drawcall/config.json`. Set
|
|
12
|
+
`DRAWCALL_AUTH_TOKEN` to use an ephemeral token instead. Existing credentials from the former
|
|
13
|
+
Market and Design config files migrate automatically.
|
|
14
|
+
|
|
15
|
+
The Commander implementation is public for aggregate CLIs:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createAuthCommand } from "@drawcall/auth";
|
|
19
|
+
|
|
20
|
+
program.addCommand(createAuthCommand());
|
|
21
|
+
```
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { createAuthCommand } from "./command.js";
|
|
4
|
+
const program = createAuthCommand({ name: "drawcall-auth" }).version(readPackageVersion());
|
|
5
|
+
if (process.argv.length <= 2) {
|
|
6
|
+
program.outputHelp();
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
program.parseAsync().catch((error) => {
|
|
10
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function readPackageVersion() {
|
|
15
|
+
const manifest = createRequire(import.meta.url)("../package.json");
|
|
16
|
+
if (!manifest ||
|
|
17
|
+
typeof manifest !== "object" ||
|
|
18
|
+
Array.isArray(manifest) ||
|
|
19
|
+
!("version" in manifest) ||
|
|
20
|
+
typeof manifest.version !== "string") {
|
|
21
|
+
throw new Error("@drawcall/auth package.json is missing a valid version");
|
|
22
|
+
}
|
|
23
|
+
return manifest.version;
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type DeviceLoginOptions } from "./device-login.js";
|
|
3
|
+
export interface AuthCommandOptions {
|
|
4
|
+
name?: string;
|
|
5
|
+
login?: DeviceLoginOptions;
|
|
6
|
+
}
|
|
7
|
+
export declare function createAuthCommand(options?: AuthCommandOptions): Command;
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfigPath, resolveAuthToken, saveAuthToken, signOut } from "./config.js";
|
|
3
|
+
import { runDeviceLogin } from "./device-login.js";
|
|
4
|
+
export function createAuthCommand(options = {}) {
|
|
5
|
+
const command = new Command(options.name ?? "auth").description("Manage shared Drawcall authentication");
|
|
6
|
+
command
|
|
7
|
+
.command("login")
|
|
8
|
+
.description("Sign in with your Drawcall account")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const token = await runDeviceLogin(options.login);
|
|
11
|
+
await saveAuthToken(token);
|
|
12
|
+
console.log(`Signed in. Credentials saved to ${getConfigPath()}.`);
|
|
13
|
+
});
|
|
14
|
+
command
|
|
15
|
+
.command("logout")
|
|
16
|
+
.description("Sign out")
|
|
17
|
+
.action(async () => {
|
|
18
|
+
console.log((await signOut()) ? "Signed out." : "Already signed out.");
|
|
19
|
+
});
|
|
20
|
+
command
|
|
21
|
+
.command("status")
|
|
22
|
+
.description("Show the active authentication source")
|
|
23
|
+
.action(async () => {
|
|
24
|
+
const auth = await resolveAuthToken();
|
|
25
|
+
if (!auth) {
|
|
26
|
+
console.log("Not signed in.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (auth.source === "environment") {
|
|
30
|
+
console.log("Signed in via DRAWCALL_AUTH_TOKEN.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(`Signed in with credentials from ${getConfigPath()}.`);
|
|
34
|
+
});
|
|
35
|
+
return command;
|
|
36
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AuthStorageOptions {
|
|
2
|
+
configHome?: string;
|
|
3
|
+
environment?: NodeJS.ProcessEnv;
|
|
4
|
+
}
|
|
5
|
+
export type AuthTokenSource = "environment" | "shared" | "legacy";
|
|
6
|
+
export interface ResolvedAuthToken {
|
|
7
|
+
token: string;
|
|
8
|
+
source: AuthTokenSource;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveAuthToken(options?: AuthStorageOptions): Promise<ResolvedAuthToken | null>;
|
|
11
|
+
export declare function getAuthToken(options?: AuthStorageOptions): Promise<string | undefined>;
|
|
12
|
+
export declare function saveAuthToken(authToken: string, options?: AuthStorageOptions): Promise<void>;
|
|
13
|
+
export declare function clearAuthToken(options?: AuthStorageOptions): Promise<boolean>;
|
|
14
|
+
export declare function signOut(options?: AuthStorageOptions): Promise<boolean>;
|
|
15
|
+
export declare function getConfigPath(options?: AuthStorageOptions): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const authConfigSchema = z.object({ authToken: z.string().min(1) });
|
|
6
|
+
const legacyConfigSchema = z.record(z.string(), z.unknown());
|
|
7
|
+
const legacyConfigDirectories = ["drawcall-market", "drawcall-design"];
|
|
8
|
+
export async function resolveAuthToken(options = {}) {
|
|
9
|
+
const environmentToken = (options.environment ?? process.env).DRAWCALL_AUTH_TOKEN;
|
|
10
|
+
if (environmentToken)
|
|
11
|
+
return { token: environmentToken, source: "environment" };
|
|
12
|
+
const config = await loadAuthConfig(options);
|
|
13
|
+
if (config)
|
|
14
|
+
return { token: config.authToken, source: "shared" };
|
|
15
|
+
const token = await readLegacyAuthToken(options);
|
|
16
|
+
if (!token)
|
|
17
|
+
return null;
|
|
18
|
+
await saveAuthToken(token, options);
|
|
19
|
+
return { token, source: "legacy" };
|
|
20
|
+
}
|
|
21
|
+
export async function getAuthToken(options = {}) {
|
|
22
|
+
return (await resolveAuthToken(options))?.token;
|
|
23
|
+
}
|
|
24
|
+
export async function saveAuthToken(authToken, options = {}) {
|
|
25
|
+
const config = authConfigSchema.parse({ authToken });
|
|
26
|
+
await writePrivateJson(getConfigPath(options), config);
|
|
27
|
+
await clearLegacyAuthTokens(options);
|
|
28
|
+
}
|
|
29
|
+
export async function clearAuthToken(options = {}) {
|
|
30
|
+
const removedSharedConfig = await removeFile(getConfigPath(options));
|
|
31
|
+
const removedLegacyTokens = await clearLegacyAuthTokens(options);
|
|
32
|
+
return removedSharedConfig || removedLegacyTokens;
|
|
33
|
+
}
|
|
34
|
+
export async function signOut(options = {}) {
|
|
35
|
+
if ((options.environment ?? process.env).DRAWCALL_AUTH_TOKEN) {
|
|
36
|
+
throw new Error("Authentication comes from DRAWCALL_AUTH_TOKEN. Unset it to sign out.");
|
|
37
|
+
}
|
|
38
|
+
return clearAuthToken(options);
|
|
39
|
+
}
|
|
40
|
+
export function getConfigPath(options = {}) {
|
|
41
|
+
return path.join(configHome(options), "drawcall", "config.json");
|
|
42
|
+
}
|
|
43
|
+
async function loadAuthConfig(options) {
|
|
44
|
+
try {
|
|
45
|
+
return authConfigSchema.parse(JSON.parse(await fs.readFile(getConfigPath(options), "utf8")));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (isMissingFile(error))
|
|
49
|
+
return null;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function readLegacyAuthToken(options) {
|
|
54
|
+
const tokens = [];
|
|
55
|
+
for (const file of legacyConfigPaths(options)) {
|
|
56
|
+
const config = await readLegacyConfig(file);
|
|
57
|
+
if (typeof config?.authToken === "string" && config.authToken.length > 0) {
|
|
58
|
+
tokens.push({ token: config.authToken, modifiedAt: (await fs.stat(file)).mtimeMs });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
tokens.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
62
|
+
return tokens[0]?.token;
|
|
63
|
+
}
|
|
64
|
+
async function clearLegacyAuthTokens(options) {
|
|
65
|
+
const results = await Promise.all(legacyConfigPaths(options).map(clearLegacyAuthToken));
|
|
66
|
+
return results.some(Boolean);
|
|
67
|
+
}
|
|
68
|
+
async function clearLegacyAuthToken(file) {
|
|
69
|
+
const config = await readLegacyConfig(file);
|
|
70
|
+
if (!config || !("authToken" in config))
|
|
71
|
+
return false;
|
|
72
|
+
const entries = Object.entries(config).filter(([key]) => key !== "authToken");
|
|
73
|
+
if (entries.length === 0)
|
|
74
|
+
return removeFile(file);
|
|
75
|
+
await writePrivateJson(file, Object.fromEntries(entries));
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
async function readLegacyConfig(file) {
|
|
79
|
+
try {
|
|
80
|
+
return legacyConfigSchema.parse(JSON.parse(await fs.readFile(file, "utf8")));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (isMissingFile(error))
|
|
84
|
+
return null;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function writePrivateJson(file, value) {
|
|
89
|
+
const directory = path.dirname(file);
|
|
90
|
+
await fs.mkdir(directory, { recursive: true });
|
|
91
|
+
await fs.chmod(directory, 0o700);
|
|
92
|
+
await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
93
|
+
await fs.chmod(file, 0o600);
|
|
94
|
+
}
|
|
95
|
+
async function removeFile(file) {
|
|
96
|
+
try {
|
|
97
|
+
await fs.unlink(file);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (isMissingFile(error))
|
|
102
|
+
return false;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function legacyConfigPaths(options) {
|
|
107
|
+
return legacyConfigDirectories.map((directory) => path.join(configHome(options), directory, "config.json"));
|
|
108
|
+
}
|
|
109
|
+
function configHome(options) {
|
|
110
|
+
if (options.configHome)
|
|
111
|
+
return options.configHome;
|
|
112
|
+
const environment = options.environment ?? process.env;
|
|
113
|
+
if (process.platform === "win32" && environment.APPDATA)
|
|
114
|
+
return environment.APPDATA;
|
|
115
|
+
return environment.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
116
|
+
}
|
|
117
|
+
function isMissingFile(error) {
|
|
118
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
119
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_AUTH_ISSUER_URL = "https://auth.drawcall.ai/api/auth";
|
|
2
|
+
export declare const DEFAULT_DEVICE_CLIENT_ID = "drawcall-cli";
|
|
3
|
+
export declare const DEFAULT_AUTH_SCOPES: readonly ["design", "market"];
|
|
4
|
+
export interface DeviceLoginOptions {
|
|
5
|
+
issuerUrl?: string;
|
|
6
|
+
clientId?: string;
|
|
7
|
+
scopes?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function runDeviceLogin(options?: DeviceLoginOptions): Promise<string>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const DEFAULT_AUTH_ISSUER_URL = "https://auth.drawcall.ai/api/auth";
|
|
2
|
+
export const DEFAULT_DEVICE_CLIENT_ID = "drawcall-cli";
|
|
3
|
+
export const DEFAULT_AUTH_SCOPES = ["design", "market"];
|
|
4
|
+
export async function runDeviceLogin(options = {}) {
|
|
5
|
+
const [{ default: open }, oauth] = await Promise.all([import("open"), import("openid-client")]);
|
|
6
|
+
const issuerUrl = options.issuerUrl ?? DEFAULT_AUTH_ISSUER_URL;
|
|
7
|
+
const clientId = options.clientId ?? DEFAULT_DEVICE_CLIENT_ID;
|
|
8
|
+
const scopes = options.scopes ?? DEFAULT_AUTH_SCOPES;
|
|
9
|
+
const configuration = await oauth.discovery(new URL(issuerUrl), clientId, undefined, oauth.None());
|
|
10
|
+
const authorization = await oauth.initiateDeviceAuthorization(configuration, {
|
|
11
|
+
scope: scopes.join(" "),
|
|
12
|
+
});
|
|
13
|
+
const verificationUrl = authorization.verification_uri_complete ?? authorization.verification_uri;
|
|
14
|
+
console.log(`Open ${verificationUrl}`);
|
|
15
|
+
console.log(`Code: ${authorization.user_code}`);
|
|
16
|
+
try {
|
|
17
|
+
await open(verificationUrl);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
21
|
+
console.error(`Could not open a browser automatically: ${message}`);
|
|
22
|
+
}
|
|
23
|
+
const tokens = await oauth.pollDeviceAuthorizationGrant(configuration, authorization);
|
|
24
|
+
if (!tokens.access_token) {
|
|
25
|
+
throw new Error("Device login completed without an access token.");
|
|
26
|
+
}
|
|
27
|
+
return tokens.access_token;
|
|
28
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createAuthCommand, type AuthCommandOptions } from "./command.js";
|
|
2
|
+
export { clearAuthToken, getAuthToken, getConfigPath, resolveAuthToken, saveAuthToken, signOut, type AuthStorageOptions, type AuthTokenSource, type ResolvedAuthToken, } from "./config.js";
|
|
3
|
+
export { DEFAULT_AUTH_ISSUER_URL, DEFAULT_AUTH_SCOPES, DEFAULT_DEVICE_CLIENT_ID, runDeviceLogin, type DeviceLoginOptions, } from "./device-login.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createAuthCommand } from "./command.js";
|
|
2
|
+
export { clearAuthToken, getAuthToken, getConfigPath, resolveAuthToken, saveAuthToken, signOut, } from "./config.js";
|
|
3
|
+
export { DEFAULT_AUTH_ISSUER_URL, DEFAULT_AUTH_SCOPES, DEFAULT_DEVICE_CLIENT_ID, runDeviceLogin, } from "./device-login.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drawcall/auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"description": "Shared authentication for Drawcall command-line tools.",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/drawcall-ai/auth.git",
|
|
12
|
+
"directory": "packages/auth"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"drawcall-auth": "./dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^14.0.3",
|
|
28
|
+
"open": "^10.2.0",
|
|
29
|
+
"openid-client": "^6.8.4",
|
|
30
|
+
"zod": "^4.3.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.6.0",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
|
|
39
|
+
"test": "tsx --test tests/*.test.ts",
|
|
40
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
41
|
+
}
|
|
42
|
+
}
|