-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(utils): add token redaction utility for safe logging #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,71 @@ | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Redact a Figma Personal Access Token for safe logging. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @remarks | ||||||||||||||||||||||||||||||||||||||||||
| * This utility masks the middle portion of a token while preserving | ||||||||||||||||||||||||||||||||||||||||||
| * the first and last few characters for identification. Useful for | ||||||||||||||||||||||||||||||||||||||||||
| * debugging and logging without exposing sensitive credentials. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param token - The token to redact (can be null/undefined) | ||||||||||||||||||||||||||||||||||||||||||
| * @param options - Optional configuration for redaction behavior | ||||||||||||||||||||||||||||||||||||||||||
| * @returns The redacted token string, or a placeholder for empty/short tokens | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||||||
| * ```ts | ||||||||||||||||||||||||||||||||||||||||||
| * import { redactToken } from '@figma-vars/hooks/utils'; | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * console.log(redactToken('figd_abc123xyz789def456')); | ||||||||||||||||||||||||||||||||||||||||||
| * // Output: 'figd_***...***456' | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * console.log(redactToken(null)); | ||||||||||||||||||||||||||||||||||||||||||
| * // Output: '[no token]' | ||||||||||||||||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @public | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| export interface RedactTokenOptions { | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Number of characters to show at the start of the token. | ||||||||||||||||||||||||||||||||||||||||||
| * @defaultValue 5 | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| visibleStart?: number | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Number of characters to show at the end of the token. | ||||||||||||||||||||||||||||||||||||||||||
| * @defaultValue 3 | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| visibleEnd?: number | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Placeholder text for null/undefined tokens. | ||||||||||||||||||||||||||||||||||||||||||
| * @defaultValue '[no token]' | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| emptyPlaceholder?: string | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+42
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export function redactToken( | ||||||||||||||||||||||||||||||||||||||||||
| token: string | null | undefined, | ||||||||||||||||||||||||||||||||||||||||||
| options?: RedactTokenOptions | ||||||||||||||||||||||||||||||||||||||||||
| ): string { | ||||||||||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||||||||||
| visibleStart = 5, | ||||||||||||||||||||||||||||||||||||||||||
| visibleEnd = 3, | ||||||||||||||||||||||||||||||||||||||||||
| emptyPlaceholder = '[no token]', | ||||||||||||||||||||||||||||||||||||||||||
| } = options ?? {} | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+53
|
||||||||||||||||||||||||||||||||||||||||||
| const { | |
| visibleStart = 5, | |
| visibleEnd = 3, | |
| emptyPlaceholder = '[no token]', | |
| } = options ?? {} | |
| const emptyPlaceholder = options?.emptyPlaceholder ?? '[no token]' | |
| let visibleStart = options?.visibleStart | |
| if (typeof visibleStart !== 'number' || !Number.isFinite(visibleStart) || visibleStart < 0) { | |
| visibleStart = 5 | |
| } else { | |
| visibleStart = Math.floor(visibleStart) | |
| } | |
| let visibleEnd = options?.visibleEnd | |
| if (typeof visibleEnd !== 'number' || !Number.isFinite(visibleEnd) || visibleEnd < 0) { | |
| visibleEnd = 3 | |
| } else { | |
| visibleEnd = Math.floor(visibleEnd) | |
| } |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a critical bug when visibleEnd is 0. The expression slice(-0) is equivalent to slice(0), which returns the entire string from the beginning, effectively exposing the full token instead of hiding it. This defeats the security purpose of the function.
To fix this, add a conditional check: if visibleEnd is 0, use an empty string for end. Similarly, ensure visibleStart works correctly when 0 (it already does, since slice(0, 0) returns an empty string).
| const end = token.slice(-visibleEnd) | |
| const end = visibleEnd === 0 ? '' : token.slice(-visibleEnd) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { describe, it, expect } from 'vitest' | ||
| import { redactToken } from '../../src/utils/redactToken' | ||
|
|
||
| describe('redactToken', () => { | ||
| describe('basic redaction', () => { | ||
| it('should redact middle portion of a normal token', () => { | ||
| const token = 'figd_abc123xyz789def456' | ||
| const result = redactToken(token) | ||
| expect(result).toBe('figd_***...***456') | ||
| }) | ||
|
|
||
| it('should show first 5 and last 3 characters by default', () => { | ||
| const token = 'abcdefghijklmnopqrstuvwxyz' | ||
| const result = redactToken(token) | ||
| expect(result).toBe('abcde***...***xyz') | ||
| }) | ||
| }) | ||
|
|
||
| describe('null/undefined/empty handling', () => { | ||
| it('should return placeholder for null token', () => { | ||
| expect(redactToken(null)).toBe('[no token]') | ||
| }) | ||
|
|
||
| it('should return placeholder for undefined token', () => { | ||
| expect(redactToken(undefined)).toBe('[no token]') | ||
| }) | ||
|
|
||
| it('should return placeholder for empty string', () => { | ||
| expect(redactToken('')).toBe('[no token]') | ||
| }) | ||
| }) | ||
|
|
||
| describe('short token handling', () => { | ||
| it('should mask entirely if token is too short', () => { | ||
| // minLength = 5 + 3 + 1 = 9, so 8 chars should be fully masked | ||
| expect(redactToken('12345678')).toBe('********') | ||
| }) | ||
|
|
||
| it('should mask single character token', () => { | ||
| expect(redactToken('x')).toBe('*') | ||
| }) | ||
|
|
||
| it('should mask token at exactly minimum length', () => { | ||
| // 9 chars is exactly minLength, should be redacted normally | ||
| expect(redactToken('123456789')).toBe('12345***...***789') | ||
| }) | ||
| }) | ||
|
|
||
| describe('custom options', () => { | ||
| it('should use custom visibleStart', () => { | ||
| const result = redactToken('abcdefghijklmnop', { visibleStart: 3 }) | ||
| expect(result).toBe('abc***...***nop') | ||
| }) | ||
|
|
||
| it('should use custom visibleEnd', () => { | ||
| const result = redactToken('abcdefghijklmnop', { visibleEnd: 5 }) | ||
| expect(result).toBe('abcde***...***lmnop') | ||
| }) | ||
|
|
||
| it('should use custom emptyPlaceholder', () => { | ||
| const result = redactToken(null, { emptyPlaceholder: 'N/A' }) | ||
| expect(result).toBe('N/A') | ||
| }) | ||
|
|
||
| it('should use all custom options together', () => { | ||
| const result = redactToken('abcdefghijklmnop', { | ||
| visibleStart: 2, | ||
| visibleEnd: 2, | ||
| emptyPlaceholder: 'custom', | ||
| }) | ||
| expect(result).toBe('ab***...***op') | ||
| }) | ||
| }) | ||
|
Comment on lines
+49
to
+73
|
||
|
|
||
| describe('edge cases', () => { | ||
| it('should handle token with special characters', () => { | ||
| const token = 'figd_!@#$%^&*()_+-=[]' | ||
| const result = redactToken(token) | ||
| // Last 3 chars of 'figd_!@#$%^&*()_+-=[]' are '=[]' | ||
| expect(result).toBe('figd_***...***=[]') | ||
| }) | ||
|
|
||
| it('should handle very long token', () => { | ||
| const token = 'a'.repeat(1000) | ||
| const result = redactToken(token) | ||
| expect(result).toBe('aaaaa***...***aaa') | ||
| }) | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The interface documentation should specify that
visibleStartandvisibleEndmust be non-negative integers. Currently, there's no indication of valid ranges or constraints, which could lead to misuse. Consider adding a note in the JSDoc comments for both properties.