Socket
Socket
Sign inDemoInstall

@furystack/rest-service

Package Overview
Dependencies
10
Maintainers
1
Versions
161
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 9.0.1 to 9.0.2

2

esm/helpers.js

@@ -16,3 +16,3 @@ import { HttpAuthenticationSettings } from './http-authentication-settings.js';

*/
export const useHttpAuthentication = (injector, settings) => injector.setExplicitInstance({ ...new HttpAuthenticationSettings(), ...settings }, HttpAuthenticationSettings);
export const useHttpAuthentication = (injector, settings) => injector.setExplicitInstance(Object.assign(new HttpAuthenticationSettings(), settings), HttpAuthenticationSettings);
/**

@@ -19,0 +19,0 @@ * Sets up a static file server

@@ -10,3 +10,3 @@ import type { PhysicalStore, StoreManager } from '@furystack/core';

model: Constructable<TUser>;
getUserStore: (storeManager: StoreManager) => PhysicalStore<TUser, keyof TUser>;
getUserStore: (sm: StoreManager) => PhysicalStore<User, "username", import("@furystack/core").WithOptionalId<User, "username">>;
getSessionStore: (storeManager: StoreManager) => PhysicalStore<TSession, keyof TSession>;

@@ -13,0 +13,0 @@ cookieName: string;

@@ -12,3 +12,3 @@ /// <reference path="server-response-extensions.d.ts" />

export declare class HttpUserContext {
getUserStore: () => import("@furystack/core").PhysicalStore<User, keyof User, import("@furystack/core").WithOptionalId<User, keyof User>>;
getUserStore: () => import("@furystack/core").PhysicalStore<User, "username", import("@furystack/core").WithOptionalId<User, "username">>;
getSessionStore: () => import("@furystack/core").PhysicalStore<DefaultSession, keyof DefaultSession, import("@furystack/core").WithOptionalId<DefaultSession, keyof DefaultSession>>;

@@ -37,5 +37,5 @@ private getUserByName;

authenticateUser(userName: string, password: string): Promise<import("@furystack/core").PartialResult<User, (keyof User)[]>>;
getCurrentUser(request: IncomingMessage): Promise<User>;
getSessionIdFromRequest(request: IncomingMessage): string | null;
authenticateRequest(request: IncomingMessage): Promise<User>;
getCurrentUser(request: Pick<IncomingMessage, 'headers'>): Promise<User>;
getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null;
authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User>;
/**

@@ -47,8 +47,9 @@ * Creates and sets up a cookie-based session for the provided user

*/
cookieLogin(user: User, serverResponse: ServerResponse): Promise<User>;
cookieLogout(request: IncomingMessage, response: ServerResponse): Promise<void>;
cookieLogin(user: User, serverResponse: Pick<ServerResponse, 'setHeader'>): Promise<User>;
cookieLogout(request: Pick<IncomingMessage, 'headers'>, response: Pick<ServerResponse, 'setHeader'>): Promise<void>;
readonly authentication: HttpAuthenticationSettings<User, DefaultSession>;
private readonly storeManager;
private readonly authenticator;
init(): Promise<void>;
}
//# sourceMappingURL=http-user-context.d.ts.map

@@ -98,3 +98,3 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

const [name, value] = val.split('=');
return { name: name.trim(), value: value.trim() };
return { name: name?.trim(), value: value?.trim() };
});

@@ -142,5 +142,5 @@ const sessionCookie = cookies.find((c) => c.name === this.authentication.cookieName);

async cookieLogout(request, response) {
this.user = undefined;
const sessionId = this.getSessionIdFromRequest(request);
response.setHeader('Set-Cookie', `${this.authentication.cookieName}=; Path=/; HttpOnly`);
this.user = undefined;
if (sessionId) {

@@ -152,2 +152,17 @@ const sessionStore = this.getSessionStore();

}
async init() {
this.getUserStore().addListener('onEntityUpdated', ({ id, change }) => {
if (this.user?.username === id) {
this.user = { ...this.user, ...change };
}
});
this.getUserStore().addListener('onEntityRemoved', ({ key }) => {
if (this.user?.username === key) {
this.user = undefined;
}
});
this.getSessionStore().addListener('onEntityRemoved', () => {
this.user = undefined; // as user cannot be determined by the session id anymore
});
}
};

