This commit is contained in:
2023-09-23 13:24:37 +00:00
parent 9c9d9e9545
commit 438df6cd3c
60 changed files with 190 additions and 1100 deletions
+5 -7
View File
@@ -1,10 +1,8 @@
import type { RNG } from "../../../server/util/RNG.js";
import { sha512 } from '../../../server/util/sha2.js';
import { convertBytesLikeToUint8Array } from "@sammo/util/convertBytesLikeToUint8Array.js";
import type { BytesLike } from "../../../server/util/BytesLike.js";
import { delay } from "./delay.js";
import type { RNG } from "./RNG.js";
import { sha512 } from './SHA2.js';
import { convertBytesLikeToUint8Array } from "@sammo/util/converter";
import type { BytesLike } from "@sammo/util";
import { delay } from "@sammo/util";
const maxRngSupportBit = 53;
const maxInt = 0x1f_ffff_ffff_ffff; // NOTE: b 0, 10000110011, 11...11
+4
View File
@@ -6,6 +6,10 @@
"scripts": {
"build": "tsc --build"
},
"exports": {
".": "./dist/index.js",
"./exports": "./dist/exports.js"
},
"author": "",
"type": "module",
"license": "MIT",
-9
View File
@@ -1,9 +0,0 @@
import type { structure } from "../apiStructure/sammoGatewayAPI.js";
import type { APINamespaceType } from "./defs.js";
import { GetGameLoginToken } from "./GatewayAPI/GetGameLoginToken.js";
import { Login } from "./GatewayAPI/Login/index.js";
export const sammoGatewayAPI = {
Login,
GetGameLoginToken
} satisfies APINamespaceType<typeof structure>;
+9
View File
@@ -0,0 +1,9 @@
export type {
ServerActionType,
UserIDType,
IUser,
} from "./schema/User.ts";
export type {
ILoginToken
} from "./schema/LoginToken.ts";
+1 -1
View File
@@ -17,7 +17,7 @@ const validGatewayActionTypeList = [
export type GatewayActionType = typeof validGatewayActionTypeList[number];
interface IUser {
export interface IUser {
_id: UserIDType;
oauthID?: bigint;
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@sammo/server",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build"
},
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/api_def": "workspace:^",
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"dotenv": "^16.3.1",
"mongoose": "^7.4.3"
},
"devDependencies": {
"@types/node": "^20.6.3"
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../../@strpc/express"
},
{
"path": "../util"
},
{
"path": "../crypto"
},
{
"path": "../server_util"
},
]
}
+5 -3
View File
@@ -8,9 +8,11 @@
},
"exports": {
".": "./dist/index.js",
"./converter": "./dist/converter.js",
"./datetime": "./dist/datetime.js",
"./korean": "./dist/korean.js"
"./converter": "./dist/converter/index.js",
"./datetime": "./dist/datetime/index.js",
"./korean": "./dist/korean/index.js",
"./josa_util": "./dist/korean/josa_util.js",
"./string": "./dist/string/index.js"
},
"type": "module",
"keywords": [],
@@ -1,17 +0,0 @@
import type { BytesLike } from "./BytesLike.js";
export function convertBytesLikeToArrayBuffer(data: BytesLike, encodeUTF8 = true): ArrayBuffer{
if (data instanceof ArrayBuffer) {
return data;
}
if (data instanceof Uint8Array) {
return data.buffer;
}
if (typeof(data) === 'string'){
if(encodeUTF8){
return (new TextEncoder()).encode(data);
}
return new Uint8Array(data.split('').map(s=>s.codePointAt(0) as number));
}
return data.buffer;
}
@@ -1,5 +1,7 @@
import type { BytesLike } from "./BytesLike.js";
import type { BytesLike, BufferSource } from "../types.js";
export function convertBytesLikeToUint8Array(data: string, encodeUTF8: boolean): Uint8Array;
export function convertBytesLikeToUint8Array(data: BufferSource | string): Uint8Array;
export function convertBytesLikeToUint8Array(data: BytesLike, encodeUTF8 = true): Uint8Array {
if (data instanceof Uint8Array) {
return data;
@@ -7,11 +9,20 @@ export function convertBytesLikeToUint8Array(data: BytesLike, encodeUTF8 = true)
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
if (data instanceof SharedArrayBuffer) {
return new Uint8Array(data);
}
if (data instanceof Uint8Array) {
return data;
}
if (ArrayBuffer.isView(data)) {
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
if (typeof (data) === 'string') {
if(encodeUTF8){
return (new TextEncoder()).encode(data);
}
return new Uint8Array(data.split('').map(s=>s.codePointAt(0) as number));
}
return new Uint8Array(data.buffer);
throw new Error(`Unknown data type ${typeof (data)}`);
}
-1
View File
@@ -1,6 +1,5 @@
export * from "./combineObject.js";
export * from "./combineArray.js";
export * from "./convertBytesLikeToArrayBuffer.js";
export * from "./convertBytesLikeToUint8Array.js";
export * from "./convertIDArray.js";
export * from "./convertIterableToMap.js"
@@ -1,5 +1,5 @@
import type { ValuesOf } from "./defs.js";
import type { ValuesOf } from "../types.js";
import { zip } from "lodash-es";
export function merge2DArrToObjectArr<T extends Record<string, unknown>>(column: (keyof T)[], list: ValuesOf<T>[][]): T[]{
+10 -54
View File
@@ -3,58 +3,19 @@ export * from "./jsonify.js"
export * from "./error.js"
export * from "./unwrap.js"
export * from "./types.js"
export * from "./web.js"
export * from "./strongType.js"
export * as converter from "./converter/index.js"
export * as datetime from "./datetime/index.js"
export * as korean from "./korean/index.js"
import type { WrappedBuffer } from "./types.js"
export function isEmail(addr: string): boolean {
return String(addr)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
) !== null;
}
export function isIPAddr4(addr: string): boolean {
if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(addr)) {
return (true)
}
return false;
}
export * as datetime from "./datetime/index.js";
export * as korean from "./korean/index.js";
export * as string from "./string/index.js";
export function calcBase64Len(length: number) {
return ((4 * length / 3) + 3) & ~3;
}
/** ArrayBuffer,Uint8Array,Buffer를 WrappedBuffer로 변환
*/
export function wrapBuffer<T extends WrappedBuffer>(buffer: BufferSource): T {
/**
* Buffer라면 wrapBuffer없이 그대로 대입 가능
* ArrayBuffer, Uint8Array 등이라면 Buffer.from을 사용 가능
* DataView 등인 경우는 ArrayBufferView 활용
* 그냥 써도 되지만, 위 방법보다 아무렇게나 쓰기 좋다는 장점은 있음
*/
if (buffer instanceof Buffer) {
return buffer as T;
}
if (buffer instanceof ArrayBuffer) {
return Buffer.from(buffer) as T;
}
if (buffer instanceof SharedArrayBuffer) {
return Buffer.from(buffer) as T;
}
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength) as T;
}
/** 호출할 필요 없이 바로 Buffer에 대입해도 되지만, 누군가 찾을 수 있어서. */
export function unwrapBuffer<T extends WrappedBuffer>(buffer: T): Buffer {
return buffer;
}
export function delay(time: number): Promise<void>;
export function delay<T>(time: number, result: T): Promise<T>;
export function delay<T = undefined>(time: number, result?: T): Promise<T | void> {
@@ -65,15 +26,6 @@ export function delay<T = undefined>(time: number, result?: T): Promise<T | void
);
}
export function base64FromWrappedBuffer<T extends WrappedBuffer>(buffer: T | ArrayBuffer | Uint8Array): Base64String<T> {
return Buffer.from(buffer).toString('base64');
}
export function wrappedBufferFromBase64<T extends WrappedBuffer>(base64: Base64String<T>): T {
return Buffer.from(base64, 'base64') as T;
}
export function isBufferSource(obj: unknown): obj is BufferSource {
if (obj instanceof ArrayBuffer){
return true;
@@ -85,4 +37,8 @@ export function isBufferSource(obj: unknown): obj is BufferSource {
return true;
}
return false;
}
export function isNotNull<T>(value: T|null|undefined):value is T{
return value !== null && value !== undefined;
}
@@ -1,4 +1,4 @@
import { unwrap_err } from "./unwrap_err.js";
import { unwrap_err } from "../unwrap.js";
// https://github.com/coxcore/postposition 의 php 버전을 다시 typescript로 재 작성
const KO_START_CODE = 44032;
+2 -1
View File
@@ -1,4 +1,5 @@
export * from "./automata초성.js";
export * from "./convertSearch초성.js"
export * from "./filter초성.js";
export * from "./filter초성withAlphabet.js";
export * from "./filter초성withAlphabet.js";
export * as JosaUtil from "./JosaUtil.js";
+5
View File
@@ -0,0 +1,5 @@
export * from "./mb_strimwidth.js"
export * from "./mb_strwidth.js"
export * from "./nl2br.js"
export * from "./numberWithCommas.js"
export * from "./randStr.js";
@@ -3,10 +3,10 @@ const charList = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
];
] as const;
export function randStr(len: number): string {
const result = [];
const result: string[] = [];
const charListLen = charList.length;
let isStart = true;
+51
View File
@@ -0,0 +1,51 @@
/** Buffer이지만 ts에서 타입 구분 편의를 제공 */
export interface WrappedBuffer extends Buffer {
/** 타입구분자. 항상 undefined일 것이다 */
_w_type?: string;
}
/** ArrayBuffer,Uint8Array,Buffer를 WrappedBuffer로 변환
*/
export function wrapBuffer<T extends WrappedBuffer>(buffer: BufferSource): T {
/**
* Buffer라면 wrapBuffer없이 그대로 대입 가능
* ArrayBuffer, Uint8Array 등이라면 Buffer.from을 사용 가능
* DataView 등인 경우는 ArrayBufferView 활용
* 그냥 써도 되지만, 위 방법보다 아무렇게나 쓰기 좋다는 장점은 있음
*/
if (buffer instanceof Buffer) {
return buffer as T;
}
if (buffer instanceof ArrayBuffer) {
return Buffer.from(buffer) as T;
}
if (buffer instanceof SharedArrayBuffer) {
return Buffer.from(buffer) as T;
}
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength) as T;
}
/** 호출할 필요 없이 바로 Buffer에 대입해도 되지만, 누군가 찾을 수 있어서. */
export function unwrapBuffer<T extends WrappedBuffer>(buffer: T): Buffer {
return buffer;
}
/**
* base64 string에 내부 타입으로 WrappedBuffer를 보관한 형태
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export type Base64String<T extends Buffer> = string & {
/** 타입구분자. 항상 undefined일 것이다 */
_b_type?: T;
}
export function base64FromWrappedBuffer<T extends WrappedBuffer>(buffer: T | ArrayBuffer | Uint8Array): Base64String<T> {
return Buffer.from(buffer).toString('base64');
}
export function wrappedBufferFromBase64<T extends WrappedBuffer>(base64: Base64String<T>): T {
return Buffer.from(base64, 'base64') as T;
}
+3 -16
View File
@@ -6,23 +6,10 @@ export type IDItem<T> = {
id: T;
};
export type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
/** Buffer이지만 ts에서 타입 구분 편의를 제공 */
export interface WrappedBuffer extends Buffer {
/** 타입구분자. 항상 undefined일 것이다 */
_w_type?: string;
}
export type BufferSource = ArrayBufferView | ArrayBuffer | SharedArrayBuffer;
export type BytesLike = BufferSource | string;
/**
* base64 string에 내부 타입으로 WrappedBuffer를 보관한 형태
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export type Base64String<T extends Buffer> = string & {
/** 타입구분자. 항상 undefined일 것이다 */
_b_type?: T;
}
+34
View File
@@ -0,0 +1,34 @@
import { unwrap } from "./unwrap.js";
export function isEmail(addr: string): boolean {
return String(addr)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
) !== null;
}
export function isIPAddr4(addr: string): boolean {
if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(addr)) {
return (true)
}
return false;
}
export function hexToRgb(hex: string): { r: number; g: number; b: number; } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
export function isBrightColor(color: string): boolean {
const cv = unwrap(hexToRgb(color));
if ((cv.r * 0.299 + cv.g * 0.587 + cv.b * 0.114) > 140) {
return true;
} else {
return false;
}
}
View File
-33
View File
@@ -1,33 +0,0 @@
import type { GatewayActionType, ServerActionType } from "../schema/gateway/User.js";
import type { SessionCtx } from "./StartSession.js";
import type { ProcDecorator } from "./base.js";
export type GatewayLoginCtx = {
userID: number;
userName: string;
userLevel: number;
allowServerAction: Map<string, Set<ServerActionType>>;
allowGatewayAction: Set<GatewayActionType>;
loginDate: Date;
}
export const loginCtxSessionKey = 'loginCtx';
export function ReqGatewayLogin<Q extends SessionCtx>(): ProcDecorator<GatewayLoginCtx & Q, Q> {
return (ctx) => {
const loginCtx = ctx.session.getItem<GatewayLoginCtx>(loginCtxSessionKey);
if(!loginCtx){
return [{
result: false,
type: 'Required Login',
info: 'ReqLogin'
}, ctx];
}
return [{
result: true,
},{
...loginCtx,
...ctx,
}];
}
}
-58
View File
@@ -1,58 +0,0 @@
import { type Session, type SessionData } from "express-session";
import type { Empty, ProcDecoratorGenerator } from "./base.js";
export type SessionCtx = {
session: {
clear: () => Promise<void>;
removeItem: (key: string) => boolean;
getItem: <T>(key: string) => T | undefined;
setItem: (key: string, value: unknown) => void;
raw: Session & Partial<SessionData> & Record<string, unknown>;
}
}
export function StartSession<Q extends object = Empty>(): ProcDecoratorGenerator<SessionCtx, Q> {
return (inCtx, req) => {
if (!req.session) {
throw 'Express-session required';
}
const sessionObj = {
raw: req.session
} as SessionCtx['session'];
sessionObj.clear = () => {
return new Promise((resolve) => {
sessionObj.raw = req.session.regenerate(resolve) as SessionCtx['session']['raw'];
})
}
sessionObj.removeItem = (key: string): boolean => {
if (key in sessionObj.raw) {
delete sessionObj.raw[key];
return true;
}
return false;
}
sessionObj.getItem = <T>(key: string): T | undefined => {
if (!(key in sessionObj.raw)) {
return undefined;
}
return sessionObj.raw[key] as T;
}
sessionObj.setItem = <T>(key: string, value: T | undefined): void => {
if (value === undefined) {
sessionObj.removeItem(key);
return;
}
sessionObj.raw[key] = value;
}
return [
{
result: true,
},
{
session: sessionObj,
...inCtx,
}
]
}
}
-183
View File
@@ -1,183 +0,0 @@
import type { Request, Response } from "express";
type MayBePromise<T> = T | Promise<T>;
export type Empty = Record<string, never>;
export type DecoratorResultTrue = {
result: true;
type?: string;
info?: string;
};
export type DecoratorResultFalse = {
result: false;
type: string;
info: string;
}
export type DecoratorResult = DecoratorResultTrue | DecoratorResultFalse;
export type DecoratorStack = DecoratorResult[];
export interface ProcDecorator<Out extends object, In = Empty> {
(inCtx: In & Partial<Out>, req: Request, res: Response)
: MayBePromise<[DecoratorResultTrue, Out] | [DecoratorResultFalse, In & Partial<Out>]>;
}
export interface PostProcDecorator<T extends object> {
(ctx: T, preResult: DecoratorResult, req: Request, res: Response, isValidRoute: boolean): MayBePromise<[DecoratorResult, T]>;
}
export interface ProcDecoratorRunner<Out extends object, In> {
(inCtx: In, req: Request, res: Response): MayBePromise<[DecoratorStack, Out]>;
}
export interface PostProcDecoratorRunner<T extends object> {
(ctx: T, preResult: DecoratorStack, req: Request, res: Response, isValidRoute: boolean): MayBePromise<[DecoratorStack, T]>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PlainDecorator = ProcDecorator<any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PlainPostDecorator = PostProcDecorator<any>;
export type ProcDecoratorGenerator<Out extends object, In = Empty> = ProcDecorator<Out & In, In>;
export type ProcDecoratorPrePostGenerator<Out extends object, In> = [ProcDecorator<Out & In, In>, PostProcDecorator<Out & In>];
export type ProcDecoratorChain = readonly ((() => PlainDecorator) | (() => [PlainDecorator, PlainPostDecorator]))[];
export type ResolveChain<T> = T extends undefined ? Empty : T extends ProcDecoratorChain ? Resolve<PackChain<T>> : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ParseInType<T> = T extends ProcDecorator<any, infer A> ? object extends A ? A : never : never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ParseOutType<T> = T extends ProcDecorator<infer B, object> ? B : never;
export const EmptyProcDecorator: readonly [ProcDecoratorRunner<Empty, Empty>, PostProcDecoratorRunner<Empty>] = [
async (ctx) => [[], ctx], async (ctx, stack) => [stack, ctx]
];
export function declProcDecorators<T extends ProcDecoratorChain>(...decorators: T) {
type OutType = Resolve<PackChain<T>>;
const preDecorator: PlainDecorator[] = [];
const postDecorator: (PlainPostDecorator | undefined)[] = [];
if (decorators) {
for (const procGen of decorators) {
const proc = procGen();
if (Array.isArray(proc)) {
preDecorator.push(proc[0]);
postDecorator.push(proc[1]);
}
else {
preDecorator.push(proc);
postDecorator.push(undefined);
}
}
}
const packedDecorators: readonly [ProcDecoratorRunner<OutType, Empty>, PostProcDecoratorRunner<OutType>] = [
async (ctx, req, res) => {
let rctx = ctx as unknown as OutType;
const decoratorStack: DecoratorStack = [];
if (!preDecorator.length) {
return [decoratorStack, rctx];
}
for (const [idx, proc] of preDecorator.entries()) {
try {
const [stackResult, newCtx] = await proc(rctx, req, res);
decoratorStack.push(stackResult);
if (stackResult.result) {
rctx = newCtx;
continue;
}
return [decoratorStack, newCtx];
}
catch (e) {
while (decoratorStack.length > idx) {
decoratorStack.pop();
}
decoratorStack.push({
result: false,
type: 'PreThrow',
info: `internal error: ${e}`,
});
return [decoratorStack, rctx];
}
}
return [decoratorStack, rctx];
},
async (ctx, stack, req, res) => {
if (!postDecorator.length) {
return [stack, ctx];
}
let isValidRoute = stack.length === postDecorator.length && stack.every(v => v.result);
for (let idx = stack.length - 1; idx >= 0; idx--) {
const preStackResult = stack[idx] as DecoratorResult;
if (!preStackResult.result) {
continue;
}
const proc = postDecorator[idx];
if (!proc) {
if (isValidRoute) {
stack.pop();
}
continue;
}
try {
const [postStackResult, nextCtx] = await proc(ctx, preStackResult, req, res, isValidRoute);
if (postStackResult.result) {
if (isValidRoute) {
stack.pop();
}
ctx = nextCtx;
continue;
}
isValidRoute = false;
stack[idx] = postStackResult;
ctx = nextCtx;
}
catch (e) {
isValidRoute = false;
stack[idx] = {
result: false,
type: 'PostThrow',
info: `internal error: ${e}`,
};
}
}
return [stack, ctx];
},
] as const;
return packedDecorators;
}
type Compose2<D extends object, C, B, A = Empty> = B extends C ? ProcDecorator<D & B, A> : never;
type PD1<B extends object, A extends object> = () => ProcDecorator<B, A>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PD2<B extends object, A extends object> = () => [ProcDecorator<B, A>, PostProcDecorator<any>];
export type PackChain<T> =
T extends readonly [] ? ProcDecorator<Empty, Empty> :
T extends readonly [PD1<infer B, infer A>] ? ProcDecorator<B, A> :
T extends readonly [PD2<infer B, infer A>] ? ProcDecorator<B, A> :
T extends readonly [PD1<infer B, infer A>, PD1<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<D, C, B, A>, ...R]> :
T extends readonly [PD2<infer B, infer A>, PD1<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<D, C, B, A>, ...R]> :
T extends readonly [PD1<infer B, infer A>, PD2<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<D, C, B, A>, ...R]> :
T extends readonly [PD2<infer B, infer A>, PD2<infer D, infer C>, ... infer R] ? PackChain<[() => Compose2<D, C, B, A>, ...R]> :
never;
type Resolve<T> = T extends ProcDecorator<infer B, infer A> ? Empty extends A ? B : never : never;
-38
View File
@@ -1,38 +0,0 @@
export function APIPathGen<T extends object, V>(
obj: T,
callback: (path: string[], tail: V, pathParam?: Record<string, string | number>) => unknown,
path: string[] = [],
): T {
const map = new Map<string, unknown>();
return new Proxy(obj, {
get(target, key) {
if(typeof key === 'symbol'){
throw new Error('Symbol is not supported');
}
const cachedResult = map.get(key);
if(cachedResult !== undefined){
return cachedResult;
}
const nextPath = [...path, key];
let next: T[keyof T];
if (key in target) {
next = target[key as keyof typeof target];
}
else {
throw `${nextPath} is not exists`;
}
if (typeof (next) === 'function') {
const result = callback(nextPath, next);
map.set(key, result);
return result;
}
const result = APIPathGen<T[keyof T], V>(next, callback, nextPath);
map.set(key, result);
return result;
}
}) as T;
};
-95
View File
@@ -1,95 +0,0 @@
import isArray from "lodash-es/isArray";
import isEmpty from "lodash-es/isEmpty";
import ky from "ky";
import type { HttpMethod, InvalidResponse, RawArgType, ValidResponse } from "../apiStructure/defs.js";
export async function callClientAPI<ResultType extends ValidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
paramArgs: Record<string, string | number> | undefined
): Promise<ResultType>;
export async function callClientAPI<ResultType extends ValidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
paramArgs: Record<string, string | number> | undefined,
returnError: undefined
): Promise<ResultType>;
export async function callClientAPI<ResultType extends ValidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
paramArgs: Record<string, string | number> | undefined,
returnError: false
): Promise<ResultType>;
export async function callClientAPI<ResultType extends ValidResponse, ErrorType extends InvalidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
paramArgs: Record<string, string | number> | undefined,
returnError: true
): Promise<ResultType | ErrorType>;
export async function callClientAPI<ResultType extends ValidResponse, ErrorType extends InvalidResponse>(
method: HttpMethod,
apiRoot: string,
path: string | string[],
args: RawArgType,
paramArgs: Record<string, string | number> | undefined,
returnError?: boolean
): Promise<ResultType | ErrorType> {
if (isArray(path)) {
path = [apiRoot, ...path].join("/");
}
else if (path.startsWith("/")) {
path = `${apiRoot}${path}`;
}
else {
path = `${apiRoot}/${path}`;
}
if (args && isEmpty(args)) {
args = undefined;
}
const result = (await (() => {
if (method == "get") {
return ky(path, {
searchParams: {
...paramArgs,
...(args as typeof paramArgs),
},
method,
headers: {
"content-type": "application/json",
},
timeout: 30000,
retry: 0,
});
}
return ky(path, {
searchParams: {
...paramArgs,
},
method,
json: args,
headers: {
"content-type": "application/json",
},
timeout: 30000,
retry: 0,
});
})().json()) as ErrorType | ResultType;
if (!result.result) {
if (returnError) {
return result;
}
throw result.reason;
}
return result;
}
-16
View File
@@ -1,16 +0,0 @@
import { APIPathGen } from "./APIPathGen.js";
import type { APITail, RawArgType } from "../apiStructure/defs.js";
import { structure } from "../apiStructure/sammoGatewayAPI.js";
import { callClientAPI } from "./generator.js";
const apiRoot = process.env.API_ROOT_PATH ?? process.env.VITE_API_ROOT_PATH ?? '/rootAPI';
export const SammoGatewayAPI = APIPathGen(structure, (path: string[], tail: APITail, pathParam) => {
const method = tail.reqType;
return (args?: RawArgType, returnError?: boolean) => {
if (returnError) {
return callClientAPI(method, apiRoot, path.join('/'), args, pathParam, returnError);
}
return callClientAPI(method, apiRoot, path.join('/'), args, pathParam);
};
});
-23
View File
@@ -1,23 +0,0 @@
import { Mongoose, Schema, model } from "mongoose";
export interface IServerConfig {
allowLogin: boolean;
allowOAuthLogin: boolean;
allowRegister: boolean;
};
export const ServerConfig = new Schema<IServerConfig>({
allowLogin: { type: Boolean, required: true },
allowOAuthLogin: { type: Boolean, required: true },
allowRegister: { type: Boolean, required: true },
}, {
autoIndex: false,
autoCreate: false,
capped: {
max: 1,
}
})
;
//단일 document로 관리
export default (conn:Mongoose)=>conn.model<IServerConfig>('ServerConfig', ServerConfig);
-90
View File
@@ -1,90 +0,0 @@
import { Schema, model } from 'mongoose';
export type UserIDType = number;
const validOAuthTypeList = ['KAKAO', 'NONE'] as const;
export type validOAuthType = typeof validOAuthTypeList[number];
const validServerActionTypeList = [
'Update', 'UpdateByGitPath', 'ShowErrorLog',
'CloseServer', 'OpenServer', 'StopAndResumeServer', 'OpenVote',
] as const;
export type ServerActionType = typeof validServerActionTypeList[number];
const validGatewayActionTypeList = [
'Update', 'UpdateByGitPath', 'ShowErrorLog', 'ResetUserPassword', 'ChangeGatewayState', 'DeleteUser',
] as const;
export type GatewayActionType = typeof validGatewayActionTypeList[number];
interface IUser {
_id: UserIDType;
oauthID?: bigint;
id: string;
email: string;
oauthType: validOAuthType;
oauthInfo?: object;
tokenValidUntil?: Date;
userSalt: string;
hashedPassword: string;
allowThirdPartyUse: boolean;
userName: string;
allowServerAction?: Map<string, ServerActionType[]>;
allowGatewayAction?: ServerActionType[];
penalty?: Map<string, Date>;
picture?: string;
useImgSvr?: boolean;
regDate: Date;
deleteAfter?: Date;
}
export const User = new Schema<IUser>({
_id: { type: Number, required: true },
oauthID: { type: BigInt, required: false },
id: { type: String, required: true },
email: { type: String, required: true },
oauthType: { type: String, required: true, enum: validOAuthTypeList },
oauthInfo: { type: Object, required: false },
tokenValidUntil: { type: Date, required: false },
userSalt: { type: String, required: true },
hashedPassword: { type: String, required: true },
allowThirdPartyUse: { type: Boolean, required: true },
userName: { type: String, required: true },
allowServerAction: {
type: Map, required: false, of: {
type: Array,
of: { type: String, enum: validServerActionTypeList }
}
},
allowGatewayAction: {
type: Array, required: false, of: {
type: String, enum: validGatewayActionTypeList
}
},
penalty: { type: Map, required: false, of: Date },
picture: { type: String, required: false },
useImgSvr: { type: Boolean, required: false },
regDate: { type: Date, required: true },
deleteAfter: { type: Date, required: false },
}, { autoIndex: false, autoCreate: false })
.index({ id: 1 }, { unique: true })
.index({ email: 1 }, { unique: true })
.index({ oauthID: 1 }, { unique: true, sparse: true })
.index({ deleteAfter: 1 }) // 자동 삭제 아님!
;
export default model<IUser>('User', User);
-30
View File
@@ -1,30 +0,0 @@
import { Schema, model, } from 'mongoose';
import { type UserIDType } from './User.js';
export type LogType = 'register'
| 'login_pw' | 'login_token' | 'login_oauth' | 'logout'
| 'change_pw' | 'reset_pw';
const LogTypeList: LogType[] = [
'register',
'login_pw', 'login_token', 'login_oauth', 'logout',
'change_pw', 'reset_pw',
];
interface IUserLog {
userID: UserIDType;
logDate: Date;
logType: LogType;
action: object;
}
export const UserLog = new Schema<IUserLog>({
userID: { type: Number, required: true },
logDate: { type: Date, required: true },
logType: { type: String, required: true, enum: LogTypeList },
action: { type: Object, required: true },
}, { autoIndex: false, autoCreate: false, })
.index({ userID: 1, logDate: 1 })
.index({ logDate: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 365 * 3 })
;
export default model<IUserLog>('UserLog', UserLog);
-3
View File
@@ -1,3 +0,0 @@
export type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
-4
View File
@@ -1,4 +0,0 @@
export class NotNullExpected extends TypeError {
public override name = 'NotNullExpected';
}
-14
View File
@@ -1,14 +0,0 @@
export class RuntimeError extends Error {
public override name = 'RuntimeError';
constructor(public override message: string = '') {
super(message);
}
override toString(): string {
if (this.message) {
return this.name + ': ' + this.message;
}
else {
return this.name;
}
}
}
-198
View File
@@ -1,198 +0,0 @@
export interface StateIncrementer {
(state: number): Promise<number>;
}
export type NumberGroup = {
start: number;
remain: number;
}
const AllocatorGroupBucketSize = 100;
const AllocatorPreserveThreshold = 50;
export function* numberGenerator(numberGroups: NumberGroup[]): Generator<number, null> {
for (const numberGroup of numberGroups) {
for (let i = 0; i < numberGroup.remain; i++) {
yield numberGroup.start + i;
}
}
return null;
}
export class UniqueNumberAllocator {
private preservedNumbers: NumberGroup[];
private incrementer: StateIncrementer;
private totalRemain: number;
private promisedCnt: number;
private promise: Promise<void>;
public constructor(incrementer: StateIncrementer, initialState?: NumberGroup) {
this.incrementer = incrementer;
if (!initialState) {
this.preservedNumbers = [];
this.totalRemain = 0;
}
else {
if (initialState.remain < 1) {
throw new Error('initialState.remain must be greater than 0');
}
this.preservedNumbers = [initialState];
this.totalRemain = initialState.remain;
}
this.promisedCnt = 0;
this.promise = Promise.resolve();
this.preserveNext();
}
public static async generate(incrementer: StateIncrementer): Promise<UniqueNumberAllocator> {
const initialStateEnd = await incrementer(AllocatorGroupBucketSize);
const initialState: NumberGroup = {
start: initialStateEnd - AllocatorGroupBucketSize + 1,
remain: AllocatorGroupBucketSize,
}
return new UniqueNumberAllocator(incrementer, initialState);
}
private preserveNext(): void {
if (this.promisedCnt + this.totalRemain >= AllocatorGroupBucketSize) {
return;
}
if (this.totalRemain >= AllocatorPreserveThreshold) {
return;
}
const waiter = this.promise;
this.promise = (async () => {
this.promisedCnt += AllocatorGroupBucketSize;
const nextP = this.incrementer(AllocatorGroupBucketSize);
await waiter;
const next = await nextP;
if (this.preservedNumbers.length > 0) {
const last = this.preservedNumbers[this.preservedNumbers.length - 1];
if (last.start + last.remain === next - AllocatorGroupBucketSize + 1) {
// 마지막 그룹과 연속됨
last.remain += AllocatorGroupBucketSize;
this.totalRemain += AllocatorGroupBucketSize;
this.promisedCnt -= AllocatorGroupBucketSize;
return;
}
}
const nextGroup: NumberGroup = {
start: next - AllocatorGroupBucketSize + 1,
remain: AllocatorGroupBucketSize,
}
this.preservedNumbers.push(nextGroup);
this.totalRemain += AllocatorGroupBucketSize;
this.promisedCnt -= AllocatorGroupBucketSize;
})();
}
public async allocateOne(): Promise<number> {
if (this.totalRemain > 0) {
const head = this.preservedNumbers[0];
if (head.remain > 1) {
const next = head.start;
head.start++;
head.remain--;
this.totalRemain--;
this.preserveNext();
return next;
}
else {
this.preservedNumbers.shift();
this.totalRemain--;
this.preserveNext();
return head.start;
}
}
const next = (await this.allocate(1)).next().value;
if (next === null) {
throw new Error('incrementer failed to allocate enough numbers');
}
return next;
}
public async allocate(n: number): Promise<Generator<number, null>> {
n = Math.ceil(n);
if (n < 1) {
throw new Error('n must be greater than 0');
}
if (n > this.totalRemain + this.promisedCnt) {
const endP = this.incrementer(n);
this.preserveNext();
const end = await endP;
return numberGenerator([{
start: end - n + 1,
remain: n,
}]);
}
const result: NumberGroup[] = [];
if (this.totalRemain > 0) {
const head = this.preservedNumbers[0];
const headRemain = head.remain;
if (n < headRemain) {
result.push({
start: head.start,
remain: n,
});
head.start += n;
head.remain -= n;
this.totalRemain -= n;
this.preserveNext();
return numberGenerator(result);
}
if (n === headRemain) {
this.preservedNumbers.shift();
result.push(head);
this.totalRemain -= n;
this.preserveNext();
return numberGenerator(result);
}
}
//assert n <= this.totalRemain + this.promisedRemain
if (n > this.totalRemain) {
await this.promise;
if (n > this.totalRemain) {
throw new Error('incrementer failed to allocate enough numbers');
}
}
this.totalRemain -= n;
this.preserveNext();
let remain = n;
let idx = 0;
while (remain > 0) {
const head = this.preservedNumbers[idx];
if (head.remain <= remain) {
remain -= head.remain;
result.push(head);
idx++;
continue;
}
result.push({
start: head.start,
remain: remain,
});
head.start += remain;
head.remain -= remain;
remain = 0;
break;
}
this.preservedNumbers = this.preservedNumbers.slice(idx);
return numberGenerator(result);
}
}
-34
View File
@@ -1,34 +0,0 @@
import { webcrypto } from "crypto";
const subtle = webcrypto.subtle;
type BufferSource = ArrayBufferView | ArrayBuffer;
export async function AES_GCM_Encrypt(key: BufferSource, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
const keyObj = await subtle.importKey("raw", key, {
name: "AES-GCM",
}, false, ["encrypt"]);
const ciphertext = await subtle.encrypt({
name: "AES-GCM",
iv,
additionalData: aad
}, keyObj, msg);
return ciphertext;
}
export async function AES_GCM_Decrypt(key: BufferSource, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
const keyObj = await subtle.importKey("raw", key, {
name: "AES-GCM",
}, false, ["decrypt"]);
const plaintext = await subtle.decrypt({
name: "AES-GCM",
iv: iv,
additionalData: aad
}, keyObj, ciphertext);
//아마도 tag 검증 실패시 예외가 발생할 것으로 예상
return plaintext;
}
View File
-8
View File
@@ -1,8 +0,0 @@
export function hexToRgb(hex: string): { r: number; g: number; b: number; } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
-11
View File
@@ -1,11 +0,0 @@
import { unwrap } from ".//unwrap.js";
import { hexToRgb } from "./hexToRgb.js";
export function isBrightColor(color: string): boolean {
const cv = unwrap(hexToRgb(color));
if ((cv.r * 0.299 + cv.g * 0.587 + cv.b * 0.114) > 140) {
return true;
} else {
return false;
}
}
-3
View File
@@ -1,3 +0,0 @@
export function isNotNull<T>(value: T|null|undefined):value is T{
return value !== null && value !== undefined;
}
-133
View File
@@ -1,133 +0,0 @@
import { every, isString } from "lodash-es";
type JsonifiableLite =
string | number | boolean | bigint | null | undefined |
Date | ArrayBuffer | SharedArrayBuffer | ArrayBufferView |
JsonifiableLite[] | { [key: string]: JsonifiableLite };
export type Jsonifiable =
JsonifiableLite |
Jsonifiable[] |
Map<JsonifiableLite, Jsonifiable> |
Set<Jsonifiable> |
{ jsonify(): Jsonifiable } |
{ [key: string]: Jsonifiable };
export type Jsonified<T extends Jsonifiable> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T extends bigint ? string :
T extends null ? null :
T extends undefined ? undefined :
T extends Date ? string :
T extends ArrayBuffer ? string :
T extends SharedArrayBuffer ? string :
T extends ArrayBufferView ? string :
T extends (infer K extends Jsonifiable)[] ? Jsonified<K>[] :
T extends Map<infer K extends Jsonifiable, infer V extends Jsonifiable> ? (
K extends string ? { [key in string]: Jsonified<V> } : [Jsonified<K>, Jsonified<V>][]) :
T extends Set<infer V extends Jsonifiable> ? Jsonified<V>[] :
T extends object ? (
T extends { jsonify(): infer V extends Jsonifiable } ? Jsonified<V> : //HACK: jsonify()는 Jsonified여야 함
{[key in keyof T]: Jsonified<T[key]>}):
T extends unknown ? unknown :
never;
/**
* Convert any object to JSON-safe object
*/
export function jsonify<T extends Jsonifiable>(item: T): Jsonified<T> {
if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item as any;
}
if (typeof item === 'bigint') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item.toString() as any;
}
if (item === undefined || item === null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item as any;
}
if (item instanceof Date) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return item.toISOString() as any;
}
if (item instanceof ArrayBuffer || item instanceof SharedArrayBuffer) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Buffer.from(item).toString('base64') as any;
}
if (ArrayBuffer.isView(item)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Buffer.from(item.buffer, item.byteOffset, item.byteLength).toString('base64') as any;
}
if (Array.isArray(item)) {
//HACK: depth hack
const result: Jsonified<string>[] = [];
for (const v of item) {
result.push(jsonify(v as string) as Jsonified<string>);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
if (item instanceof Map) {
//HACK: depth hack
const onlyStringKey = every(item.keys, isString);
if (onlyStringKey) {
const result: { [key: string]: Jsonified<string> } = {};
for (const [k, v] of item.entries()) {
result[k as string] = jsonify(v as string) as Jsonified<string>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
const result: [Jsonified<string>, Jsonified<string>][] = [];
for (const [k, v] of item.entries()) {
result.push([jsonify(k as string), jsonify(v as string)] as [Jsonified<string>, Jsonified<string>]);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
if (item instanceof Set) {
const result: Jsonified<string>[] = [];
for (const v of item.values()) {
result.push(jsonify(v as string) as Jsonified<string>);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
if (typeof item === 'object') {
if (typeof (item as { jsonify(): unknown }).jsonify === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (item as { jsonify(): unknown }).jsonify() as any;
}
const result: { [key: string]: Jsonified<string> } = {};
for (const [k, v] of Object.entries(item)) {
if (typeof k !== 'string') {
continue;
}
if (typeof v === 'function') {
continue;
}
result[k] = jsonify(v as string) as Jsonified<string>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result as any;
}
throw new Error(`jsonify: invalid type ${typeof item}`);
}
-11
View File
@@ -1,11 +0,0 @@
import { webcrypto } from "node:crypto";
const subtle = webcrypto.subtle;
export async function sha256(msg: ArrayBuffer): Promise<ArrayBuffer>{
return await subtle.digest('SHA-256', msg);
}
export async function sha512(msg: ArrayBuffer): Promise<ArrayBuffer>{
return await subtle.digest('SHA-512', msg);
}