feat: support code in puter.signup.validate error responses (#2945)
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled

Extensions listening on the `puter.signup.validate` event can now set a
`code` field (in addition to `message`) when blocking signups. The code
is forwarded through HttpError and appears in the JSON error response,
giving clients a stable, machine-readable signal to act on.

Covers both the AuthController (password signup) and OIDCService paths.
Adds tests for the new field.
This commit is contained in:
Nariman Jelveh
2026-05-07 10:11:08 -07:00
committed by GitHub
parent bbe93defe9
commit 360cbf48af
3 changed files with 128 additions and 1 deletions
+12 -1
View File
@@ -404,6 +404,7 @@ export class AuthController extends PuterController {
no_temp_user: false,
requires_email_confirmation: false,
message: null,
code: null,
};
try {
await this.clients.event?.emitAndWait(
@@ -418,6 +419,11 @@ export class AuthController extends PuterController {
throw new HttpError(
403,
validateEvent.message ?? 'Signup blocked',
{
...(validateEvent.code
? { code: validateEvent.code }
: {}),
},
);
}
if (is_temp && validateEvent.no_temp_user) {
@@ -425,7 +431,12 @@ export class AuthController extends PuterController {
403,
validateEvent.message ??
'Temporary accounts are disabled',
{ legacyCode: 'must_login_or_signup' },
{
legacyCode: 'must_login_or_signup',
...(validateEvent.code
? { code: validateEvent.code }
: {}),
},
);
}
const force_email_confirmation = Boolean(
@@ -0,0 +1,114 @@
/**
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { PuterServer } from '../../server.js';
import { setupTestServer } from '../../testUtil.js';
import type { EventClient } from '../../clients/EventClient.js';
import { HttpError } from '../../core/http/HttpError.js';
let server: PuterServer;
let eventClient: EventClient;
beforeAll(async () => {
server = await setupTestServer();
eventClient = server.clients.event as unknown as EventClient;
});
afterAll(async () => {
await server?.shutdown();
});
describe('puter.signup.validate event', () => {
it('supports code in the validate event when allow is false', async () => {
eventClient.on('puter.signup.validate', (_key, data) => {
const event = data as {
allow: boolean;
message: string | null;
code: string | null;
};
event.allow = false;
event.message = 'Region not supported';
event.code = 'region_blocked';
});
const validateEvent = {
req: {},
data: {},
ip: '127.0.0.1',
email: 'test@example.com',
allow: true,
no_temp_user: false,
requires_email_confirmation: false,
message: null as string | null,
code: null as string | null,
};
await eventClient.emitAndWait(
'puter.signup.validate',
validateEvent,
{},
);
expect(validateEvent.allow).toBe(false);
expect(validateEvent.message).toBe('Region not supported');
expect(validateEvent.code).toBe('region_blocked');
// Verify the HttpError constructed from this event carries the code
const err = new HttpError(
403,
validateEvent.message ?? 'Signup blocked',
{
...(validateEvent.code
? { code: validateEvent.code }
: {}),
},
);
expect(err.statusCode).toBe(403);
expect(err.message).toBe('Region not supported');
expect(err.code).toBe('region_blocked');
});
it('omits code from HttpError when extension does not set it', async () => {
const validateEvent = {
req: {},
data: {},
ip: '127.0.0.1',
email: 'nocode@example.com',
allow: false,
no_temp_user: false,
requires_email_confirmation: false,
message: 'Blocked',
code: null as string | null,
};
const err = new HttpError(
403,
validateEvent.message ?? 'Signup blocked',
{
...(validateEvent.code
? { code: validateEvent.code }
: {}),
},
);
expect(err.statusCode).toBe(403);
expect(err.message).toBe('Blocked');
expect(err.code).toBeUndefined();
});
});
+2
View File
@@ -407,6 +407,7 @@ export class OIDCService extends PuterService {
no_temp_user: false,
requires_email_confirmation: false,
message: null as string | null,
code: null as string | null,
};
try {
await this.clients.event?.emitAndWait(
@@ -421,6 +422,7 @@ export class OIDCService extends PuterService {
return {
success: false,
error: validateEvent.message ?? 'Signup blocked',
...(validateEvent.code ? { code: validateEvent.code } : {}),
};
}