Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ permissions:
on:
push:
branches:
- main
- master
paths:
- '.github/workflows/android-build.yml'
- 'example/android/**'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ios-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ permissions:
on:
push:
branches:
- main
- master
paths:
- '.github/workflows/ios-build.yml'
- 'example/ios/**'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ permissions:
on:
push:
branches:
- main
- master
paths:
- '.github/workflows/test.yml'
- 'src/**'
Expand Down
33 changes: 33 additions & 0 deletions src/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,39 @@ describe('errors', () => {
const err = new KeyInvalidatedError('invalid', { alias: 'rnsi.svc.v1' })
expect(err.alias).toBe('rnsi.svc.v1')
})

describe('cause chaining', () => {
it('retains the provided cause on SensitiveInfoError', () => {
const cause = new Error('underlying')
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped', {
cause,
})
expect(err.cause).toBe(cause)
})

it('keeps cause non-enumerable to match native ES2022 Error semantics', () => {
const cause = new Error('underlying')
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped', {
cause,
})
const descriptor = Object.getOwnPropertyDescriptor(err, 'cause')
expect(descriptor).toBeDefined()
expect(descriptor?.enumerable).toBe(false)
expect(Object.keys(err)).not.toContain('cause')
expect(JSON.parse(JSON.stringify(err))).not.toHaveProperty('cause')
})

it('does not define cause when not provided', () => {
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped')
expect(Object.hasOwn(err, 'cause')).toBe(false)
})

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test for the ES2022 edge case where options explicitly include { cause: undefined }. Native Error still defines an own (non-enumerable) cause property in that case, and covering it here would make the intended semantics for SensitiveInfoError explicit.

Suggested change
it('defines a non-enumerable own cause when explicitly passed as undefined', () => {
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped', {
cause: undefined,
})
const descriptor = Object.getOwnPropertyDescriptor(err, 'cause')
expect(err.cause).toBeUndefined()
expect(Object.hasOwn(err, 'cause')).toBe(true)
expect(descriptor).toBeDefined()
expect(descriptor?.enumerable).toBe(false)
expect(Object.keys(err)).not.toContain('cause')
})

Copilot uses AI. Check for mistakes.
it('propagates cause through subclasses (NotFoundError)', () => {
const cause = new Error('native miss')
const err = new NotFoundError('missing', { cause })
expect(err.cause).toBe(cause)
})
})
})

describe('toSensitiveInfoError', () => {
Expand Down
16 changes: 16 additions & 0 deletions src/__tests__/hooks.types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ describe('hooks/types', () => {
expect(error.hint).toBe('Check the key.')
})

it('keeps cause non-enumerable to match native ES2022 Error semantics', () => {
const cause = new Error('native failure')
const error = new HookError('Wrapper message', { cause })

const descriptor = Object.getOwnPropertyDescriptor(error, 'cause')
expect(descriptor).toBeDefined()
expect(descriptor?.enumerable).toBe(false)
expect(Object.keys(error)).not.toContain('cause')
expect(JSON.parse(JSON.stringify(error))).not.toHaveProperty('cause')
})

it('omits cause when not provided', () => {
const error = new HookError('Wrapper message')
expect(Object.hasOwn(error, 'cause')).toBe(false)
})
Comment on lines +22 to +36

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests assert non-enumerability and omission when no options are passed, but they don’t cover the ES2022 edge case of explicitly providing { cause: undefined }. Native Error still defines an own non-enumerable cause property in that case; adding a test for it would prevent regressions and clarify intended semantics for HookError.

Copilot uses AI. Check for mistakes.

it('creates the initial async state', () => {
const state = createInitialAsyncState<string>()
expect(state).toEqual({
Expand Down
12 changes: 9 additions & 3 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,17 @@ export class SensitiveInfoError extends Error {
super(message)
this.name = 'SensitiveInfoError'
this.code = code
// Assign `cause` directly instead of passing it to `super()` so this
// Define `cause` manually instead of passing it to `super()` so this
// compiles cleanly under TS configs whose `lib` predates ES2022 (where
// the second `Error` constructor argument was introduced).
// the second `Error` constructor argument was introduced), while keeping
// the property non-enumerable to match the native ES2022 `Error` constructor.
if (options && 'cause' in options) {
;(this as { cause?: unknown }).cause = options.cause
Object.defineProperty(this, 'cause', {
value: options.cause,
writable: true,
configurable: true,
enumerable: false,
})
}
Comment on lines 85 to 99

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If “TS libs predating ES2022” compatibility is a goal, consumers compiling with older lib targets won’t see cause on Error, and SensitiveInfoError doesn’t declare it either. Consider adding a type-only member declare readonly cause?: unknown to SensitiveInfoError so err.cause type-checks without emitting a real class field (which would otherwise create an enumerable own property and defeat the non-enumerability change).

Copilot uses AI. Check for mistakes.
}
}
Expand Down
12 changes: 9 additions & 3 deletions src/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ export class HookError extends Error {
this.name = 'HookError'
this.operation = operation
this.hint = hint
// Assign `cause` directly instead of passing it to `super()` so this
// Define `cause` manually instead of passing it to `super()` so this
// compiles cleanly under TS configs whose `lib` predates ES2022 (where
// the second `Error` constructor argument was introduced).
// the second `Error` constructor argument was introduced), while keeping
// the property non-enumerable to match the native ES2022 `Error` constructor.
Comment on lines 49 to +55

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the goal is compatibility with TS libs predating ES2022, consumers on older lib targets won’t have Error['cause'] in their type definitions, and HookError doesn’t declare it either. To preserve typing without changing runtime enumerability, add a type-only class member like declare readonly cause?: unknown (avoid a real class field, which would emit JS and create an enumerable own property).

Copilot uses AI. Check for mistakes.
if (cause !== undefined) {
;(this as { cause?: unknown }).cause = cause
Object.defineProperty(this, 'cause', {
value: cause,
writable: true,
configurable: true,
enumerable: false,
})

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HookError claims to match native ES2022 Error cause semantics, but the current destructuring + cause !== undefined check cannot distinguish between “no cause provided” and { cause: undefined }. Native Error(message, { cause: undefined }) defines an own cause property (value undefined) that is still non-enumerable. Consider accepting an options object (no destructuring in the parameter list) and checking options && 'cause' in options before calling Object.defineProperty to align with ES2022 behavior.

Copilot uses AI. Check for mistakes.
}
}
}
Expand Down
Loading