Author SHA1 Message Date
Hide_D 2e23be1585 확장자 제거 2024-03-08 11:35:19 +00:00
Hide_D ca0bf296a6 simple test 2024-03-08 11:24:35 +09:00
Hide_D c713b858c1 delay 0 2024-03-08 11:08:05 +09:00
Hide_D 7885c8c8d7 test 2024-03-07 16:58:03 +00:00
Hide_D a8ddaac1ed worker_test init 2024-03-07 15:06:49 +00:00
Hide_D 21da4d14e6 wip: 개체 작업 진행 2024-03-06 16:33:23 +00:00
Hide_D be6e765d76 denque 2024-03-05 15:45:41 +00:00
Hide_D 23070ee19a dep: update 2024-03-05 14:50:55 +00:00
Hide_D 2fb1364282 php버전과의 비교 추가 2024-03-01 12:48:30 +00:00
Hide_D 9120943a1a 컴포넌트 문서 추가 2024-03-01 12:16:15 +00:00
Hide_D bd6158c0bc 실수 ㅜㅜ 2023-09-24 06:11:28 +00:00
Hide_D 3687d597c4 타입 정의 상세 2023-09-24 06:11:01 +00:00
Hide_D 45f14f0b72 che_필살 예시 구현 2023-09-24 05:43:06 +00:00
Hide_D 905b401aab ResourceController 란 것이 있을 것 2023-09-24 04:06:59 +00:00
Hide_D 4bc5407bf2 defs 준비 2023-09-23 18:41:45 +00:00
Hide_D e084835249 game_logic 정의 2023-09-23 18:34:27 +00:00
Hide_D cea888573c crypto 누락분 2023-09-23 17:53:46 +00:00
Hide_D 9495cff295 callClientAPI warning 제거 2023-09-23 17:34:56 +00:00
Hide_D c3d3a05f6e package.json author 2023-09-23 17:26:02 +00:00
Hide_D a0c730299c dep 수정 2023-09-23 17:24:30 +00:00
Hide_D 778f189a21 fix import 2023-09-23 17:12:20 +00:00
Hide_D 5ec1173f15 dep 정리 2023-09-23 17:05:18 +00:00
Hide_D 409e6513e3 fix: 누락 package 2023-09-23 16:34:23 +00:00
Hide_D e59f9a9659 monorepo 버전 준비
## @strpc
기존 RPC를 package화

### @strpc/express
express의 middleware + router 결함

