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
36 changes: 26 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ jobs:
fetch-depth: 0 # Fetch all history for changelog generation

- name: Verify tag is on main branch
env:
COMMIT_SHA: ${{ github.sha }}
REF_NAME: ${{ github.ref_name }}
run: |
# Check if the tag is reachable from main branch
git fetch origin main
if ! git merge-base --is-ancestor ${{ github.sha }} origin/main; then
if ! git merge-base --is-ancestor "$COMMIT_SHA" origin/main; then
echo "Error: This tag is not on the main branch"
echo "Tags must be created on the main branch to trigger a release"
echo "Current tag: ${{ github.ref_name }}"
echo "Current tag: $REF_NAME"
exit 1
fi
echo "Tag is on main branch, proceeding with release"
Expand All @@ -48,14 +51,24 @@ jobs:

- name: Extract version from tag
id: version
env:
REF_NAME: ${{ github.ref_name }}
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Git ref names can legitimately contain shell metacharacters, and
# this value flows into later steps. Reject anything that is not a
# clean semver release tag before using it anywhere.
if [[ ! "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Error: Tag '$REF_NAME' is not a valid release tag (expected vX.Y.Z)"
exit 1
fi
VERSION="${REF_NAME#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing version: $VERSION"

- name: Verify tag matches package.json version
env:
TAG_VERSION: ${{ steps.version.outputs.version }}
run: |
TAG_VERSION=${{ steps.version.outputs.version }}
PKG_VERSION=$(node -p "require('./package.json').version")

if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
Expand All @@ -74,10 +87,13 @@ jobs:

- name: Generate release notes
id: release_notes
env:
CURRENT_TAG: ${{ github.ref_name }}
VERSION: ${{ steps.version.outputs.version }}
REPO: ${{ github.repository }}
run: |
# Get the previous version tag (second most recent v* tag by version order)
# This is more reliable than using git describe which follows commit ancestry
CURRENT_TAG=${{ github.ref_name }}
PREV_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v "^${CURRENT_TAG}$" | head -1)

if [ -z "$PREV_TAG" ]; then
Expand All @@ -88,7 +104,7 @@ jobs:

# Get current date
RELEASE_DATE=$(date +%Y-%m-%d)
VERSION=${{ steps.version.outputs.version }}
# VERSION is provided via the validated env var declared above

# Generate changelog with PR links
if [ -n "$PREV_TAG" ]; then
Expand All @@ -115,11 +131,11 @@ jobs:
PR_NUM="${BASH_REMATCH[1]}"
# Remove the (#PR_NUM) from message to avoid duplication (handles with or without space)
CLEAN_MESSAGE=$(echo "$message" | sed -E 's/ ?\(#[0-9]+\)//')
PR_LINK="[#$PR_NUM](https://github.com/${{ github.repository }}/pull/$PR_NUM)"
COMMIT_LINK="[$hash](https://github.com/${{ github.repository }}/commit/$hash)"
PR_LINK="[#$PR_NUM](https://github.com/$REPO/pull/$PR_NUM)"
COMMIT_LINK="[$hash](https://github.com/$REPO/commit/$hash)"
ITEM="$CLEAN_MESSAGE ($PR_LINK) ($COMMIT_LINK)"
else
COMMIT_LINK="[$hash](https://github.com/${{ github.repository }}/commit/$hash)"
COMMIT_LINK="[$hash](https://github.com/$REPO/commit/$hash)"
ITEM="$message ($COMMIT_LINK)"
fi

Expand Down
3 changes: 0 additions & 3 deletions src/FormoAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,11 @@ export class FormoAnalytics implements IFormoAnalytics {
chainId,
address,
message,
signatureHash,
}: {
status: SignatureStatus;
chainId?: ChainID;
address: Address;
message: string;
signatureHash?: string;
},
properties?: IFormoEventProperties,
context?: IFormoEventContext,
Expand All @@ -424,7 +422,6 @@ export class FormoAnalytics implements IFormoAnalytics {
...(chainId !== undefined && chainId !== null && { chainId }),
address,
message,
...(signatureHash && { signatureHash }),
},
properties,
context,
Expand Down
18 changes: 17 additions & 1 deletion src/__tests__/FormoAnalytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,27 @@ describe('FormoAnalytics', () => {
chainId: 1,
address: '0x742d35cc6634c0532925a3b844bc9e7595f3f6d2',
message: 'test message',
signatureHash: '0xabc123',
});

expect(mockEventManager.addEvent).toHaveBeenCalled();
});

it('never forwards a signatureHash to the event pipeline', async () => {
await analytics.signature({
status: SignatureStatus.CONFIRMED,
chainId: 1,
address: '0x742d35cc6634c0532925a3b844bc9e7595f3f6d2',
message: 'test message',
// signatureHash was removed from the API; a caller forcing it in
// must never reach the emitted event.
signatureHash: '0x' + 'a'.repeat(130),
} as any);

expect(mockEventManager.addEvent).toHaveBeenCalled();
const emitted = mockEventManager.addEvent.mock.calls[0][0];
expect(emitted).not.toHaveProperty('signatureHash');
expect(JSON.stringify(emitted)).not.toContain('a'.repeat(130));
});
});

