Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import eslint from "@eslint/js";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import tseslint from "typescript-eslint";

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
plugins: { "simple-import-sort": simpleImportSort },
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports", fixStyle: "separate-type-imports" },
],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"prefer-const": "error",
"no-var": "error",
},
Expand Down
1,760 changes: 1,214 additions & 546 deletions package-lock.json

Large diffs are not rendered by default.

32 changes: 18 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"lint:fix": "eslint src --fix",
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\"",
"test": "vitest run test/utils test/cli-core.test.ts test/specs-completeness.test.ts",
"test": "vitest run test/utils test/auth test/cli-core.test.ts test/specs-completeness.test.ts",
"test:watch": "vitest",
"test:all": "vitest run",
"test:integration": "vitest run test/cli.test.ts",
Expand Down Expand Up @@ -73,42 +73,46 @@
}
],
"plugins": [
"@semantic-release/commit-analyzer",
["@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "refactor", "release": "patch" }
]
}],
"@semantic-release/release-notes-generator",
"@semantic-release/github",
"@semantic-release/npm"
]
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1000.0",
"@aws-sdk/credential-providers": "^3.1000.0",
"@smithy/shared-ini-file-loader": "^4.4.5",
"@tigrisdata/iam": "^1.3.0",
"@tigrisdata/storage": "^2.15.5",
"@aws-sdk/credential-providers": "^3.1018.0",
"@smithy/shared-ini-file-loader": "^4.4.7",
"@tigrisdata/iam": "^1.4.1",
"@tigrisdata/storage": "^2.15.6",
"axios": "^1.13.6",
"commander": "^14.0.3",
"enquirer": "^2.4.1",
"jose": "^6.1.3",
"jose": "^6.2.2",
"open": "^11.0.0",
"yaml": "^2.8.2"
"yaml": "^2.8.3"
},
"devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^10.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@types/node": "^22.19.11",
"dotenv": "^17.3.1",
"eslint": "^10.0.2",
"eslint": "^10.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"husky": "^9.1.7",
"prettier": "^3.8.1",
"publint": "^0.3.18",
"semantic-release": "^25.0.3",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"vitest": "^4.0.18"
"typescript-eslint": "^8.57.2",
"vitest": "^4.1.2"
}
}
72 changes: 60 additions & 12 deletions src/auth/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,73 @@
*/

import axios from 'axios';
import open from 'open';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import type {
TokenSet,
IdTokenClaims,
TigrisNamespace,
TigrisOrg,
OrganizationInfo,
} from './types.js';
import { getAuth0Config, TIGRIS_CLAIMS_NAMESPACE } from './config.js';
import open from 'open';

import type { OrganizationInfo, TokenSet } from './storage.js';
import {
storeTokens,
getTokens,
clearTokens,
storeOrganizations,
getOrganizations,
getTokens,
storeLoginMethod,
storeOrganizations,
storeTokens,
} from './storage.js';

/**
* Auth0 configuration for CLI authentication
*/
export interface Auth0Config {
domain: string;
clientId: string;
audience: string;
}

/**
* Get Auth0 configuration from environment variables or defaults
*/
export function getAuth0Config(): Auth0Config {
const isDev = process.env.TIGRIS_ENV === 'development';
const domain = isDev
? 'auth-dev.tigris.dev'
: (process.env.AUTH0_DOMAIN ?? 'auth.storage.tigrisdata.io');
const clientId = isDev
? 'JdJVYIyw0O1uHi5L5OJH903qaWBgd3gF'
: (process.env.AUTH0_CLIENT_ID ?? 'DMejqeM3CQ4IqTjEcd3oA9eEiT40hn8D');
const audience = isDev
? 'https://tigris-api-dev'
: (process.env.AUTH0_AUDIENCE ?? 'https://tigris-os-api');

return { domain, clientId, audience };
}

