mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-06 05:55:42 +00:00
Covers PUT-1704 and PUT-1707: creating a workspace, admitting the master account, and the whole of offboarding. `createWorkspace` admits the creator with org_owned = 0, which is what makes the master pay for itself and stay an invalid target of every member route. `checkOwnerInvariant` asserts the rule no dialect can express -- the owner is a member with org_owned = 0 and the only such member -- and a test breaks it deliberately, since the schema cannot refuse a second one. Three authority checks: 404 to a stranger so the endpoint is not an existence oracle, 403 to a member who is not the master, and the master refused as a target of member routes. Handle problems surface as 400 (unusable) or 409 (taken), including the unique-index race. `TeamStore` throws a bare Error, which the server would turn into a 500 and a deduped critical alarm -- an uppercase handle should not page on-call. Disable writes `user.suspended` as well as suspended_at and suspended_reason. PUT-1707 named only the latter two, but those are siblings added by 0061 and 0063 -- `userProtected` rejects on `if (user.suspended)` and reads neither. Setting only the timestamp and reason would have recorded a disable that never took effect, and disable is the whole of offboarding here. Sessions are dropped through SessionStore.removeByUuid rather than a raw DELETE. The store invalidates every composite cache key with its double-delete pattern; without that a disabled member keeps authenticating from cache for the session TTL, which is exactly the "next request, not after a cache TTL" property disable is supposed to have. Revoking also preserves last_ip and last_user_agent, which the member-facing audit view reads. Files are untouched and re-enable restores the account. Adds team_not_found, not_the_master_account and not_an_org_account to the HttpError legacy codes, which the controller also needs. Billing events, invalidateActorSubscription, audit rows and the GUI push are deliberately not here -- they belong to phase 3 and PUT-1708.
170 lines
5.5 KiB
TypeScript
170 lines
5.5 KiB
TypeScript
// NOT ALL OF THEM, ADD OLD ONES AS NEEDED, IF NEEDED. DO NOT ADD NEW ONES THOUGH.
|
|
export type LegacyErrorCodes =
|
|
| 'payment_account_not_set_up'
|
|
| 'unknown_error'
|
|
| 'disallowed_value'
|
|
| 'invalid_token'
|
|
| 'item_with_same_name_exists'
|
|
| 'cannot_move_directory_into_itself'
|
|
| 'cannot_copy_directory_into_itself'
|
|
| 'directory_depth_limit_exceeded'
|
|
| 'cannot_move_to_root'
|
|
| 'cannot_copy_to_root'
|
|
| 'cannot_write_to_root'
|
|
| 'cannot_overwrite_a_directory'
|
|
| 'cannot_read_a_directory'
|
|
| 'source_and_dest_are_the_same'
|
|
| 'dest_is_not_a_directory'
|
|
| 'dest_does_not_exist'
|
|
| 'source_does_not_exist'
|
|
| 'subject_does_not_exist'
|
|
| 'shortcut_target_not_found'
|
|
| 'shortcut_target_is_a_directory'
|
|
| 'shortcut_target_is_a_file'
|
|
| 'forbidden'
|
|
| 'storage_limit_reached'
|
|
| 'internal_error'
|
|
| 'response_timeout'
|
|
| 'app_name_already_in_use'
|
|
| 'app_index_url_already_in_use'
|
|
| 'subdomain_limit_reached'
|
|
| 'subdomain_reserved'
|
|
| 'subdomain_not_owned'
|
|
| 'email_already_in_use'
|
|
| 'email_not_allowed'
|
|
| 'username_already_in_use'
|
|
| 'too_many_username_changes'
|
|
| 'token_invalid'
|
|
| 'insufficient_funds'
|
|
| 'token_missing'
|
|
| 'token_auth_failed'
|
|
| 'token_expired'
|
|
| 'permission_denied'
|
|
| 'account_suspended'
|
|
| 'bad_request'
|
|
| 'not_found'
|
|
| 'conflict'
|
|
| 'unauthorized'
|
|
| 'too_many_requests'
|
|
| 'oidc_revalidation_required'
|
|
| 'user_tokens_only'
|
|
| 'session_required'
|
|
| 'temporary_accounts_not_allowed'
|
|
| 'password_required'
|
|
| 'password_mismatch'
|
|
| 'field_not_allowed_for_create'
|
|
| 'account_is_not_verified'
|
|
| 'app_or_api_token_required'
|
|
| 'team_not_found'
|
|
| 'not_the_workspace_owner'
|
|
| 'not_an_org_account';
|
|
|
|
/**
|
|
* 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/](https://www.gnu.org/licenses/).
|
|
*/
|
|
|
|
/** Options accepted by `HttpError`. All optional. */
|
|
export interface HttpErrorOptions {
|
|
/** Underlying error. Set as the standard `Error.cause`. */
|
|
cause?: unknown;
|
|
/**
|
|
* Stable wire-format error code that legacy clients key on (e.g.
|
|
* `item_with_same_name_exists`, `forbidden`, `subject_does_not_exist`).
|
|
* Serialized as `code` in the response body for back-compat.
|
|
*/
|
|
legacyCode?: LegacyErrorCodes | (string & {});
|
|
/**
|
|
* Modern, structured error code. If both `legacyCode` and `code` are set,
|
|
* the legacy one takes the `code` slot in the response body and `code` is
|
|
* emitted as `errorCode`, so clients keying on either field find what they
|
|
* expect.
|
|
*/
|
|
code?: string;
|
|
/** Additional fields merged into the response body. */
|
|
fields?: Record<string, unknown>;
|
|
/**
|
|
* Skip the terminal alarm gate for this error. Set it only where the call
|
|
* site already knows the failure is expected and arrives in proportion to
|
|
* traffic — an upstream rate limit on a zero-cost model, say. Never
|
|
* serialized to the client.
|
|
*/
|
|
noAlarm?: boolean;
|
|
}
|
|
|
|
/**
|
|
* The single error type controllers and services throw to surface an HTTP
|
|
* failure. The terminal `errorHandler` middleware catches it, serializes a JSON
|
|
* body, and sets the response status.
|
|
*
|
|
* Usage:
|
|
*
|
|
* ```ts
|
|
* throw new HttpError(404, 'Item not found');
|
|
* throw new HttpError(409, 'Cannot overwrite directory', {
|
|
* legacyCode: 'is_directory',
|
|
* });
|
|
* throw new HttpError(403, 'Forbidden', {
|
|
* legacyCode: 'forbidden',
|
|
* fields: { target },
|
|
* });
|
|
* ```
|
|
*
|
|
* Express 5 forwards thrown errors (sync and async) to error-handling
|
|
* middleware automatically — no `next(err)` ceremony required.
|
|
*/
|
|
export class HttpError extends Error {
|
|
readonly statusCode: number;
|
|
readonly legacyCode?: LegacyErrorCodes | (string & {});
|
|
readonly code?: string;
|
|
readonly fields?: Record<string, unknown>;
|
|
readonly noAlarm?: boolean;
|
|
|
|
constructor(
|
|
statusCode: number,
|
|
message: string,
|
|
options: HttpErrorOptions = {},
|
|
) {
|
|
super(
|
|
message,
|
|
options.cause !== undefined ? { cause: options.cause } : undefined,
|
|
);
|
|
this.name = 'HttpError';
|
|
this.statusCode = statusCode;
|
|
this.legacyCode = options.legacyCode;
|
|
this.code = options.code;
|
|
this.fields = options.fields;
|
|
this.noAlarm = options.noAlarm;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Type guard that survives module-graph duplication (defensive — cross-realm
|
|
* `instanceof` can be unreliable in test setups). Pure runtime convenience;
|
|
* normal callers can use `instanceof HttpError`.
|
|
*/
|
|
export const isHttpError = (e: unknown): e is HttpError => {
|
|
if (e instanceof HttpError) return true;
|
|
return Boolean(
|
|
e &&
|
|
typeof e === 'object' &&
|
|
(e as { name?: unknown }).name === 'HttpError' &&
|
|
typeof (e as { statusCode?: unknown }).statusCode === 'number',
|
|
);
|
|
};
|