describe('transaction()', () => {
Expand Down
70 changes: 70 additions & 0 deletions src/__tests__/signature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { WagmiEventHandler } from "../lib/wagmi/WagmiEventHandler";

// A realistic 65-byte ECDSA signature (the replayable credential we must not leak).
const RAW_SIGNATURE = "0x" + "a".repeat(130);

// Build a WagmiEventHandler with the formo boundary mocked. The wagmi/query
// plumbing is external integration surface (unavoidable mock); the code under
// test — handleSignatureMutation — is exercised for real.
function makeHandler() {
const signature = jest.fn().mockResolvedValue(undefined);
const formo = {
connect: jest.fn(),
disconnect: jest.fn(),
chain: jest.fn(),
signature,
transaction: jest.fn(),
isAutocaptureEnabled: jest.fn(() => true),
};
const wagmiConfig = { subscribe: () => () => {} };
const handler = new WagmiEventHandler(formo as any, wagmiConfig as any);
// Connection state is established elsewhere; inject it directly so we can
// exercise the signature path in isolation.
(handler as any).trackingState = {
isProcessing: false,
lastChainId: 1,
lastAddress: "0x742d35cc6634c0532925a3b844bc9e7595f3f6d2",
};
const fire = (mutationType: "signMessage" | "signTypedData", state: any) =>
(handler as any).handleSignatureMutation(mutationType, { state });
return { signature, fire };
}

const flat = (obj: unknown) => JSON.stringify(obj);

describe("signature autocapture must never emit the raw signature", () => {
it("signMessage: no signatureHash, no raw signature value", () => {
const { signature, fire } = makeHandler();

fire("signMessage", {
status: "success",
data: RAW_SIGNATURE,
variables: { message: "hello" },
});

expect(signature).toHaveBeenCalledTimes(1);
const arg = signature.mock.calls[0][0];
expect(arg).not.toHaveProperty("signatureHash");
expect(flat(arg)).not.toContain(RAW_SIGNATURE);
});

it("signTypedData: no signatureHash, no raw signature value", () => {
const { signature, fire } = makeHandler();
const permit = {
domain: { name: "USD Coin", chainId: 1 },
primaryType: "Permit",
types: { Permit: [{ name: "owner", type: "address" }] },
message: { owner: "0xVictim", spender: "0xRouter", value: "1", deadline: 9 },
};

fire("signTypedData", {
status: "success",
data: RAW_SIGNATURE,
variables: permit,
});

const arg = signature.mock.calls[0][0];
expect(arg).not.toHaveProperty("signatureHash");
expect(flat(arg)).not.toContain(RAW_SIGNATURE);
});
});
3 changes: 0 additions & 3 deletions src/lib/event/EventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,6 @@ class EventFactory implements IEventFactory {
chainId: ChainID | undefined,
address: Address,
message: string,
signatureHash?: string,
properties?: IFormoEventProperties,
context?: IFormoEventContext
): Promise<IFormoEvent> {
Expand All @@ -508,7 +507,6 @@ class EventFactory implements IEventFactory {
status,
...(chainId !== undefined && chainId !== null && { chainId }),
message,
...(signatureHash && { signatureHash }),
...properties,
},
address,
Expand Down Expand Up @@ -646,7 +644,6 @@ class EventFactory implements IEventFactory {
event.chainId,
event.address,
event.message,
event.signatureHash,
event.properties,
event.context
);
Expand Down
1 change: 0 additions & 1 deletion src/lib/event/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export interface IEventFactory {
chainId: ChainID | undefined,
address: Address,
message: string,
signatureHash?: string,
properties?: IFormoEventProperties,
context?: IFormoEventContext
): Promise<IFormoEvent>;
Expand Down
4 changes: 0 additions & 4 deletions src/lib/wagmi/WagmiEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ interface IFormoAnalyticsInstance {
chainId: number;
address: string;
message: string;
signatureHash?: string;
}): Promise<void>;
transaction(params: {
status: TransactionStatus;
Expand Down Expand Up @@ -362,13 +361,11 @@ export class WagmiEventHandler {

try {
let status: SignatureStatus;
let signatureHash: string | undefined;

if (state.status === "pending") {
status = SignatureStatus.REQUESTED;
} else if (state.status === "success") {
status = SignatureStatus.CONFIRMED;
signatureHash = state.data as string;
} else if (state.status === "error") {
status = SignatureStatus.REJECTED;
} else {
Expand All @@ -394,7 +391,6 @@ export class WagmiEventHandler {
chainId,
address,
message,
...(signatureHash && { signatureHash }),
}).catch((error) => {
logger.error("WagmiEventHandler: Error tracking signature:", error);
});
Expand Down
1 change: 0 additions & 1 deletion src/types/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export interface IFormoAnalytics {
chainId?: ChainID;
address: Address;
message: string;
signatureHash?: string;
},
properties?: IFormoEventProperties,
context?: IFormoEventContext,
Expand Down
1 change: 0 additions & 1 deletion src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export interface SignatureAPIEvent {
chainId?: ChainID;
address: Address;
message: string;
signatureHash?: string;
}

export interface ConnectAPIEvent {
Expand Down