## @sammo
게임 전체
- server, client
- gateway_server, gateway_client
2023-09-23 16:29:18 +00:00
233 changed files with 17024 additions and 3 deletions
+17
View File
@@ -0,0 +1,17 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
parser: "vue-eslint-parser",
'extends': [
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting',
],
parserOptions: {
ecmaVersion: 'latest',
}
}
+30
View File
@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist_server
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
+1
View File
@@ -0,0 +1 @@
20
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@sammo/api_def",
"version": "1.0.1",
"description": "",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
"./gateway": "./dist/gateway.js",
"./def": "./dist/def/index.js"
},
"scripts": {
"build": "tsc --build"
},
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/crypto": "workspace:^",
"@sammo/secure_token": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/def": "workspace:^"
},
"devDependencies": {
"zod": "^3.22.4"
},
"peerDependencies": {
"zod": "^3.22.2"
}
}
+1
View File
@@ -0,0 +1 @@
export const hello = "hello";
+62
View File
@@ -0,0 +1,62 @@
import { GET, POST, type DefAPINamespace } from "@strpc/def";
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
InvalidResponse, ValidResponse
} from "@strpc/def";
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ArgDeleteAPI, ArgGetAPI, ArgHeadAPI, ArgPatchAPI, ArgPostAPI, ArgPutAPI,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
EmptyDeleteAPI, EmptyGetAPI, EmptyHeadAPI, EmptyPatchAPI, EmptyPostAPI, EmptyPutAPI,
} from "@strpc/def/types";
export type LoginResponse = {
result: true,
nextToken: [number, string] | undefined,
}
export type LoginFailed = {
result: false,
reqOTP: boolean,
reason: string,
}
export type AutoLoginNonceResponse = {
result: true,
loginNonce: string,
};
export type AutoLoginResponse = {
result: true,
nextToken: [number, string] | undefined,
}
export type AutoLoginFailed = {
result: false,
silent: boolean,
reason: string,
}
export const structure = {
Login: {
LoginByID: POST<{
id: string,
password: string,
}, LoginResponse, LoginFailed>(),
LoginByToken: POST<{
hashedToken: string,
token_id: number,
}, AutoLoginResponse, AutoLoginFailed>(),
ReqNonce: GET<AutoLoginNonceResponse, AutoLoginFailed>(),
},
GetGameLoginToken: GET<{
result: true,
gameLoginToken: string,
userID: number,
}>(),
} satisfies DefAPINamespace;
+27
View File
@@ -0,0 +1,27 @@
import type {
DefAPINamespace,
} from "@strpc/def";
import { GET, POST } from '@strpc/def';
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
InvalidResponse, ValidResponse
} from "@strpc/def";
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ArgDeleteAPI, ArgGetAPI, ArgHeadAPI, ArgPatchAPI, ArgPostAPI, ArgPutAPI,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
EmptyDeleteAPI, EmptyGetAPI, EmptyHeadAPI, EmptyPatchAPI, EmptyPostAPI, EmptyPutAPI,
} from "@strpc/def/types";
export type {
InferResponse,
InferError,
InferQuery,
} from "@strpc/def";
export type * as def from './def/index.js';
export const structure = {
} satisfies DefAPINamespace;
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../util"
},
{
"path": "../secure_token"
},
{
"path": "../../@strpc/def"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@sammo/client",
"version": "1.0.1",
"description": "",
"main": "dist/index.js",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"type-check": "vue-tsc",
"build-only": "vite build",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@sammo/game_logic": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/client_ky": "workspace:^",
"@strpc/def": "workspace:^",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
},
"peerDependencies": {
"zod": "^3.22.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+85
View File
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>
+86
View File
@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+35
View File
@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
@@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
@@ -0,0 +1,86 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>
@@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>
@@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>
+14
View File
@@ -0,0 +1,14 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router/index.js'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
+23
View File
@@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
})
export default router
+12
View File
@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+23
View File
@@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.vite.json",
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "../util"
},
{
"path": "../secure_token"
},
{
"path": "../../@strpc/def"
},
{
"path": "./tsconfig.node.json"
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+17
View File
@@ -0,0 +1,17 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@util': fileURLToPath(new URL('./server/util', import.meta.url)),
}
}
})
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
root: false,
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
env: {
node: true,
},
rules: {
'@typescript-eslint/no-unused-vars': 'off'
},
}
+38
View File
@@ -0,0 +1,38 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
const jestConfig = {
preset: 'ts-jest/presets/default-esm',
extensionsToTreatAsEsm: ['.ts'],
testEnvironment: 'node',
rootDir: '.',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: 'tsconfig.jest.json',
},
],
},
moduleFileExtensions: [
'js',
'mjs',
'ts',
'mts',
'tsx',
'jsx',
],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
"^lodash-es$": "lodash",
"^lodash-es/(.*)$": "<rootDir>/node_modules/lodash/$1",
},
modulePathIgnorePatterns: [
'<rootDir>/dist/',
],
moduleDirectories: [
"node_modules",
".*/@sammo/util"
],
};
export default jestConfig
+48
View File
@@ -0,0 +1,48 @@
{
"name": "@sammo/crypto",
"version": "1.0.1",
"description": "",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
"./AES": "./dist/AES.js",
"./ECDSA": "./dist/ECDSA.js",
"./ECDHe": "./dist/ECDHe.js",
"./ECDHe_AES": "./dist/ECDHe_AES.js",
"./ECKey": "./dist/ECKey.js",
"./PBKDF2": "./dist/PBKDF2.js",
"./SHA2": "./dist/SHA2.js",
"./RawTypes": "./dist/RawTypes.js"
},
"scripts": {
"build": "tsc --build",
"test": "jest"
},
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/util": "workspace:^"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"bson": "^6.4.0",
"buffer": "^6.0.3",
"eslint": "^8.57.0",
"jest": "^29.7.0",
"lodash-es": "^4.17.21",
"ts-jest": "^29.1.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3"
},
"peerDependencies": {
"bson": "^5.4.0",
"buffer": "^6.0.3",
"lodash-es": "^4.17.21"
}
}
+92
View File
@@ -0,0 +1,92 @@
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./AES.js";
type TestType = {
key: Buffer,
IV: Buffer,
PT: Buffer,
AAD?: Buffer,
CT: Buffer,
failed?: boolean,
};
type RawTestType = {
key: string,
IV: string,
PT: string,
AAD?: string,
CT: string,
failed?: boolean,
}
function convertItem(obj: RawTestType): TestType {
return {
key: Buffer.from(obj.key, 'hex'),
IV: Buffer.from(obj.IV, 'hex'),
PT: Buffer.from(obj.PT, 'hex'),
AAD: obj.AAD ? Buffer.from(obj.AAD, 'hex') : undefined,
CT: Buffer.from(obj.CT, 'hex'),
failed: obj.failed,
}
}
const tests: TestType[] = [
{
key: '92e11dcdaa866f5ce790fd24501f92509aacf4cb8b1339d50c9c1240935dd08b',
IV : 'ac93a1a6145299bde902f21a',
PT : '2d71bcfa914e4ac045b2aa60955fad24',
AAD: '1e0889016f67601c8ebea4943bc23ad6',
CT : '8995ae2e6df3dbf96fac7b7137bae67feca5aa77d51d4a0a14d9c51e1da474ab',
failed: false,
},
{
key: 'b52c505a37d78eda5dd34f20c22540ea1b58963cf8e5bf8ffa85f9f2492505b4',
IV : '516c33929df5a3284ff463d7',
PT : '',
AAD: '',
CT : 'bdc1ac884d332457a1d2664f168c76f0',
failed: false,
},
{
key: '886cff5f3e6b8d0e1ad0a38fcdb26de97e8acbe79f6bed66959a598fa5047d65',
IV : '3a8efa1cd74bbab5448f9945',
PT : '',
AAD: '519fee519d25c7a304d6c6aa1897ee1eb8c59655',
CT : 'f6d47505ec96c98a42dc3ae719877b87',
failed: false,
},
{
key: '460fc864972261c2560e1eb88761ff1c992b982497bd2ac36c04071cbb8e5d99',
IV : '8a4a16b9e210eb68bcb6f58d',
PT : '99e4e926ffe927f691893fb79a96b067',
AAD: '',
CT : '133fc15751621b5f325c7ff71ce08324ec4e87e0cf74a13618d0b68636ba9fa7',
failed: false,
},
].map(convertItem)
test.each(tests)('encrypt(%#)', async (item) => {
for (const item of tests) {
if(item.failed){
expect(() => AES_GCM_Encrypt(item.key, item.IV, item.PT, item.AAD)).rejects.toThrow('Invalid');
return;
}
const ct = await AES_GCM_Encrypt(item.key, item.IV, item.PT, item.AAD);
expect(ct).toEqual(item.CT.buffer);
}
})
test.each(tests)('decrypt(%#)', async (item) => {
for (const item of tests) {
if(item.failed){
expect(() => AES_GCM_Decrypt(item.key, item.IV, item.CT, item.AAD)).rejects.toThrow('Invalid');
return;
}
const pt = await AES_GCM_Decrypt(item.key, item.IV, item.CT, item.AAD);
expect(pt).toEqual(item.PT.buffer);
}
})
+61
View File
@@ -0,0 +1,61 @@
import type { BufferSource } from "@sammo/util";
const subtle = globalThis.crypto.subtle;
export async function AES_GCM_Encrypt(key: BufferSource | CryptoKey, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
const keyObj = key instanceof CryptoKey ? key : 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 | CryptoKey, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
const keyObj = key instanceof CryptoKey ? key : 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;
}
export async function AES_CBC_Encrypt(key: BufferSource | CryptoKey, iv: BufferSource, msg: BufferSource): Promise<ArrayBuffer> {
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
name: "AES-CBC",
}, false, ["encrypt"]);
const ciphertext = await subtle.encrypt({
name: "AES-CBC",
iv,
}, keyObj, msg);
return ciphertext;
}
export async function AES_CBC_Decrypt(key: BufferSource | CryptoKey, iv: BufferSource, ciphertext: BufferSource): Promise<ArrayBuffer> {
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
name: "AES-CBC",
}, false, ["decrypt"]);
const plaintext = await subtle.decrypt({
name: "AES-CBC",
iv: iv,
}, keyObj, ciphertext);
return plaintext;
}
+63
View File
@@ -0,0 +1,63 @@
import { wrapBuffer } from "@sammo/util";
import { ECDSA_sign, ECDSA_verify } from "./ECDSA.js";
import { verifyKeyFromSignKey } from "./ECKey.js";
import type { ECDHe_P384_PublicKey, ECDHe_P384_KeyPair, ECDSA_PKCS8_P384_SignKey, ECDHe_P384_PrivateKey, ECDHe_P384_PublicKeyInfo, ECDHe_P384_LiteKeyPair } from "./RawTypes.js";
const subtle = globalThis.crypto.subtle;
export const curveName: EcKeyGenParams | EcKeyImportParams = {
name: 'ECDH',
namedCurve: 'P-384'
}
export async function genECDHeKey(signKey: ECDSA_PKCS8_P384_SignKey): Promise<ECDHe_P384_KeyPair> {
const keyPairP = subtle.generateKey(curveName, true, ['deriveKey', 'deriveBits']);
const verifyKeyP = verifyKeyFromSignKey(signKey);
const rawKeyPair = await keyPairP;
const publicKey = wrapBuffer<ECDHe_P384_PublicKey>(await subtle.exportKey('spki', rawKeyPair.publicKey));
const privateKey = wrapBuffer<ECDHe_P384_PrivateKey>(await subtle.exportKey('pkcs8', rawKeyPair.privateKey));
const signatureP = ECDSA_sign(signKey, privateKey);
return {
publicInfo: {
publicKey,
verifyKey: await verifyKeyP,
sign: await signatureP
},
privateKey,
}
}
export async function genECDHeLiteKey(): Promise<ECDHe_P384_LiteKeyPair> {
const keyPair = await subtle.generateKey(curveName, true, ['deriveKey', 'deriveBits']);
const publicKey = wrapBuffer<ECDHe_P384_PublicKey>(await subtle.exportKey('spki', keyPair.publicKey));
const privateKey = wrapBuffer<ECDHe_P384_PrivateKey>(await subtle.exportKey('pkcs8', keyPair.privateKey));
return {
publicKey,
privateKey
};
}
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, keySizeBit?: number): Promise<ArrayBuffer>;
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, keySizeBit?: number): Promise<ArrayBuffer>;
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, keySizeBit = 256): Promise<ArrayBuffer> {
const rawPublicKey = other instanceof Uint8Array ? other : other.publicKey;
const rawPrivateKey = me.privateKey;
const publicKeyP = subtle.importKey('spki', rawPublicKey, curveName, true, ['deriveBits']);
const privateKeyP = subtle.importKey('pkcs8', rawPrivateKey, curveName, true, ['deriveBits']);
if (!(other instanceof Uint8Array) && !(await ECDSA_verify(other.verifyKey, other.publicKey, other.sign))) {
throw new Error("Invalid signature");
}
return await subtle.deriveBits({
name: 'ECDH',
public: await publicKeyP
}, await privateKeyP, keySizeBit);
}
+48
View File
@@ -0,0 +1,48 @@
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./AES.js";
import { deriveKey } from "./ECDHe.js";
import type { ECDHe_P384_KeyPair, ECDHe_P384_LiteKeyPair, ECDHe_P384_PublicKey, ECDHe_P384_PublicKeyInfo } from "./RawTypes.js";
export type EncryptedItem = {
ct: Uint8Array;
}
const keySizeBit = 256;
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>{
let key: ArrayBuffer;
if('publicInfo' in me){
key = await deriveKey(other, me, keySizeBit);
}
else if('publicKey' in other){
key = await deriveKey(other, me, keySizeBit);
}
else{
throw new Error("Invalid argument: may be trying to do ECDH, not ECDHe.");
}
return AES_GCM_Encrypt(key, iv, msg, aad);
}
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>{
let key: ArrayBuffer;
if('publicInfo' in me){
key = await deriveKey(other, me, keySizeBit);
}
else if('publicKey' in other){
key = await deriveKey(other, me, keySizeBit);
}
else{
throw new Error("Invalid argument: may be trying to do ECDH, not ECDHe.");
}
return AES_GCM_Decrypt(key, iv, ciphertext, aad);
}
+32
View File
@@ -0,0 +1,32 @@
import { ECDSA_sign_bson, ECDSA_verify_bson } from "./ECDSA.js";
import { genKey } from "./ECKey.js";
import { randomBytes } from "./utils.js";
test('basic sign', async () => {
const [signKey, verifyKey] = await genKey();
const misVerifyKey = Buffer.alloc(verifyKey.length);
verifyKey.copy(misVerifyKey);
misVerifyKey[misVerifyKey.length - 2] ^= 1; //1bit
const msg = randomBytes(100);
const misMsg = Buffer.alloc(msg.length);
msg.copy(misMsg);
misMsg[1] ^= 1;
const signature = await ECDSA_sign_bson(signKey, msg);
expect(signature.length).toEqual(96);
const misSignature = Buffer.alloc(signature.length);
signature.copy(misSignature);
misSignature[misSignature.length - 2] ^= 3;
const t0P = ECDSA_verify_bson(verifyKey, msg, signature);
const f1P = ECDSA_verify_bson(misVerifyKey, msg, signature);
const f2P = ECDSA_verify_bson(verifyKey, misMsg, signature);
const f3P = ECDSA_verify_bson(verifyKey, msg, misSignature);
expect(await t0P).toEqual(true);
expect(await f1P).toEqual(false);
expect(await f2P).toEqual(false);
expect(await f3P).toEqual(false);
})
+81
View File
@@ -0,0 +1,81 @@
import { BSON } from "bson";
import { type Bsonifiable, bsonify, type Jsonifiable, jsonify } from "@sammo/util";
import { importSignKey, importVerifyKey } from "./ECKey.js";
import type { ECDSASignature, ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey, ECDSASignatureBSON, ECDSASignatureJSON, ECDSASignatureRaw } from "./RawTypes.js";
import { isBufferSource } from './types.js';
import { isArray, isDate, isObject } from "lodash-es";
// 간편하게 저장하기 위해서 그냥! 매번 복잡한 일을 하도록 하자
const subtle = globalThis.crypto.subtle;
const hashName: EcdsaParams = {
name: 'ECDSA',
hash: 'SHA-512'
}
export async function ECDSA_sign(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw> {
const subtleSignKeyP = importSignKey(sign_key);
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, msg));
}
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureBSON>
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw>
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable | BufferSource): Promise<ECDSASignatureRaw | ECDSASignatureBSON> {
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg as Record<string, string>));
return await ECDSA_sign(sign_key, msg) as ECDSASignatureBSON;
}
return ECDSA_sign(sign_key, msg);
}
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureJSON>
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource): Promise<ECDSASignatureRaw>
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Jsonifiable | BufferSource): Promise<ECDSASignatureRaw | ECDSASignatureJSON> {
if (!isBufferSource(msg)) {
msg = Buffer.from(JSON.stringify(jsonify(msg as Record<string, string>)), 'utf-8');
return await ECDSA_sign(sign_key, msg) as ECDSASignatureJSON;
}
return ECDSA_sign(sign_key, msg);
}
export async function ECDSA_verify(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignature): Promise<boolean> {
try {
const subtleVerifyKeyP = importVerifyKey(verify_key);
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, msg);
}
catch (e) {
return false;
}
}
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureBSON): Promise<boolean>;
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignatureRaw): Promise<boolean>;
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable | BufferSource, sign: ECDSASignatureBSON | ECDSASignatureRaw): Promise<boolean> {
try {
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg as Record<string, string>));
}
return ECDSA_verify(verify_key, msg, sign);
}
catch (e) {
return false;
}
}
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureJSON): Promise<boolean>;
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource, sign: ECDSASignatureRaw): Promise<boolean>;
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Jsonifiable | BufferSource, sign: ECDSASignatureJSON | ECDSASignatureRaw): Promise<boolean> {
try {
if (!isBufferSource(msg)) {
const x = JSON.stringify(jsonify(msg as Record<string, string>));
msg = Buffer.from(x, 'utf-8');
}
return ECDSA_verify(verify_key, msg, sign);
}
catch (e) {
return false;
}
}
+35
View File
@@ -0,0 +1,35 @@
import { genKey, verifyKeyFromSignKey, pemFromVerifyKey, importVerifyKey, importSignKey, pemFromSignKey } from "./ECKey.js";
import { TypeLength } from "./TypeLength.js";
import { decodePEM } from "./utils.js";
test('gen_key', async () => {
const [signKey, verifyKey] = await genKey();
expect(signKey.length).toEqual(TypeLength.ECDSA_PKCS8_P384_SignKey);
expect(verifyKey.length).toEqual(TypeLength.ECDSA_SPKI_P384_VerifyKey);
const pemVerifyKey = await pemFromVerifyKey(verifyKey);
const verifyKey2 = decodePEM(pemVerifyKey, 'PUBLIC KEY')[0];
expect(verifyKey).toEqual(verifyKey2);
});
test('convert', async () => {
const [signKey, verifyKey] = await genKey();
const newVerifyKey = await verifyKeyFromSignKey(signKey);
expect(verifyKey).toEqual(newVerifyKey);
})
test('import sign_key', async() => {
const [signKey, verifyKey] = await genKey();
const pemSignKey = pemFromSignKey(signKey);
const signKey2 = decodePEM(pemSignKey, 'EC PRIVATE KEY')[0];
const signKeyObj = await importSignKey(signKey, true);
const signKeyObj2 = await importSignKey(signKey2, true);
expect(signKeyObj).toEqual(signKeyObj2);
})
+62
View File
@@ -0,0 +1,62 @@
import type { ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey } from "./RawTypes.js";
import { encodePEM } from "./utils.js";
// 간편하게 저장하기 위해서 그냥! 매번 복잡한 일을 하도록 하자
const subtle = globalThis.crypto.subtle;
export const curveName: EcKeyGenParams | EcKeyImportParams = {
name: 'ECDSA',
namedCurve: 'P-384'
}
export async function importSignKey(signKey: ECDSA_PKCS8_P384_SignKey, allowExport?: boolean): Promise<CryptoKey> {
return await subtle.importKey('pkcs8', signKey, curveName, allowExport ?? false, ['sign']);
}
export async function importVerifyKeyRaw(verifyKey: ECDSA_P384_VerifyKey, allowExport?: boolean): Promise<CryptoKey> {
return await subtle.importKey('raw', verifyKey, curveName, allowExport ?? false, ['verify'])
}
export async function exportVerifyKeyRaw(verifyKey: ECDSA_P384_VerifyKey): Promise<Buffer>{
const key = await importVerifyKeyRaw(verifyKey);
return Buffer.from(await subtle.exportKey('raw', key));
}
export async function importVerifyKey(verifyKey: ECDSA_P384_VerifyKey, allowExport?: boolean): Promise<CryptoKey> {
return await subtle.importKey('spki', verifyKey, curveName, allowExport ?? false, ['verify'])
}
export async function genKey(): Promise<[ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey]> {
const keyPair = await subtle.generateKey(curveName, true, ['sign', 'verify']);
const signKey = subtle.exportKey('pkcs8', keyPair.privateKey);
const verifyKey = subtle.exportKey('spki', keyPair.publicKey);
return [Buffer.from(await signKey), Buffer.from(await verifyKey)];
}
export async function pemFromVerifyKey(verifyKey: ECDSA_P384_VerifyKey): Promise<string> {
const subtleVerifyKey = await importVerifyKey(verifyKey, true);
const spkiRaw = await subtle.exportKey('spki', subtleVerifyKey)
return encodePEM(Buffer.from(spkiRaw), 'PUBLIC KEY');
}
export function pemFromSignKey(signKey: ECDSA_PKCS8_P384_SignKey): string {
return encodePEM(signKey, 'EC PRIVATE KEY');
}
export async function verifyKeyFromSignKey(signKey: ECDSA_PKCS8_P384_SignKey): Promise<ECDSA_P384_VerifyKey> {
const subtleSignKey = await importSignKey(signKey, true);
const jwk = await subtle.exportKey('jwk', subtleSignKey);
delete jwk.d;
delete jwk.dp;
delete jwk.dq;
delete jwk.q;
delete jwk.qi;
jwk.key_ops = ["verify"];
const subtleVerifyKey = await subtle.importKey('jwk', jwk, curveName, true, ['verify']);
const verifyKey = await subtle.exportKey('spki', subtleVerifyKey);
return Buffer.from(verifyKey);
}
+262
View File
@@ -0,0 +1,262 @@
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
const maxIntMore1 = 0x20_0000_0000_0000n; //NOTE: b 0, 10000110100, 00...00
const maxIntMore1f = Number(maxIntMore1);
export const bufferByteSize = 512 / 8; //SHA512
const intBitMapMask = new Map([
[0x1n, 1],
[0x3n, 2],
[0x7n, 3],
[0xfn, 4],
[0x1fn, 5],
[0x3fn, 6],
[0x7fn, 7],
[0xffn, 8],
[0x1ffn, 9],
[0x3ffn, 10],
[0x7ffn, 11],
[0xfffn, 12],
[0x1fffn, 13],
[0x3fffn, 14],
[0x7fffn, 15],
[0xffffn, 16],
[0x1ffffn, 17],
[0x3ffffn, 18],
[0x7ffffn, 19],
[0xfffffn, 20],
[0x1fffffn, 21],
[0x3fffffn, 22],
[0x7fffffn, 23],
[0xffffffn, 24],
[0x1ffffffn, 25],
[0x3ffffffn, 26],
[0x7ffffffn, 27],
[0xfffffffn, 28],
[0x1fffffffn, 29],
[0x3fffffffn, 30],
[0x7fffffffn, 31],
[0xffffffffn, 32],
[0x1ffffffffn, 33],
[0x3ffffffffn, 34],
[0x7ffffffffn, 35],
[0xfffffffffn, 36],
[0x1fffffffffn, 37],
[0x3fffffffffn, 38],
[0x7fffffffffn, 39],
[0xffffffffffn, 40],
[0x1ffffffffffn, 41],
[0x3ffffffffffn, 42],
[0x7ffffffffffn, 43],
[0xfffffffffffn, 44],
[0x1fffffffffffn, 45],
[0x3fffffffffffn, 46],
[0x7fffffffffffn, 47],
[0xffffffffffffn, 48],
[0x1ffffffffffffn, 49],
[0x3ffffffffffffn, 50],
[0x7ffffffffffffn, 51],
[0xfffffffffffffn, 52],
[0x1fffffffffffffn, 53],
]);
function calcBitMask(n: bigint): bigint {
n |= n >> 1n;
n |= n >> 2n;
n |= n >> 4n;
n |= n >> 8n;
n |= n >> 16n;
n |= n >> 32n;
return n;
}
export class LiteHashDRBG implements RNG {
protected buffer!: ArrayBuffer;
protected bufferIdx!: number;
protected hq: DataView;
protected hqIdxPos: number;
protected ready: Promise<void>;
public constructor(protected seed: BytesLike, protected stateIdx = 0, bufferIdx = 0) {
if (bufferIdx < 0) {
throw new Error(`bufferIdx ${bufferIdx} < 0`);
}
if (bufferIdx >= bufferByteSize) {
throw new Error(`bufferidx ${bufferIdx} >= ${bufferByteSize}`);
}
if (stateIdx < 0) {
throw new Error(`stateIdx ${stateIdx} < 0`);
}
const seedU8 = convertBytesLikeToUint8Array(seed);
const hqBuffer = new ArrayBuffer(seedU8.byteLength + 4);
const hqU8 = new Uint8Array(hqBuffer);
hqU8.set(seedU8, 0);
this.hq = new DataView(hqBuffer);
this.hqIdxPos = seedU8.byteLength;
this.ready = this.genNextBlock();
this.bufferIdx = bufferIdx;
}
protected async genNextBlock(): Promise<void> {
this.bufferIdx = 0;
this.hq.setUint32(this.hqIdxPos, this.stateIdx, true);
this.stateIdx += 1;
const digest = await sha512(this.hq.buffer);
this.buffer = digest;
}
public getMaxInt(): number {
return maxInt;
}
public async nextBytes(bytes: number, baseBytes?: number): Promise<Uint8Array> {
bytes |= 0;
if (bytes <= 0) {
throw new Error(`${bytes} <= 0`);
}
const ticket = this.ready;
let waiter: Promise<Uint8Array | undefined> = Promise.resolve(undefined);
let nextBlockWait: (() => void) | null = (() => { throw 'something wrong'; });
this.ready = new Promise((resolve, reject) => {
waiter = (async () => {
await ticket;
nextBlockWait = resolve;
if (this.bufferIdx + bytes <= bufferByteSize) {
if (baseBytes === undefined || bytes >= baseBytes) {
const result = this.buffer.slice(this.bufferIdx, this.bufferIdx + bytes);
this.bufferIdx += bytes;
if (this.bufferIdx === bufferByteSize) {
nextBlockWait = null;
this.genNextBlock().then(resolve, reject);
}
return new Uint8Array(result);
}
const resultBuffer = new ArrayBuffer(Math.max(bytes, baseBytes ?? 0));
const result = new Uint8Array(resultBuffer);
result.set(new Uint8Array(this.buffer, this.bufferIdx, bytes));
this.bufferIdx += bytes;
if (this.bufferIdx === bufferByteSize) {
nextBlockWait = null;
this.genNextBlock().then(resolve, reject);
}
return result;
}
const resultBuffer = new ArrayBuffer(baseBytes ? Math.max(bytes, baseBytes) : bytes);
const result = new Uint8Array(resultBuffer);
result.set(new Uint8Array(this.buffer, this.bufferIdx));
let offset = bufferByteSize - this.bufferIdx;
let remain = bytes - offset;
while (remain > bufferByteSize) {
await this.genNextBlock();
result.set(new Uint8Array(this.buffer), offset);
offset += bufferByteSize;
remain -= bufferByteSize;
}
if (remain === 0) {
nextBlockWait = null;
this.genNextBlock().then(resolve, reject);
return result;
}
await this.genNextBlock();
result.set(new Uint8Array(this.buffer, 0, remain), offset);
this.bufferIdx = remain;
return result;
})();
});
//이 코드를 통해 Promise 내부가 실행된다
await delay(0);
const nextBlock = await waiter;
if (nextBlockWait) {
nextBlockWait();
}
return nextBlock as Uint8Array;
}
public async nextBits(bits: number, baseBytes?: number): Promise<Uint8Array> {
await this.ready;
bits |= 0;
const bytes = (bits + 7) >> 3;
const headBits = bits & 0x7;
const result = await this.nextBytes(bytes, baseBytes);
if (headBits === 0) {
return result;
}
result[bytes - 1] &= 0xff >> (8 - headBits);
return result;
}
protected async _nextInt(bits: number): Promise<bigint> {
const buffer = await this.nextBits(bits, 8);
const dataView = new DataView(buffer.buffer);
return dataView.getBigUint64(0, true);
}
public async nextInt(max?: number): Promise<number> {
if (max === undefined || max === maxInt) {
return Number(await this._nextInt(maxRngSupportBit));
}
if (max > maxInt) {
throw new Error('Over max int');
}
if (max === 0) {
return 0;
}
if (max < 0) {
return -this.nextInt(-max);
}
const mask = calcBitMask(BigInt(max));
const bits = intBitMapMask.get(mask) as number;
let n = Number(this._nextInt(bits));
while (n > max) {
n = Number(this._nextInt(bits));
}
return n;
}
public async nextFloat1(): Promise<number> {
// eslint-disable-next-line no-constant-condition
while (true) {
const nInt = await this._nextInt(maxRngSupportBit + 1);
if (nInt < maxIntMore1) {
return Number(nInt) / maxIntMore1f;
}
if (nInt === maxIntMore1) {
return 1;
}
}
}
public static build(seed: BytesLike, stateIdx = 0): LiteHashDRBG {
return new LiteHashDRBG(seed, stateIdx);
}
}
+48
View File
@@ -0,0 +1,48 @@
const subtle = globalThis.crypto.subtle;
export type ValidEncAlg = 'AES256-GCM' | 'AES128-GCM' | 'AES256-CBC' | 'AES128-CBC';
const encAlgMap: Record<ValidEncAlg, AesDerivedKeyParams> = {
'AES256-GCM': {
name: 'AES-GCM',
length: 256,
},
'AES128-GCM' : {
name: 'AES-GCM',
length: 128,
},
'AES256-CBC' : {
name: 'AES-CBC',
length: 256,
},
'AES128-CBC' : {
name: 'AES-CBC',
length: 128,
},
}
export async function PBKDF2_SHA512(password: string, salt: BufferSource, alg: ValidEncAlg = 'AES256-GCM', iterations = 600000): Promise<CryptoKey> {
const param: Pbkdf2Params = {
name: 'PBKDF2',
hash: 'SHA-512',
salt: salt,
iterations,
};
const encoder = new TextEncoder();
const baseKey = await subtle.importKey(
"raw",
encoder.encode(password),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
);
return await subtle.deriveKey(
param,
baseKey,
encAlgMap[alg],
true,
["encrypt", "decrypt"]
);
}
+13
View File
@@ -0,0 +1,13 @@
export interface RNG {
/**
* nextInt()가 반환 가능한 최댓값
*/
getMaxInt(): number;
nextBytes(bytes: number): Promise<Uint8Array>;
nextBits(bits: number): Promise<Uint8Array>;
nextInt(max?: number): Promise<number>;
nextFloat1(): Promise<number>;
}
+144
View File
@@ -0,0 +1,144 @@
import type { RNG } from './RNG.js';
export class RandUtil {
constructor(protected rng: RNG) {
}
public nextFloat1(): Promise<number> {
return this.rng.nextFloat1();
}
public async nextRange(min: number, max: number): Promise<number> {
const range = max - min;
return await this.nextFloat1() * (range) + min;
}
public async nextRangeInt(min: number, max: number): Promise<number> {
const range = max - min;
return await this.rng.nextInt(range) + min;
}
public nextInt(max?: number): Promise<number> {
return this.rng.nextInt(max);
}
public async nextBit(): Promise<boolean> {
const view = new DataView(await this.rng.nextBits(1) as ArrayBufferLike);
return view.getUint8(0) != 0;
}
public async nextBool(prob = 0.5): Promise<boolean> {
if (prob >= 1) {
return true;
}
if (prob === 0.5){
return this.nextBit();
}
if (prob <= 0){
return false;
}
return await this.nextFloat1() < prob;
}
public async shuffle<T>(srcArray: T[]): Promise<T[]> {
const cnt = srcArray.length;
if(cnt === 0){
return [];
}
if (cnt > this.rng.getMaxInt()) {
throw 'Invalid random int range';
}
const result: T[] = Array.from(srcArray);
for (let srcIdx = 0; srcIdx < cnt; srcIdx += 1) {
const destIdx = await this.rng.nextInt(cnt - srcIdx - 1) + srcIdx;
if(srcIdx === destIdx){
continue;
}
[result[srcIdx], result[destIdx]] = [result[destIdx], result[srcIdx]];
}
return result;
}
//Object는 integer key에 예외가 있어 shuffleAssoc은 없음
public async choice<T>(items: T[] | Record<string | number, T> | Set<T>): Promise<T> {
if (items instanceof Array) {
if(items.length === 0){
throw new Error('Empty items');
}
const idx = await this.rng.nextInt(items.length - 1);
return items[idx];
}
if (items instanceof Set) {
return this.choice(Array.from(items.values()));
}
return items[await this.choice(Array.from(Object.keys(items)))];
}
public async choiceUsingWeight(items: Record<string | number, number>): Promise<string | number> {
if(Object.keys(items).length === 0){
throw new Error('Empty items');
}
let sum = 0;
for (const value of Object.values(items)) {
if (value <= 0) {
continue;
}
sum += value;
}
let rd = await this.nextFloat1() * sum;
for (const [item, value] of Object.entries(items)) {
if (value <= 0) {
if (rd <= 0) {
return item;
}
continue;
}
if (rd <= value) {
return item;
}
rd -= value;
}
throw new Error('Unreacheable');
}
public async choiceUsingWeightPair<T>(items: [T, number][]): Promise<T> {
if(items.length === 0){
throw new Error('Empty items');
}
let sum = 0;
for (const [, value] of items) {
if (value <= 0) {
continue;
}
sum += value;
}
let rd = await this.nextFloat1() * sum;
for (const [item, value] of items) {
if (value <= 0) {
if (rd <= 0) {
return item;
}
continue;
}
if (rd <= value) {
return item;
}
rd -= value;
}
throw new Error('Unreacheable');
}
}
+50
View File
@@ -0,0 +1,50 @@
import type { WrappedBuffer } from "@sammo/util";
export interface ECDSA_PKCS8_P384_SignKey extends WrappedBuffer{
_w_type?: "ECDSA_PKCS8_P384_SignKey";
}
export interface ECDSA_P384_VerifyKey extends WrappedBuffer{
_w_type?: "ECDSA_P384_VerifyKey";
}
export interface ECDSASignatureRaw extends WrappedBuffer{
_w_type?: "ECDSASignature";
}
export interface ECDSASignatureBSON extends WrappedBuffer{
_w_type?: "ECDSASignatureBSON";
}
export interface ECDSASignatureJSON extends WrappedBuffer{
_w_type?: "ECDSASignatureJSON";
}
export type ECDSASignature = ECDSASignatureRaw | ECDSASignatureBSON | ECDSASignatureJSON;
export interface ECDHe_P384_PublicKey extends WrappedBuffer{
_w_type?: "ECDHe_P384_PublicKey";
}
export interface ECDHe_P384_PrivateKey extends WrappedBuffer{
_w_type?: "ECDHe_P384_PrivateKey";
}
export interface ECDHe_P384_LiteKeyPair {
publicKey: ECDHe_P384_PublicKey;
privateKey: ECDHe_P384_PrivateKey;
}
export interface ECDHe_P384_PublicKeyInfo {
publicKey: ECDHe_P384_PublicKey;
verifyKey: ECDSA_P384_VerifyKey;
sign: ECDSASignatureRaw; //sign of publicKey
}
export interface ECDHe_P384_KeyPair{
publicInfo: ECDHe_P384_PublicKeyInfo;
privateKey: ECDHe_P384_PrivateKey;
}
export {
TypeLength
} from "./TypeLength.js";
+10
View File
@@ -0,0 +1,10 @@
import { sha256, sha512 } from "./SHA2.js";
test('sha2', async () => {
const msg = Buffer.from('hello');
const answer256 = Buffer.from('2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824', 'hex');
const answer512 = Buffer.from('9B71D224BD62F3785D96D46AD3EA3D73319BFBC2890CAADAE2DFF72519673CA72323C3D99BA5C11D7C7ACC6E14B8C5DA0C4663475C2E5C3ADEF46F73BCDEC043', 'hex');
expect(await sha256(msg)).toEqual(answer256.buffer);
expect(await sha512(msg)).toEqual(answer512.buffer);
});
+19
View File
@@ -0,0 +1,19 @@
import { isBufferSource } from "./types.js";
import { BSON } from "bson";
import { type Bsonifiable, bsonify } from "@sammo/util";
import { isArray, isDate, isObject } from "lodash-es";
const subtle = globalThis.crypto.subtle;
export async function sha256(msg: Bsonifiable | BufferSource): Promise<ArrayBuffer> {
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg));
}
return await subtle.digest('SHA-256', msg);
}
export async function sha512(msg: Bsonifiable | BufferSource): Promise<ArrayBuffer> {
if (!isBufferSource(msg)) {
msg = BSON.serialize(bsonify(msg));
}
return await subtle.digest('SHA-512', msg);
}
+6
View File
@@ -0,0 +1,6 @@
export enum TypeLength {
//ECDSA P384 서명
ECDSA_PKCS8_P384_SignKey = 185,
ECDSA_SPKI_P384_VerifyKey = 120,
ECDSASignature = 96,
}
+113
View File
@@ -0,0 +1,113 @@
import { InvalidArgument } from '@sammo/util';
import type { RandUtil } from './RandUtil.js';
export class InvalidVType extends InvalidArgument {
public override name = 'InvalidVType';
}
export class InvalidArgumentBufferSize extends InvalidArgument {
public override name = 'InvalidArgumentBufferSize';
}
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;
}
}
}
export class NotNullExpected extends RuntimeError {
public override name = 'NotNullExpected';
}
export class PrivilegedGenViolation extends RuntimeError {
public override name = 'PrivilegedGenViolation';
}
export type MergeableBuffer = MergeableBuffer[] | Buffer;
export function calcMergeableBuffer(item: MergeableBuffer): number {
if (item instanceof Buffer) {
return item.byteLength;
}
let bufferSize = 0;
for (const subItem of item) {
bufferSize += calcMergeableBuffer(subItem);
}
return bufferSize;
}
export function mergeBuffer(...buffers: MergeableBuffer[]): Buffer {
if (buffers.length == 0) {
return Buffer.alloc(0);
}
if (buffers.length == 1) {
const item = buffers[0];
if (item instanceof Buffer) {
return item;
}
}
const fillBuffer = function (item: MergeableBuffer, bufferIdx: number): number {
if (item instanceof Buffer) {
result.set(item, bufferIdx);
bufferIdx += item.byteLength;
return bufferIdx;
}
for (const subItem of item) {
bufferIdx = fillBuffer(subItem, bufferIdx);
}
return bufferIdx;
}
const bufferSize = calcMergeableBuffer(buffers);
const result = Buffer.alloc(bufferSize);
fillBuffer(buffers, 0);
return result;
}
type ErrType<T> = { new(msg?: string): T }
type Nullable<T> = T | null | undefined
export function unwrap<T>(result: Nullable<T>): T {
if (result === null || result === undefined) {
throw new NotNullExpected();
}
return result;
}
export function unwrap_err<T, ErrT extends Error>(result: Nullable<T>, errType: ErrType<ErrT>, errMsg?: string): T {
if (result === null || result === undefined) {
throw new errType(errMsg);
}
return result;
}
export * as AES from './AES.js';
export * as ECDSA from './ECDSA.js';
export * as ECKey from './ECKey.js';
export * as PBKDF2 from './PBKDF2.js';
export * as SHA2 from './SHA2.js';
export * as ECDHe from './ECDHe.js';
export * as ECDHe_AES from './ECDHe_AES.js';
export { RandUtil } from './RandUtil.js';
export { LiteHashDRBG } from './LiteHashDRBG.js';
export type { RNG } from './RNG.js';
export * as RawTypes from './RawTypes.js';
export * from './utils.js';
export * from './types.js';
export { TypeLength } from './TypeLength.js';
export * from "./LiteHashDRBG.js"
+13
View File
@@ -0,0 +1,13 @@
export function isBufferSource(obj: unknown): obj is BufferSource {
if (obj instanceof ArrayBuffer){
return true;
}
if('SharedArrayBuffer' in globalThis && obj instanceof SharedArrayBuffer){
return true;
}
if (ArrayBuffer.isView(obj)){
return true;
}
return false;
}
+73
View File
@@ -0,0 +1,73 @@
import type { WrappedBuffer } from "@sammo/util";
const crypto = globalThis.crypto;
export function randomBytes(length: number): Buffer {
const buffer = Buffer.alloc(length);
crypto.getRandomValues(buffer);
return buffer;
}
//TODO: 필요할때마다 확장
export type ValidPEMType = 'PUBLIC KEY' | 'EC PRIVATE KEY' | 'CERTIFICATE';
/**
* PEM string에 내부 타입으로 WrappedBuffer를 보관한 형태
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export type PEMString<T extends Buffer, S extends ValidPEMType> = string & {
/** 타입구분자. 항상 undefined일 것이다 */
_pem_b_type?: T;
_pem_type?: S;
}
export function encodePEM<T extends WrappedBuffer, S extends ValidPEMType>(data: T, pemType: S): PEMString<T, S>;
export function encodePEM(data: Buffer, pemType: ValidPEMType): string;
export function encodePEM(data: Buffer, pemType: ValidPEMType): string {
const base64text = data.toString('base64');
const splitText = base64text.match(/.{1,64}/g)?.join('\n') ?? '';
return `-----BEGIN ${pemType}-----
${splitText}
-----END ${pemType}-----
`;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type InferPEMType<T extends PEMString<any, any>> = Exclude<undefined, T['_pem_type']>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type InferPEMBuffer<T extends PEMString<any, any>> = Exclude<undefined, T['_pem_b_type']>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function decodePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>[];
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer[];
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer[] {
const tag = pemType ?? "[A-Z0-9 ]+";
const pattern = new RegExp(`-{5}BEGIN ${tag}-{5}([a-zA-Z0-9=+\\/\\n\\r]+)-{5}END ${tag}-{5}`, "g");
const res: Buffer[] = [];
let matches: RegExpExecArray | null = null;
// eslint-disable-next-line no-cond-assign
while (matches = pattern.exec(pem)) {
const base64 = matches[1]
.replace(/\r/g, "")
.replace(/\n/g, "");
res.push(Buffer.from(base64, 'base64'));
}
return res;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function decodeSinglePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>;
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer;
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer {
const res = decodePEM(pem, pemType);
if (res.length != 1) {
throw new Error("invalid pem");
}
return res[0];
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"verbatimModuleSyntax": false,
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"references": [
{
"path": "../util"
}
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@sammo/game_logic",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build"
},
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/api_def": "workspace:^",
"@sammo/crypto": "workspace:^",
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"dotenv": "^16.4.5",
"mongoose": "^8.2.1"
},
"devDependencies": {
"@types/node": "^20.11.24"
}
}
+8
View File
@@ -0,0 +1,8 @@
import type { CityID } from "./defs.js";
export class City{
constructor(
public readonly id: CityID
){
}
}
View File
+3
View File
@@ -0,0 +1,3 @@
export class GameEnv{
}
+149
View File
@@ -0,0 +1,149 @@
import { InvalidArgument } from "@sammo/util";
import { City } from "./City.js";
import type { IGeneralEntity } from "./GeneralEntity.js";
import type { IResourceController } from "./ResourceController.js";
import { DiplomaticPermission, type CityID, type GeneralID, type GeneralName, type NationID, OfficerLevel, type TroopID } from "./defs.js";
import { must } from "@sammo/util";
import { Nation } from "./Nation.js";
import { Troop } from "./Troop.js";
export class General {
private _raw: IGeneralEntity;
private _city: WeakRef<City>;
private _troop: WeakRef<Troop> | undefined;
private _nation: WeakRef<Nation>;
public get raw(): Readonly<IGeneralEntity> {
return this._raw;
}
constructor(
raw: IGeneralEntity,
private resourceController: IResourceController
){
this._raw = raw;
this._city = new WeakRef(must(resourceController.getCity(raw.cityID)));
this._nation = new WeakRef(must(resourceController.getNation(raw.nationID)));
if(raw.troopID){
this._troop = new WeakRef(must(resourceController.getTroop(raw.troopID, raw.nationID)));
}
}
public get id(): GeneralID {
return this._raw.id;
}
public get name(): GeneralName {
return this._raw.name;
}
public update(callback: (oldRaw: IGeneralEntity) => void, force = false) {
const oldCityID = this._raw.cityID;
const oldTroopID = this._raw.troopID;
const oldNationID = this._raw.nationID;
callback(this._raw);
if(!force){
if(oldCityID !== this._raw.cityID){
throw new Error("City change by update() is not allowed");
}
if(oldTroopID !== this._raw.troopID){
throw new Error("Troop change by update() is not allowed");
}
if(oldNationID !== this._raw.nationID){
throw new Error("Nation change by update() is not allowed");
}
}
this.resourceController.setDirtyGeneral(this._raw);
}
public changeCity(city: CityID | City){
const cityObj: City = (()=>{
if(city instanceof City){
return city;
}
const cityObj = this.resourceController.getCity(city);
if(!cityObj){
throw new InvalidArgument(`City not found: ${city}`);
}
return cityObj;
})();
this._city = new WeakRef(cityObj);
//TODO: ResourceController에게 알려야 하는가?
this.update((raw) => {
raw.cityID = cityObj.id;
}, true);
}
public quitTroop(){
if(!this._troop){
return;
}
const troop = must(this._troop.deref());
if(troop.leader.id === this._raw.id){
troop.destructTroop(false);
}
else{
troop.members.delete(this._raw.id);
}
this._troop = undefined;
this.update((raw) => {
raw.troopID = undefined;
}, true);
}
public changeTroop(troop: TroopID | Troop){
this.quitTroop();
const troopObj: Troop = (()=>{
if(troop instanceof Troop){
return troop;
}
const troopObj = this.resourceController.getTroop(troop, this._raw.nationID);
if(!troopObj){
throw new InvalidArgument(`Troop not found: ${troop}`);
}
return troopObj;
})();
troopObj.members.set(this._raw.id, new WeakRef(this));
this._troop = new WeakRef(troopObj);
this.update((raw) => {
raw.troopID = troopObj.id;
}, true);
}
public changeNation(nation: NationID | Nation){
const nationObj: Nation = (()=>{
if(nation instanceof Nation){
return nation;
}
const nationObj = this.resourceController.getNation(nation);
if(!nationObj){
throw new InvalidArgument(`Nation not found: ${nation}`);
}
return nationObj;
})();
this._nation = new WeakRef(nationObj);
this.quitTroop();
this.update((raw) => {
raw.nationID = nationObj.id;
raw.diplomaticPermission = DiplomaticPermission.none;
if(nationObj.id == 0){
raw.officerLevel = OfficerLevel.none;
}
else{
raw.officerLevel = OfficerLevel.normal;
}
raw.nationBelong = 0;
}, true);
}
}
+65
View File
@@ -0,0 +1,65 @@
import type { DiplomaticPermission, GeneralID, GeneralName, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, OwnerID, OwnerName, PersonalityType, SpecialityDomesticType, SpecialityWarType, TroopID } from "./defs.js";
export interface IGeneralEntity {
id: GeneralID;
owner?: OwnerID;
npcType: NpcType;
affinity?: number;
picture?: string;
name: GeneralName;
ownerName?: OwnerName;
nationID: NationID;
cityID: number;
troopID?: TroopID;
leadership: number;
leadershipExp: number;
strength: number;
strengthExp: number;
intel: number;
intelExp: number;
injury: number;
experience: number;
dedication: number;
experienceLevel: number;
dedicationLevel: number;
dex: number[];
officerLevel: OfficerLevel;
diplomaticPermission: DiplomaticPermission;
gold: number;
rice: number;
item: Record<ItemKeyType, {
id: ItemID,
option: Record<string, unknown>,
}>;
turntime: Date;
killturn: number;
age: number;
startAge: number;
nationBelong: number;
betray: number;
personality: PersonalityType;
specialityDomestic: SpecialityDomesticType;
specialityWar: SpecialityWarType;
defenceTrain: number;
npcSpec?: {
msg?: string;
bornyear: number;
deadyear: number;
}
}
@@ -0,0 +1,19 @@
import type { IAction } from "../IAction.js";
import type { SpecialReqType, WeightType } from "./defs.js";
export abstract class BaseSpecial implements IAction {
public abstract readonly name: string;
static readonly selectWeight: WeightType = {
type: "normal",
weight: 1,
}
static readonly type: SpecialReqType[] = [];
getName(): string {
return this.name;
}
abstract getInfo(): string[]|string;
}
@@ -0,0 +1,48 @@
import type { General } from "../../General.js";
import type { BattlePhaseTrigger, CalcStat, CalcStatRange } from "../../IActionMethod.js";
import { WarUnitTriggerCaller } from "../../TriggerCaller/WarUnitTriggerCaller.js";
import type { WarUnit } from "../../WarUnit.js";
import { che_필살강화_회피불가 } from "../../WarUnitTrigger/che_필살강화_회피불가.js";
import type { CalcStatName, CalcStatRangeName } from "@/defs.js";
import { BaseSpecial } from "../BaseSpecial.js";
import type { SpecialReqType, WeightNormal } from "../defs.js";
export class che_필살 extends BaseSpecial implements CalcStat, CalcStatRange, BattlePhaseTrigger {
public readonly name = "필살";
static override readonly selectWeight: WeightNormal = {
type: "normal",
weight: 1,
}
static override type: SpecialReqType[] = [
'statLeadership',
'statStrength',
'statIntel',
];
getInfo() {
return "[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상";
}
onCalcStat(general: General, statName: CalcStatName, value: number, aux?: unknown[] | undefined): number {
if (statName == 'warCriticalRatio') {
return value + 0.3;
}
return value;
}
onCalcStatRange(general: General, statName: CalcStatRangeName, value: [number, number], aux?: unknown[] | undefined): [number, number] {
if (statName === 'criticalDamageRange'){
const [rangeMin, rangeMax] = value;
return [(rangeMin + rangeMax) / 2, rangeMax];
}
return value;
}
getBattlePhaseSkillTriggerList(unit: WarUnit): WarUnitTriggerCaller | undefined {
return new WarUnitTriggerCaller(
new che_필살강화_회피불가(unit),
);
}
}
@@ -0,0 +1,6 @@
class SpecialityHelper {
}
export const specialityHelper = new SpecialityHelper();
@@ -0,0 +1,30 @@
export type SpecialReqType =
"statLeadership" |
"statStrength" |
"statIntel" |
"armyFootMan" |
"armyArcher" |
"armyCavalry" |
"armyWizard" |
"armySiege" |
"reqDexterity" |
"statNotLeadership" |
"statNotStrength" |
"statNotIntel"
;
export type WeightNormal = {
type: "normal";
weight: number; // 1을 기준으로 한 가중치 곱
};
export type WeightPercent = {
type: "percent";
weight: number; // 1이면 100%
};
export type WeightType = WeightNormal | WeightPercent;
@@ -0,0 +1,8 @@
export { BaseSpecial } from "./BaseSpecial.js";
import { che_필살 } from "./SpecialWar/che_필살.js";
export const SpecialWar = {
che_필살
}
+23
View File
@@ -0,0 +1,23 @@
import type * as IMethod from "./IActionMethod.js";
export type * as IMethod from "./IActionMethod.js";
export interface IActionMethod extends
IMethod.TurnExecuteTriggerList,
IMethod.CalcDomestic,
IMethod.CalcStat,
IMethod.CalcStatRange,
IMethod.CalcOpposeStat,
IMethod.CalcStrategic,
IMethod.CalcIncome,
IMethod.WarPowerMultiplier,
IMethod.BattleInitTrigger,
IMethod.BattlePhaseTrigger,
IMethod.ArbitraryAction {
}
export interface IAction extends Partial<IActionMethod> {
readonly name: string;
getName(): string;
getInfo(): string[] | string;
}
+63
View File
@@ -0,0 +1,63 @@
import type { RandUtil } from "@sammo/crypto";
import type { WarUnitTriggerCaller } from "./TriggerCaller/WarUnitTriggerCaller.js";
import type { GeneralTriggerCaller } from "./TriggerCaller/GeneralTriggerCaller.js";
import type { General } from "./General.js";
import type { WarUnit } from "./WarUnit.js";
import type { ArbitraryActionType, CalcIncomeType, CalcStatName, CalcStatRangeName, CalcStrategicVarType, StrategicTurnType } from "./defs.js";
//TODO: turnType, varType이 string이면 안됨. enum에 가까워야함!
type onCalcDomesticFunc = (turnType: string, varType: string, value: number, aux?: unknown[]) => number;
type onCalcStatFunc = (general: General, statName: CalcStatName, value: number, aux?: unknown[]) => number;
type onCalcStatRangeFunc = (general: General, statName: CalcStatRangeName, value: [number, number], aux?: unknown[]) => [number, number];
type onCalcStrategicFunc = (turnType: StrategicTurnType, varType: CalcStrategicVarType, value: number) => number;
type onCalcIncomeFunc = (type: CalcIncomeType, value: number) => number;
type getWarPowerMultiplierFunc = (unit: WarUnit) => [number, number];
type getWarUnitTriggerListFunc = (unit: WarUnit) => WarUnitTriggerCaller | undefined;
type onArbitraryActionFunc = (general: General, rng: RandUtil, actionType: ArbitraryActionType, phase?: string, aux?: unknown[]) => null | string[];
export interface TurnExecuteTriggerList {
getPreTurnExecuteTriggerList(general: General): GeneralTriggerCaller | undefined;
}
export interface CalcDomestic {
onCalcDomestic: onCalcDomesticFunc;
}
export interface CalcStat {
onCalcStat: onCalcStatFunc;
}
export interface CalcStatRange {
onCalcStatRange: onCalcStatRangeFunc;
}
export interface CalcOpposeStat {
onCalcOpposeStat: onCalcStatFunc;
}
export interface CalcStrategic {
onCalcStrategic: onCalcStrategicFunc;
}
export interface CalcIncome {
onCalcNationalIncome: onCalcIncomeFunc;
}
export interface WarPowerMultiplier {
getWarPowerMultiplier: getWarPowerMultiplierFunc;
}
export interface BattleInitTrigger {
getBatteInitSkillTriggerList: getWarUnitTriggerListFunc;
}
export interface BattlePhaseTrigger {
getBattlePhaseSkillTriggerList: getWarUnitTriggerListFunc;
}
export interface ArbitraryAction {
onArbitraryAction: onArbitraryActionFunc;
}
+9
View File
@@ -0,0 +1,9 @@
import type { NationID, NationName, NationType } from "./defs.js";
export class Nation {
constructor(
public readonly id: NationID,
public readonly name: NationName,
public readonly type: NationType,
) { }
}
@@ -0,0 +1,64 @@
import type { General } from "./General.js";
import type { City } from "./City.js";
import type { Nation } from "./Nation.js";
import type { CityID, GeneralID, NationID, TroopID } from "./defs.js";
import type { GameEnv } from "./GameEnv.js";
import type { IGeneralEntity } from "./GeneralEntity.js";
import type { Troop } from "./Troop.js";
import type { ITroopEntity } from "./TroopEntity.js";
export interface IResourceController{
addCity(city: City): boolean;
addNation(nation: Nation): boolean;
addGeneral(general: General): boolean;
getCity(id: CityID): City | undefined;
getNation(id: NationID): Nation | undefined;
getGeneral(id: GeneralID): General | undefined;
getGameEnv(): Promise<GameEnv>;
findAllNations(): Map<NationID, Nation>;
findAllCities(): Map<CityID, City>;
findAllGenerals(): Map<GeneralID, General>;
findGeneralByTroop(id: TroopID): Map<GeneralID, General>;
findCityByNation(id: NationID): Map<CityID, City>;
findGeneralByNation(id: NationID): Map<GeneralID, General>;
findGeneralByCity(id: CityID, nationID?: NationID): Map<GeneralID, General>;
getTroop(id: TroopID, nation: NationID): Troop | undefined;
_getAllChanges(): Map<unknown, unknown>;
setDirtyGeneral(rawGeneral: IGeneralEntity): void;
setDirtyCity(city: City): void;
setDirtyTroop(troop: Troop | TroopID): void;
setDirtyNation(nation: Nation): void;
createGeneral(raw: IGeneralEntity): General;
createTroop(raw: ITroopEntity): Troop;
//createNation
reserveDeleteTroop(id: TroopID): void;
reserveDeleteGeneral(id: GeneralID): void;
reserveDeleteNation(id: NationID): void;
resetAndFill(): Promise<void>;
saveAll(): Promise<void>;
}
let _resourceController: IResourceController | undefined = undefined;
export const setResourceController = (resourceController: IResourceController) => {
_resourceController = resourceController;
}
export const getResourceController = () => {
if (_resourceController === undefined) {
throw new Error("ResourceController not set");
}
return _resourceController;
}
@@ -0,0 +1,11 @@
import type { General } from "../General.js";
import { ObjectTrigger } from "./ObjectTrigger.js";
export abstract class BaseGeneralTrigger extends ObjectTrigger<General> {
constructor(
protected object: General,
protected priority: number
) {
super();
}
}
@@ -0,0 +1,43 @@
import type { RandUtil } from "@sammo/crypto";
import type { WarUnit, WarUnitEnv } from "../WarUnit.js";
import { ObjectTrigger, PRIORITY } from "./ObjectTrigger.js";
import type { GameEnv } from "../GameEnv.js";
import { NotYetImplemented } from "@sammo/util";
export const RaiseType = {
None: 0,
Item: 1,
ConsumableItem: 3,
DedupTypeBase: 1024,
} as const;
export abstract class BaseWarUnitTrigger extends ObjectTrigger<WarUnit> {
protected override priority: number = PRIORITY.BODY;
constructor(
protected object: WarUnit,
protected raiseType: number = RaiseType.None,
) {
super();
}
override getUniqueID(): string {
const priority = this.getPriority();
const name = this.constructor.name;
const unitName = this.object?.constructor.name ?? "undefined";
return `${priority}:${name}:${unitName}`;
}
action(rng: RandUtil, env: GameEnv, arg: unknown[]): void {
throw new NotYetImplemented();
console.log(rng, env, arg);
}
abstract actionWar(self: WarUnit, oppose: WarUnit, selfEnv: WarUnitEnv, opposeEnv: WarUnitEnv, rng: RandUtil): boolean;
processConsumableItem(): boolean {
throw new NotYetImplemented();
}
}
@@ -0,0 +1,37 @@
import type { RandUtil } from "@sammo/crypto";
import type { GameEnv } from "../GameEnv.js";
export const PRIORITY = {
MIN: 0,
BEGIN: 10000,
PRE: 20000,
BODY: 30000,
POST: 40000,
FINAL: 50000,
} as const;
export abstract class ObjectTrigger<T extends object> {
protected abstract priority: number;
protected abstract object?: T;
getPriority(): number {
return this.priority;
}
setPriority(priority: number): this {
this.priority = priority;
return this;
}
//FIXME: arg array. generic이어야하나?
abstract action(rng: RandUtil, env: GameEnv, arg: unknown[]): void;
//FIXME: 재설계 필요.
getUniqueID(): string{
const priority = this.getPriority();
//FIXME: PHP 버전에서 namespace 개념도 옮겨와야하는가?
const objName = this.constructor.name;
return `${priority}:${objName}`;
}
}
@@ -0,0 +1,9 @@
import { BaseGeneralTrigger } from "../Trigger/BaseGeneralTrigger.js";
import type { ObjectTrigger } from "../Trigger/ObjectTrigger.js";
import { TriggerCaller } from "./TriggerCaller.js";
export class GeneralTriggerCaller extends TriggerCaller<BaseGeneralTrigger> {
checkValidTrigger(trigger: ObjectTrigger<object>): boolean {
return trigger instanceof BaseGeneralTrigger;
}
}
@@ -0,0 +1,54 @@
import { ObjectTrigger, PRIORITY } from "../Trigger/ObjectTrigger.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export abstract class TriggerCaller<T extends ObjectTrigger<any>> {
protected triggerListByPriority: Map<number, Map<string, T>>;
abstract checkValidTrigger(trigger: T): boolean;
isEmpty(): boolean {
return this.triggerListByPriority.size === 0;
}
constructor(...triggerList: T[]) {
if(triggerList.length === 0){
this.triggerListByPriority = new Map();
return;
}
this.triggerListByPriority = new Map();
let sorted = true;
let maxPriority: number = PRIORITY.MIN;
for (const trigger of triggerList) {
const priority = trigger.getPriority();
const uniqueID = trigger.getUniqueID();
if(sorted){
if(maxPriority < priority){
maxPriority = priority;
}
else if(maxPriority > priority){
sorted = false;
}
}
if(this.triggerListByPriority.has(priority)){
const subTriggerList = this.triggerListByPriority.get(priority) as Map<string, T>;
if(subTriggerList.has(uniqueID)){
//TODO: 완성되기 전까진..
throw `Duplicated trigger: ${uniqueID}`;
}
subTriggerList.set(uniqueID, trigger);
}
else {
this.triggerListByPriority.set(priority, new Map([[uniqueID, trigger]]));
}
}
if(!sorted){
this.triggerListByPriority = new Map([...this.triggerListByPriority.entries()].sort((a, b) => b[0] - a[0]));
}
}
}
@@ -0,0 +1,9 @@
import { BaseWarUnitTrigger } from "../Trigger/BaseWarUnitTrigger.js";
import type { ObjectTrigger } from "../Trigger/ObjectTrigger.js";
import { TriggerCaller } from "./TriggerCaller.js";
export class WarUnitTriggerCaller extends TriggerCaller<BaseWarUnitTrigger> {
checkValidTrigger(trigger: ObjectTrigger<object>): boolean {
return trigger instanceof BaseWarUnitTrigger;
}
}
+53
View File
@@ -0,0 +1,53 @@
import { must } from "@sammo/util";
import type { General } from "./General.js";
import type { IResourceController } from "./ResourceController.js";
import type { ITroopEntity } from "./TroopEntity.js";
import type { GeneralID } from "./defs.js";
export class Troop {
private _raw: ITroopEntity
private _leader: WeakRef<General>;
private _members: Map<GeneralID, WeakRef<General>>;
public get raw(): Readonly<ITroopEntity> {
return this._raw;
}
constructor(
raw: ITroopEntity,
private resourceController: IResourceController
){
this._raw = raw;
const members = resourceController.findGeneralByTroop(raw.id);
const leaderID: GeneralID = raw.id as GeneralID;
this._leader = new WeakRef(must(members.get(leaderID)));
members.delete(leaderID);
this._members = new Map([...members].map(([k,v])=>[k, new WeakRef(v)]));
}
public get id(){
return this._raw.id;
}
public get leader(){
return must(this._leader.deref());
}
public get members(): Map<GeneralID, WeakRef<General>> {
return this._members;
}
public destructTroop(withLeader = true){
this._members.forEach((member)=>{
const general = must(member.deref());
general.quitTroop();
});
if(withLeader){
this.leader.quitTroop();
}
this.resourceController.reserveDeleteTroop(this._raw.id);
}
}
+7
View File
@@ -0,0 +1,7 @@
import type { NationID, TroopID } from "./defs.js";
export interface ITroopEntity {
id: TroopID;
nationID: NationID;
name: string;
}
+15
View File
@@ -0,0 +1,15 @@
import type { WarSkillName } from "./defs.js";
export class WarUnitEnv {
}
export abstract class WarUnit{
hasActivatedSkill(skillName: WarSkillName): boolean {
throw new Error("Method not implemented.");
}
activateSkill(skillName: WarSkillName): void {
throw new Error("Method not implemented.");
}
}
+5
View File
@@ -0,0 +1,5 @@
import { WarUnit } from "./WarUnit.js";
export class WarUnitCity extends WarUnit{
}
+5
View File
@@ -0,0 +1,5 @@
import { WarUnit } from "./WarUnit.js";
export class WarUnitGeneral extends WarUnit{
}
@@ -0,0 +1,17 @@
import type { RandUtil } from "@sammo/crypto";
import { BaseWarUnitTrigger } from "../Trigger/BaseWarUnitTrigger.js";
import type { WarUnit, WarUnitEnv } from "../WarUnit.js";
import { PRIORITY } from "../Trigger/ObjectTrigger.js";
export class che_필살강화_회피불가 extends BaseWarUnitTrigger {
protected override priority = PRIORITY.PRE + 150;
actionWar(self: WarUnit, oppose: WarUnit, selfEnv: WarUnitEnv, opposeEnv: WarUnitEnv, rng: RandUtil): boolean {
if(!self.hasActivatedSkill("필살")){
return true;
}
oppose.activateSkill("회피불가");
return true;
}
}
+126
View File
@@ -0,0 +1,126 @@
//Type 정의
export type OwnerID = number & { zz_t?: "OwnerID" };
export type OwnerName = string & { zz_t?: "OwnerName" };
export type GeneralID = number & { zz_t?: "GeneralID" };
export type GeneralName = string & { zz_t?: "GeneralName" };
export type CityID = number & { zz_t?: "CityID" };
export type CityName = string & { zz_t?: "CityName" };
export type TroopID = number & { zz_t?: "TroopID" };
export type TroopName = string & { zz_t?: "TroopName" };
export type NationID = number & { zz_t?: "NationID" };
export type NationName = string & { zz_t?: "NationName" };
//TODO: 무언가의 keyof 로 바꿔야함
export type NationType = string & { zz_t?: "NationType" };
export type StrategicTurnType = string & { zz_t?: "StrategicTurnType" };
export type ItemKeyType = 'horse' | 'weapon' | 'book' | 'item';
export type ItemID = string & { zz_t?: "ItemID" };
export type PersonalityType = string & { zz_t?: "PersonalityType" };
export type SpecialityDomesticType = string & { zz_t?: "SpecialityDomesticType" };
export type SpecialityWarType = string & { zz_t?: "SpecialityWarType" };
export const enum NpcType {
user = 0,
user_borrowed_npc = 1,
npc_scenario = 2,
npc_generated = 3,
npc_strategic = 4,
npc_troop_leader = 5,
npc_unselectable = 6,
npc_invader = 9,
}
export const enum OfficerLevel {
//재야
none = 0,
//일반
normal = 1,
//도시관직
cityLeadership = 2,
cityIntel = 3,
cityStrength = 4,
//지력관직
staffIntel3 = 5,
staffIntel2 = 7,
staffIntel1 = 9,
//무력관직
staffStrength3 = 6,
staffStrength2 = 8,
staffStrength1 = 10,
//참모
staffChief = 11,
//군주
lord = 12,
}
export const enum DiplomaticPermission {
none = 0,
audit = 1,
ambassador = 2,
}
/* 전용 stat이어서 확장하고자 하는경우 아래와 같이 확장 가능
```ts
declare module "@/defs.js" {
export interface CalcStatNameBase {
statName: unknown
}
export interface CalcStatRangeNameBase {
statName: unknown
}
}
```
*/
//필요할 때 확장
export interface CalcStatNameBase {
leadership: unknown,
strength: unknown,
intel: unknown,
warCriticalRatio: unknown;
}
export type CalcStatName = keyof CalcStatNameBase;
//필요할 때 확장
export interface CalcStatRangeNameBase {
criticalDamageRange: unknown;
}
export type CalcStatRangeName = keyof CalcStatRangeNameBase;
export interface CalcStrategicVarTypeBase {
globalDelay: unknown;
}
export type CalcStrategicVarType = keyof CalcStrategicVarTypeBase;
export interface CalcIncomeTypeBase {
gold: unknown;
rice: unknown;
}
export type CalcIncomeType = keyof CalcIncomeTypeBase;
export interface ArbitraryActionTypeBase {
}
export type ArbitraryActionType = keyof ArbitraryActionTypeBase;
export interface WarSkillNameBase {
필살: unknown;
회피불가: unknown;
회피: unknown;
}
export type WarSkillName = keyof WarSkillNameBase;
+140
View File
@@ -0,0 +1,140 @@
interface Stats { min: number, max: number, total: number };
export function abilityRand(stats: Stats): [number, number, number] {
let leadership = Math.random() * 65 + 10;
let strength = Math.random() * 65 + 10;
let intel = Math.random() * 65 + 10;
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
leadership += 1;
}
if (
leadership > stats.max ||
strength > stats.max ||
intel > stats.max ||
leadership < stats.min ||
strength < stats.min ||
intel < stats.min
) {
return abilityRand(stats);
}
return [leadership, strength, intel];
}
export function abilityLeadpow(stats: Stats): [number, number, number] {
let leadership = Math.random() * 6;
let strength = Math.random() * 6;
let intel = Math.random() * 1;
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
strength += 1;
}
if (intel < stats.min) {
leadership -= stats.min - intel;
intel = stats.min;
}
if (leadership > stats.max) {
strength += leadership - stats.max;
leadership = stats.max;
}
if (strength > stats.max) {
leadership += strength - stats.max;
strength = stats.max;
}
if (leadership > stats.max) {
intel += leadership - stats.max;
leadership = stats.max;
}
return [leadership, strength, intel];
}
export function abilityLeadint(stats: Stats): [number, number, number] {
let leadership = Math.random() * 6;
let strength = Math.random() * 1;
let intel = Math.random() * 6;
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
intel += 1;
}
if (strength < stats.min) {
leadership -= stats.min - strength;
strength = stats.min;
}
if (leadership > stats.max) {
intel += leadership - stats.max;
leadership = stats.max;
}
if (intel > stats.max) {
leadership += intel - stats.max;
intel = stats.max;
}
if (leadership > stats.max) {
strength += leadership - stats.max;
leadership = stats.max;
}
return [leadership, strength, intel];
}
export function abilityPowint(stats: Stats): [number, number, number] {
let leadership = Math.random() * 1;
let strength = Math.random() * 6;
let intel = Math.random() * 6;
const rate = leadership + strength + intel;
leadership = Math.floor((leadership / rate) * stats.total);
strength = Math.floor((strength / rate) * stats.total);
intel = Math.floor((intel / rate) * stats.total);
while (leadership + strength + intel < stats.total) {
intel += 1;
}
if (leadership < stats.min) {
strength -= stats.min - leadership;
leadership = stats.min;
}
if (strength > stats.max) {
intel += strength - stats.max;
strength = stats.max;
}
if (intel > stats.max) {
strength += intel - stats.max;
intel = stats.max;
}
if (strength > stats.max) {
leadership += strength - stats.max;
strength = stats.max;
}
return [leadership, strength, intel];
}
+5
View File
@@ -0,0 +1,5 @@
export * from "./generalStats.js";
export * as defs from "./defs.js";
// NOTE: game_logic은 client, server 모두에서 사용되는 라이브러리이다.
// DB에 접근한다면 DI여야하나?
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "../../@strpc/express"
},
{
"path": "../util"
},
{
"path": "../crypto"
},
{
"path": "../server_util"
},
]
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+52
View File
@@ -0,0 +1,52 @@
{
"name": "@sammo/gateway_client",
"version": "1.0.1",
"description": "",
"main": "dist/index.js",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"type-check": "vue-tsc",
"build-only": "vite build",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@sammo/util": "workspace:^",
"@strpc/client_ky": "workspace:^",
"@strpc/def": "workspace:^",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
},
"peerDependencies": {
"zod": "^3.22.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+85
View File
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>
+86
View File
@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

+35
View File
@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
@@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
@@ -0,0 +1,86 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>
@@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

Some files were not shown because too many files have changed in this diff Show More