/**
* Custom claims namespace for Tigris
*/
export const TIGRIS_CLAIMS_NAMESPACE =
process.env.TIGRIS_CLAIMS_NAMESPACE || 'https://tigris';

/**
* OAuth-specific types
*/
export interface IdTokenClaims {
sub: string;
email?: string;
email_verified?: boolean;
[key: string]: unknown;
}

interface TigrisOrg {
id: string;
name: string;
slug: string;
}

interface TigrisNamespace {
ns?: (string | TigrisOrg)[];
organizations?: (string | TigrisOrg)[];
}

interface DeviceCodeResponse {
device_code: string;
user_code: string;
Expand Down
68 changes: 0 additions & 68 deletions src/auth/config.ts

This file was deleted.

5 changes: 3 additions & 2 deletions src/auth/fly.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from 'axios';
import type { OrganizationInfo } from './types.js';
import { getAuth0Config, TIGRIS_CLAIMS_NAMESPACE } from './config.js';

import { getAuth0Config, TIGRIS_CLAIMS_NAMESPACE } from './client.js';
import type { OrganizationInfo } from './storage.js';

export function isFlyUser(organizationId?: string): boolean {
return !!organizationId?.startsWith('flyio_');
Expand Down
102 changes: 102 additions & 0 deletions src/auth/iam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Shared IAM auth helpers
* Consolidates OAuth check + auth check + config building patterns
*/

import { failWithError } from '@utils/exit.js';
import type { MessageContext } from '@utils/messages.js';

import { getAuthClient } from './client.js';
import { isFlyUser } from './fly.js';
import { getLoginMethod, getTigrisConfig } from './provider.js';
import { getCredentials, getSelectedOrganization } from './storage.js';

/**
* Check if current org is Fly.io. Prints message and returns true if so.
*/
export function isFlyOrganization(): boolean {
const selectedOrg = getSelectedOrganization();
if (isFlyUser(selectedOrg ?? undefined)) {
console.log(
'User management is not available for Fly.io organizations.\n' +
'Your users are managed through Fly.io.\n\n' +
'Visit https://fly.io to manage your organization members.'
);
return true;
}
return false;
}

/**
* OAuth-only IAM config. Exits on non-OAuth or unauthenticated.
* Used by IAM policy and user commands.
*/
export async function getOAuthIAMConfig(context: MessageContext) {
const loginMethod = await getLoginMethod();
if (loginMethod !== 'oauth') {
failWithError(
context,
'This operation requires OAuth login.\nRun "tigris login oauth" first.'
);
}

const authClient = getAuthClient();
if (!(await authClient.isAuthenticated())) {
failWithError(
context,
'Not authenticated. Run "tigris login oauth" first.'
);
}

const accessToken = await authClient.getAccessToken();
const selectedOrg = getSelectedOrganization();
const { iamEndpoint, mgmtEndpoint } = getTigrisConfig();

return {
sessionToken: accessToken,
organizationId: selectedOrg ?? undefined,
iamEndpoint,
mgmtEndpoint,
};
}

/**
* Dual-mode IAM config (OAuth or credentials).
* Used by access-key commands.
*/
export async function getIAMConfig(context: MessageContext) {
const loginMethod = await getLoginMethod();
const tigrisConfig = getTigrisConfig();
const selectedOrg = getSelectedOrganization();

if (loginMethod === 'oauth') {
const authClient = getAuthClient();
if (!(await authClient.isAuthenticated())) {
failWithError(
context,
'Not authenticated. Run "tigris login oauth" first.'
);
}

return {
sessionToken: await authClient.getAccessToken(),
organizationId: selectedOrg ?? undefined,
iamEndpoint: tigrisConfig.iamEndpoint,
};
}

const credentials = getCredentials();
if (!credentials) {
failWithError(
context,
'Not authenticated. Run "tigris login" or "tigris configure" first.'
);
}

return {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
organizationId: selectedOrg ?? undefined,
iamEndpoint: tigrisConfig.iamEndpoint,
};
}
Loading
Loading