mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-05 17:10:45 +00:00
4f5fec5ee4
* feat: resolve private app hosts by index_url fallback Adds a private-app lookup fallback for hosted subdomains without associated_app_id by matching owner-scoped index_url candidates built from request host and configured protocol. * fix: redirect path * fix: add new domains too * fix, bootstrap url * fix: bootstrap url * fix: auto sign in puter pirvate app
162 lines
5.9 KiB
TypeScript
162 lines
5.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import * as jwt from 'jsonwebtoken';
|
|
import { AuthService } from './AuthService.js';
|
|
|
|
type AuthServiceForPrivateTokenTests = AuthService & {
|
|
global_config: {
|
|
jwt_secret: string;
|
|
private_app_asset_token_ttl_seconds: number;
|
|
private_app_asset_cookie_name: string;
|
|
private_app_hosting_domain: string;
|
|
};
|
|
modules: {
|
|
jwt: {
|
|
sign: typeof jwt.sign;
|
|
verify: typeof jwt.verify;
|
|
};
|
|
};
|
|
uuid_fpe: {
|
|
encrypt: (value: string) => string;
|
|
decrypt: (value: string) => string;
|
|
};
|
|
};
|
|
|
|
const createAuthService = (): AuthServiceForPrivateTokenTests => {
|
|
const authService = Object.create(AuthService.prototype) as AuthServiceForPrivateTokenTests;
|
|
authService.global_config = {
|
|
jwt_secret: 'private-asset-test-secret',
|
|
private_app_asset_token_ttl_seconds: 3600,
|
|
private_app_asset_cookie_name: 'puter.private.asset.token',
|
|
private_app_hosting_domain: 'app.puter.localhost',
|
|
};
|
|
authService.modules = {
|
|
jwt: {
|
|
sign: jwt.sign.bind(jwt),
|
|
verify: jwt.verify.bind(jwt),
|
|
},
|
|
};
|
|
authService.uuid_fpe = {
|
|
encrypt: (value) => value,
|
|
decrypt: (value) => value,
|
|
};
|
|
// @ts-expect-error test-only lightweight stub
|
|
authService.get_session_ = vi.fn().mockResolvedValue(undefined);
|
|
return authService;
|
|
};
|
|
|
|
describe('AuthService private asset token helpers', () => {
|
|
it('creates and verifies private asset tokens with expected claims', () => {
|
|
const authService = createAuthService();
|
|
const appUid = 'app-7e2d3016-8d36-456a-9dc7-b75b0f4f7683';
|
|
const userUid = '4b0cecf8-dd6a-4eb5-bcc4-c76cc7e8d7f0';
|
|
const sessionUuid = 'f9000804-2fd3-4da5-819b-afc5296f90f7';
|
|
|
|
const token = authService.createPrivateAssetToken({
|
|
appUid,
|
|
userUid,
|
|
sessionUuid,
|
|
ttlSeconds: 120,
|
|
});
|
|
|
|
const claims = authService.verifyPrivateAssetToken(token, {
|
|
expectedAppUid: appUid,
|
|
expectedUserUid: userUid,
|
|
expectedSessionUuid: sessionUuid,
|
|
});
|
|
|
|
expect(claims.appUid).toBe(appUid);
|
|
expect(claims.userUid).toBe(userUid);
|
|
expect(claims.sessionUuid).toBe(sessionUuid);
|
|
expect(typeof claims.exp).toBe('number');
|
|
});
|
|
|
|
it('rejects tokens when expected user or app does not match', () => {
|
|
const authService = createAuthService();
|
|
const token = authService.createPrivateAssetToken({
|
|
appUid: 'app-9f1c10e3-9a7f-43fb-8671-af4918e65407',
|
|
userUid: '9885b80e-1a14-4c8d-9e3f-4fa5915b1136',
|
|
});
|
|
|
|
expect(() => authService.verifyPrivateAssetToken(token, {
|
|
expectedAppUid: 'app-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
|
})).toThrow();
|
|
|
|
expect(() => authService.verifyPrivateAssetToken(token, {
|
|
expectedUserUid: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
|
})).toThrow();
|
|
});
|
|
|
|
it('rejects non private-asset tokens', () => {
|
|
const authService = createAuthService();
|
|
const token = jwt.sign({
|
|
type: 'session',
|
|
uuid: '245f33f0-c07e-40e2-be22-5215752e3462',
|
|
user_uid: '6cce4692-3855-4ef8-af7d-5c2a02e6b6d8',
|
|
}, authService.global_config.jwt_secret, { expiresIn: 60 });
|
|
|
|
expect(() => authService.verifyPrivateAssetToken(token)).toThrow();
|
|
});
|
|
|
|
it('returns hardened cookie options with config-driven ttl and domain', () => {
|
|
const authService = createAuthService();
|
|
const options = authService.getPrivateAssetCookieOptions();
|
|
|
|
expect(authService.getPrivateAssetCookieName()).toBe('puter.private.asset.token');
|
|
expect(options.sameSite).toBe('none');
|
|
expect(options.secure).toBe(true);
|
|
expect(options.httpOnly).toBe(true);
|
|
expect(options.path).toBe('/');
|
|
expect(options.maxAge).toBe(3_600_000);
|
|
expect(options.domain).toBe('.app.puter.localhost');
|
|
});
|
|
|
|
it('resolves bootstrap identity from app-under-user token without app lookup', async () => {
|
|
const authService = createAuthService();
|
|
const userUid = '4b0cecf8-dd6a-4eb5-bcc4-c76cc7e8d7f0';
|
|
const sessionUuid = 'f9000804-2fd3-4da5-819b-afc5296f90f7';
|
|
const token = jwt.sign({
|
|
type: 'app-under-user',
|
|
version: '0.0.0',
|
|
user_uid: userUid,
|
|
app_uid: 'app-7e2d3016-8d36-456a-9dc7-b75b0f4f7683',
|
|
session: sessionUuid,
|
|
}, authService.global_config.jwt_secret, { expiresIn: 60 });
|
|
|
|
// @ts-expect-error test-only lightweight stub
|
|
authService.get_session_ = vi.fn().mockResolvedValue({
|
|
uuid: sessionUuid,
|
|
user_uid: userUid,
|
|
});
|
|
|
|
const identity = await authService.resolvePrivateBootstrapIdentityFromToken(token);
|
|
|
|
expect(identity).toEqual({
|
|
userUid,
|
|
sessionUuid,
|
|
});
|
|
// @ts-expect-error test-only lightweight stub
|
|
expect(authService.get_session_).toHaveBeenCalledWith(sessionUuid);
|
|
});
|
|
|
|
it('rejects bootstrap identity when session owner does not match token user', async () => {
|
|
const authService = createAuthService();
|
|
const token = jwt.sign({
|
|
type: 'app-under-user',
|
|
version: '0.0.0',
|
|
user_uid: '4b0cecf8-dd6a-4eb5-bcc4-c76cc7e8d7f0',
|
|
app_uid: 'app-7e2d3016-8d36-456a-9dc7-b75b0f4f7683',
|
|
session: 'f9000804-2fd3-4da5-819b-afc5296f90f7',
|
|
}, authService.global_config.jwt_secret, { expiresIn: 60 });
|
|
|
|
// @ts-expect-error test-only lightweight stub
|
|
authService.get_session_ = vi.fn().mockResolvedValue({
|
|
uuid: 'f9000804-2fd3-4da5-819b-afc5296f90f7',
|
|
user_uid: '9885b80e-1a14-4c8d-9e3f-4fa5915b1136',
|
|
});
|
|
|
|
await expect(authService.resolvePrivateBootstrapIdentityFromToken(token))
|
|
.rejects
|
|
.toThrow();
|
|
});
|
|
});
|