@@ -154,0 +169,0 @@ __decorate([

@@ -267,3 +267,62 @@ import { usingAsync } from '@furystack/utils';

});
describe('Changes in the store during the context lifetime', () => {
it('Should update user roles', () => {
return usingAsync(new Injector(), async (i) => {
await prepareInjector(i);
const ctx = i.getInstance(HttpUserContext);
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username');
userStore.add(testUser);
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw);
await ctx.cookieLogin(testUser, { setHeader: vi.fn() });
const originalUser = await ctx.getCurrentUser(request);
expect(originalUser).toEqual(testUser);
const updatedUser = { ...testUser, roles: ['newFancyRole'] };
await userStore.update(testUser.username, updatedUser);
const updatedUserFromContext = await ctx.getCurrentUser(request);
expect(updatedUserFromContext.roles).toEqual(['newFancyRole']);
await userStore.update(testUser.username, { ...updatedUser, roles: [] });
const reloadedUserFromContext = await ctx.getCurrentUser(request);
expect(reloadedUserFromContext.roles).toEqual([]);
});
});
it('Should remove current user when the user is removed from the store', () => {
return usingAsync(new Injector(), async (i) => {
await prepareInjector(i);
const ctx = i.getInstance(HttpUserContext);
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username');
userStore.add(testUser);
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw);
await ctx.cookieLogin(testUser, { setHeader: vi.fn() });
const originalUser = await ctx.getCurrentUser(request);
expect(originalUser).toEqual(testUser);
await userStore.remove(testUser.username);
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError);
});
});
it('Should remove current user when the session is removed from the store', () => {
return usingAsync(new Injector(), async (i) => {
await prepareInjector(i);
const ctx = i.getInstance(HttpUserContext);
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username');
userStore.add(testUser);
let sessionId = '';
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test');
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw);
await ctx.cookieLogin(testUser, {
setHeader: (_headerName, headerValue) => {
sessionId = headerValue;
return {};
},
});
const originalUser = await ctx.getCurrentUser(request);
expect(originalUser).toEqual(testUser);
const sessionStore = ctx.getSessionStore();
await sessionStore.remove(sessionId);
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError);
});
});
});
});
//# sourceMappingURL=http-user-context.spec.js.map

@@ -16,12 +16,12 @@ /// <reference path="server-response-extensions.d.ts" />

[K: string]: string;
} | undefined) => ActionResult<T>;
}) => ActionResult<T>;
export declare const PlainTextResult: (text: string, statusCode?: number, headers?: {
[K: string]: string;
} | undefined) => ActionResult<string>;
}) => ActionResult<string>;
export declare const XmlResult: (text: string, statusCode?: number, headers?: {
[K: string]: string;
} | undefined) => ActionResult<string>;
}) => ActionResult<string>;
export declare const EmptyResult: (statusCode?: number, headers?: {
[K: string]: string;
} | undefined) => ActionResult<undefined>;
}) => ActionResult<undefined>;
export declare const BypassResult: () => ActionResult<"BypassResult">;

@@ -28,0 +28,0 @@ export type RequestActionOptions<T extends {

@@ -12,4 +12,2 @@ import { Injector } from '@furystack/inject';

import { getPort } from '@furystack/core/port-generator';
class UserWithPassword extends User {
}
const createIntegrationApi = async () => {

@@ -21,3 +19,3 @@ const i = new Injector();

useHttpAuthentication(i, {
getUserStore: (sm) => sm.getStoreFor(UserWithPassword, 'username'),
getUserStore: (sm) => sm.getStoreFor(User, 'username'),
getSessionStore: (sm) => sm.getStoreFor(DefaultSession, 'sessionId'),

@@ -24,0 +22,0 @@ });

import type { RequestAction } from './request-action-implementation.js';
export declare const Validate: <TSchema extends {
definitions: {
[K: string]: any;
[K: string]: {
required?: string[];
} | any;
};

@@ -6,0 +8,0 @@ }>(validationOptions: {

@@ -10,2 +10,4 @@ import { Injector } from '@furystack/inject';

import { getPort } from '@furystack/core/port-generator';
import { getStoreManager, InMemoryStore, User } from '@furystack/core';
import { DefaultSession } from './models/default-session.js';
// To recreate: yarn ts-json-schema-generator -f tsconfig.json --no-type-check -p packages/rest-service/src/validate.integration.schema.ts -o packages/rest-service/src/validate.integration.spec.schema.json

@@ -15,2 +17,4 @@ const createValidateApi = async () => {

const port = getPort();
getStoreManager(injector).addStore(new InMemoryStore({ model: User, primaryKey: 'username' }));
getStoreManager(injector).addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }));
await useRestService({

@@ -17,0 +21,0 @@ injector,

{
"name": "@furystack/rest-service",
"version": "9.0.1",
"version": "9.0.2",
"description": "Repository implementation for FuryStack",

@@ -40,20 +40,20 @@ "type": "module",

"dependencies": {
"@furystack/core": "^14.0.1",
"@furystack/inject": "^11.0.0",
"@furystack/repository": "^9.0.1",
"@furystack/rest": "^7.0.1",
"@furystack/security": "^5.0.1",
"@furystack/utils": "^6.0.1",
"@furystack/core": "^14.0.2",
"@furystack/inject": "^11.0.1",
"@furystack/repository": "^9.0.2",
"@furystack/rest": "^7.0.2",
"@furystack/security": "^5.0.2",
"@furystack/utils": "^7.0.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"path-to-regexp": "^6.2.1",
"ajv-formats": "^3.0.1",
"path-to-regexp": "^6.2.2",
"semaphore-async-await": "^1.5.1"
},
"devDependencies": {
"@furystack/rest-client-fetch": "^7.0.1",
"@types/node": "^20.11.30",
"typescript": "^5.4.3",
"vitest": "^1.4.0"
"@furystack/rest-client-fetch": "^7.0.2",
"@types/node": "^20.12.7",
"typescript": "^5.4.5",
"vitest": "^1.5.0"
},
"gitHead": "1045d854bfd8c475b7035471d130d401417a2321"
}

@@ -28,3 +28,3 @@ import type { User } from '@furystack/core'

settings?: Partial<HttpAuthenticationSettings<TUser, TSession>>,
) => injector.setExplicitInstance({ ...new HttpAuthenticationSettings(), ...settings }, HttpAuthenticationSettings)
) => injector.setExplicitInstance(Object.assign(new HttpAuthenticationSettings(), settings), HttpAuthenticationSettings)

@@ -31,0 +31,0 @@ /**

@@ -14,4 +14,3 @@ import type { PhysicalStore, StoreManager } from '@furystack/core'

public getUserStore: (storeManager: StoreManager) => PhysicalStore<TUser, keyof TUser> = (sm) =>
sm.getStoreFor<TUser, keyof TUser>(User as any, 'username')
public getUserStore = (sm: StoreManager) => sm.getStoreFor(User, 'username')

@@ -18,0 +17,0 @@ public getSessionStore: (storeManager: StoreManager) => PhysicalStore<TSession, keyof TSession> = (sm) =>

@@ -313,2 +313,80 @@ import type { IncomingMessage, ServerResponse } from 'http'

})
describe('Changes in the store during the context lifetime', () => {
it('Should update user roles', () => {
return usingAsync(new Injector(), async (i) => {
await prepareInjector(i)
const ctx = i.getInstance(HttpUserContext)
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username')
userStore.add(testUser)
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw)
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
const originalUser = await ctx.getCurrentUser(request)
expect(originalUser).toEqual(testUser)
const updatedUser = { ...testUser, roles: ['newFancyRole'] }
await userStore.update(testUser.username, updatedUser)
const updatedUserFromContext = await ctx.getCurrentUser(request)
expect(updatedUserFromContext.roles).toEqual(['newFancyRole'])
await userStore.update(testUser.username, { ...updatedUser, roles: [] })
const reloadedUserFromContext = await ctx.getCurrentUser(request)
expect(reloadedUserFromContext.roles).toEqual([])
})
})
it('Should remove current user when the user is removed from the store', () => {
return usingAsync(new Injector(), async (i) => {
await prepareInjector(i)
const ctx = i.getInstance(HttpUserContext)
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username')
userStore.add(testUser)
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw)
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
const originalUser = await ctx.getCurrentUser(request)
expect(originalUser).toEqual(testUser)
await userStore.remove(testUser.username)
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
})
})
it('Should remove current user when the session is removed from the store', () => {
return usingAsync(new Injector(), async (i) => {
await prepareInjector(i)
const ctx = i.getInstance(HttpUserContext)
const userStore = i.getInstance(StoreManager).getStoreFor(User, 'username')
userStore.add(testUser)
let sessionId = ''
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
await i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName').add(pw)
await ctx.cookieLogin(testUser, {
setHeader: (_headerName, headerValue) => {
sessionId = headerValue as string
return {} as ServerResponse
},
})
const originalUser = await ctx.getCurrentUser(request)
expect(originalUser).toEqual(testUser)
const sessionStore = ctx.getSessionStore()
await sessionStore.remove(sessionId as string)
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
})
})
})
})

@@ -87,3 +87,3 @@ import type { IncomingMessage, ServerResponse } from 'http'

public async getCurrentUser(request: IncomingMessage) {
public async getCurrentUser(request: Pick<IncomingMessage, 'headers'>) {
if (!this.user) {

@@ -96,3 +96,3 @@ this.user = await this.authenticateRequest(request)

public getSessionIdFromRequest(request: IncomingMessage): string | null {
public getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null {
if (request.headers.cookie) {

@@ -105,3 +105,3 @@ const cookies = request.headers.cookie

const [name, value] = val.split('=')
return { name: name.trim(), value: value.trim() }
return { name: name?.trim(), value: value?.trim() }
})

@@ -116,3 +116,3 @@ const sessionCookie = cookies.find((c) => c.name === this.authentication.cookieName)

public async authenticateRequest(request: IncomingMessage): Promise<User> {
public async authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User> {
// Basic auth

@@ -146,3 +146,3 @@ if (this.authentication.enableBasicAuth && request.headers.authorization) {

*/
public async cookieLogin(user: User, serverResponse: ServerResponse): Promise<User> {
public async cookieLogin(user: User, serverResponse: Pick<ServerResponse, 'setHeader'>): Promise<User> {
const sessionId = randomBytes(32).toString('hex')

@@ -155,6 +155,7 @@ await this.getSessionStore().add({ sessionId, username: user.username })

public async cookieLogout(request: IncomingMessage, response: ServerResponse) {
public async cookieLogout(request: Pick<IncomingMessage, 'headers'>, response: Pick<ServerResponse, 'setHeader'>) {
this.user = undefined
const sessionId = this.getSessionIdFromRequest(request)
response.setHeader('Set-Cookie', `${this.authentication.cookieName}=; Path=/; HttpOnly`)
this.user = undefined
if (sessionId) {

@@ -175,2 +176,20 @@ const sessionStore = this.getSessionStore()

private declare readonly authenticator: PasswordAuthenticator
public async init() {
this.getUserStore().addListener('onEntityUpdated', ({ id, change }) => {
if (this.user?.username === id) {
this.user = { ...this.user, ...change }
}
})
this.getUserStore().addListener('onEntityRemoved', ({ key }) => {
if (this.user?.username === key) {
this.user = undefined
}
})
this.getSessionStore().addListener('onEntityRemoved', () => {
this.user = undefined // as user cannot be determined by the session id anymore
})
}
}

@@ -14,6 +14,2 @@ import { Injector } from '@furystack/inject'

class UserWithPassword extends User {
declare password: string
}
interface IntegrationTestApi extends RestApi {

@@ -42,3 +38,3 @@ GET: {

useHttpAuthentication(i, {
getUserStore: (sm) => sm.getStoreFor(UserWithPassword, 'username'),
getUserStore: (sm) => sm.getStoreFor(User, 'username'),
getSessionStore: (sm) => sm.getStoreFor(DefaultSession, 'sessionId'),

@@ -45,0 +41,0 @@ })

@@ -11,2 +11,4 @@ import { Injector } from '@furystack/inject'

import { getPort } from '@furystack/core/port-generator'
import { getStoreManager, InMemoryStore, User } from '@furystack/core'
import { DefaultSession } from './models/default-session.js'

@@ -18,2 +20,6 @@ // To recreate: yarn ts-json-schema-generator -f tsconfig.json --no-type-check -p packages/rest-service/src/validate.integration.schema.ts -o packages/rest-service/src/validate.integration.spec.schema.json

const port = getPort()
getStoreManager(injector).addStore(new InMemoryStore({ model: User, primaryKey: 'username' }))
getStoreManager(injector).addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
await useRestService<ValidationApi>({

@@ -61,2 +67,3 @@ injector,

})
return {

@@ -63,0 +70,0 @@ dispose: injector.dispose.bind(injector),

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc