Initial commit
This commit is contained in:
commit
ad0e31bf55
|
@ -0,0 +1,8 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
js
|
||||
package.json
|
||||
*.config.*
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"plugins": [
|
||||
"cadence"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:cadence/recommended"
|
||||
],
|
||||
"root": true,
|
||||
"rules": {},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"rules": {
|
||||
// broken
|
||||
"lines-around-comment": "off",
|
||||
|
||||
// TENPORARILY OFF AS THEY'RE BROKEN.
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-property-computation": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"**/*.test.ts"
|
||||
],
|
||||
"rules": {
|
||||
// Our test files break these rules *all* the time, and there's
|
||||
// no point refactoring. Our tests deliberately play with the
|
||||
// dynamic nature of TS to more accurately test arbitrary input.
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
// we abuse this one for faking data, but we really shouldn't.
|
||||
"@typescript-eslint/consistent-type-assertions": "warn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
|
||||
*.sqlite3
|
||||
*.sqlite3-shm
|
||||
*.sqlite3-wal
|
||||
|
||||
config.json5
|
||||
|
||||
js
|
||||
.tsbuildinfo
|
|
@ -0,0 +1,14 @@
|
|||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
/**
|
||||
* This is a type-safe variant of "req.safeBody".
|
||||
* "req.safeBody" is 'any' by default, which makes it exceptionally difficult
|
||||
* to use in our codebase (due to the strict cadence rules.)
|
||||
*/
|
||||
safeBody: Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
|
@ -0,0 +1,6 @@
|
|||
import type { Config } from "drizzle-kit";
|
||||
|
||||
export default {
|
||||
schema: "./src/external/db/schemas/",
|
||||
out: "./src/external/db/drizzle",
|
||||
} satisfies Config;
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "kozukata-toa",
|
||||
"version": "0.1.0",
|
||||
"description": "An ALL.Net network service emulator.",
|
||||
"main": "./js/main.js",
|
||||
"files": [
|
||||
"/js"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"typecheck": "tsc --project tsconfig.build.json --noEmit",
|
||||
"start": "pnpm build && pnpm start-no-build",
|
||||
"start-no-build": "node js/main.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "beerpsi",
|
||||
"license": "0BSD",
|
||||
"engines": {
|
||||
"node": "16",
|
||||
"pnpm": "7"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^9.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-orm": "^0.29.0",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"fletcher": "^0.0.3",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"json5": "^2.2.3",
|
||||
"luxon": "^3.4.4",
|
||||
"micro-packed": "^0.3.2",
|
||||
"raw-body": "^2.5.2",
|
||||
"safe-json-stringify": "^1.2.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typed-struct": "^2.3.0",
|
||||
"winston": "^3.11.0",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.7",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/iconv": "^3.0.4",
|
||||
"@types/luxon": "^3.3.4",
|
||||
"@types/node": "16",
|
||||
"@types/safe-json-stringify": "^1.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||
"@typescript-eslint/parser": "5.47.1",
|
||||
"drizzle-kit": "^0.20.4",
|
||||
"eslint": "8.18.0",
|
||||
"eslint-plugin-cadence": "^0.1.0",
|
||||
"typescript": "4.9.4"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
|||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import { Config } from "lib/setup/config";
|
||||
|
||||
const sqlite = new Database(Config.DATABASE_PATH);
|
||||
|
||||
export const db = drizzle(sqlite);
|
||||
|
||||
migrate(db, { migrationsFolder: "src/external/db/drizzle" });
|
|
@ -0,0 +1,48 @@
|
|||
CREATE TABLE `aimedb_felica_card_lookup` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`idm` text(16) NOT NULL,
|
||||
`access_code` text(20) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `aimedb_felica_mobile_lookup` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`idm` text(16) NOT NULL,
|
||||
`access_code` text(20) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `allnet_arcade` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text(60) DEFAULT 'Random arcade at nowhere',
|
||||
`nickname` text(40) DEFAULT 'Please send help',
|
||||
`country` text(3) DEFAULT 'JPN',
|
||||
`region_id` integer DEFAULT 1,
|
||||
`region_name0` text(48) DEFAULT 'W',
|
||||
`region_name1` text(48) DEFAULT '',
|
||||
`region_name2` text(48) DEFAULT '',
|
||||
`region_name3` text(48) DEFAULT '',
|
||||
`utc_offset` real DEFAULT 9
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `allnet_arcade_ip` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`arcade_id` integer,
|
||||
`ip` text(15),
|
||||
FOREIGN KEY (`arcade_id`) REFERENCES `allnet_arcade`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `allnet_machine` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`arcade_id` integer,
|
||||
`serial` text(11),
|
||||
`game` text(5),
|
||||
`can_venue_hop` integer,
|
||||
`last_authenticated` integer,
|
||||
FOREIGN KEY (`arcade_id`) REFERENCES `allnet_arcade`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `aimedb_felica_card_lookup_idm_unique` ON `aimedb_felica_card_lookup` (`idm`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `aimedb_felica_card_lookup_access_code_unique` ON `aimedb_felica_card_lookup` (`access_code`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `aimedb_felica_mobile_lookup_idm_unique` ON `aimedb_felica_mobile_lookup` (`idm`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `aimedb_felica_mobile_lookup_access_code_unique` ON `aimedb_felica_mobile_lookup` (`access_code`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `allnet_arcade_ip_ip_unique` ON `allnet_arcade_ip` (`ip`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `allnet_machine_serial_unique` ON `allnet_machine` (`serial`);
|
|
@ -0,0 +1,29 @@
|
|||
CREATE TABLE `aimedb_card` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`user_id` integer,
|
||||
`access_code` text(20) NOT NULL,
|
||||
`created_date` integer NOT NULL,
|
||||
`last_login_date` integer NOT NULL,
|
||||
`is_locked` integer DEFAULT false NOT NULL,
|
||||
`is_banned` integer DEFAULT false NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `aimedb_user`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `aimedb_user` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`created_date` integer NOT NULL,
|
||||
`last_login_date` integer NOT NULL,
|
||||
`suspend_expiration_date` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `event_log` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`timestamp` integer NOT NULL,
|
||||
`system` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`severity` text NOT NULL,
|
||||
`message` text,
|
||||
`details` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `aimedb_card_access_code_unique` ON `aimedb_card` (`access_code`);
|
|
@ -0,0 +1,317 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "a691ce52-7207-49ee-8450-d679c38b147a",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"aimedb_felica_card_lookup": {
|
||||
"name": "aimedb_felica_card_lookup",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"idm": {
|
||||
"name": "idm",
|
||||
"type": "text(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_code": {
|
||||
"name": "access_code",
|
||||
"type": "text(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"aimedb_felica_card_lookup_idm_unique": {
|
||||
"name": "aimedb_felica_card_lookup_idm_unique",
|
||||
"columns": [
|
||||
"idm"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"aimedb_felica_card_lookup_access_code_unique": {
|
||||
"name": "aimedb_felica_card_lookup_access_code_unique",
|
||||
"columns": [
|
||||
"access_code"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"aimedb_felica_mobile_lookup": {
|
||||
"name": "aimedb_felica_mobile_lookup",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"idm": {
|
||||
"name": "idm",
|
||||
"type": "text(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_code": {
|
||||
"name": "access_code",
|
||||
"type": "text(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"aimedb_felica_mobile_lookup_idm_unique": {
|
||||
"name": "aimedb_felica_mobile_lookup_idm_unique",
|
||||
"columns": [
|
||||
"idm"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"aimedb_felica_mobile_lookup_access_code_unique": {
|
||||
"name": "aimedb_felica_mobile_lookup_access_code_unique",
|
||||
"columns": [
|
||||
"access_code"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"allnet_arcade": {
|
||||
"name": "allnet_arcade",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text(60)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'Random arcade at nowhere'"
|
||||
},
|
||||
"nickname": {
|
||||
"name": "nickname",
|
||||
"type": "text(40)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'Please send help'"
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "text(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'JPN'"
|
||||
},
|
||||
"region_id": {
|
||||
"name": "region_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"region_name0": {
|
||||
"name": "region_name0",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'W'"
|
||||
},
|
||||
"region_name1": {
|
||||
"name": "region_name1",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"region_name2": {
|
||||
"name": "region_name2",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"region_name3": {
|
||||
"name": "region_name3",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"utc_offset": {
|
||||
"name": "utc_offset",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 9
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"allnet_arcade_ip": {
|
||||
"name": "allnet_arcade_ip",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"arcade_id": {
|
||||
"name": "arcade_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ip": {
|
||||
"name": "ip",
|
||||
"type": "text(15)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"allnet_arcade_ip_ip_unique": {
|
||||
"name": "allnet_arcade_ip_ip_unique",
|
||||
"columns": [
|
||||
"ip"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"allnet_arcade_ip_arcade_id_allnet_arcade_id_fk": {
|
||||
"name": "allnet_arcade_ip_arcade_id_allnet_arcade_id_fk",
|
||||
"tableFrom": "allnet_arcade_ip",
|
||||
"tableTo": "allnet_arcade",
|
||||
"columnsFrom": [
|
||||
"arcade_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"allnet_machine": {
|
||||
"name": "allnet_machine",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"arcade_id": {
|
||||
"name": "arcade_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"serial": {
|
||||
"name": "serial",
|
||||
"type": "text(11)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"game": {
|
||||
"name": "game",
|
||||
"type": "text(5)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"can_venue_hop": {
|
||||
"name": "can_venue_hop",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_authenticated": {
|
||||
"name": "last_authenticated",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"allnet_machine_serial_unique": {
|
||||
"name": "allnet_machine_serial_unique",
|
||||
"columns": [
|
||||
"serial"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"allnet_machine_arcade_id_allnet_arcade_id_fk": {
|
||||
"name": "allnet_machine_arcade_id_allnet_arcade_id_fk",
|
||||
"tableFrom": "allnet_machine",
|
||||
"tableTo": "allnet_arcade",
|
||||
"columnsFrom": [
|
||||
"arcade_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,494 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "ca3c0d34-cebd-4ea7-806e-f4eb141e586b",
|
||||
"prevId": "a691ce52-7207-49ee-8450-d679c38b147a",
|
||||
"tables": {
|
||||
"aimedb_card": {
|
||||
"name": "aimedb_card",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_code": {
|
||||
"name": "access_code",
|
||||
"type": "text(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_date": {
|
||||
"name": "created_date",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_login_date": {
|
||||
"name": "last_login_date",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_locked": {
|
||||
"name": "is_locked",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"is_banned": {
|
||||
"name": "is_banned",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"aimedb_card_access_code_unique": {
|
||||
"name": "aimedb_card_access_code_unique",
|
||||
"columns": [
|
||||
"access_code"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"aimedb_card_user_id_aimedb_user_id_fk": {
|
||||
"name": "aimedb_card_user_id_aimedb_user_id_fk",
|
||||
"tableFrom": "aimedb_card",
|
||||
"tableTo": "aimedb_user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"aimedb_felica_card_lookup": {
|
||||
"name": "aimedb_felica_card_lookup",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"idm": {
|
||||
"name": "idm",
|
||||
"type": "text(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_code": {
|
||||
"name": "access_code",
|
||||
"type": "text(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"aimedb_felica_card_lookup_idm_unique": {
|
||||
"name": "aimedb_felica_card_lookup_idm_unique",
|
||||
"columns": [
|
||||
"idm"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"aimedb_felica_card_lookup_access_code_unique": {
|
||||
"name": "aimedb_felica_card_lookup_access_code_unique",
|
||||
"columns": [
|
||||
"access_code"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"aimedb_felica_mobile_lookup": {
|
||||
"name": "aimedb_felica_mobile_lookup",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"idm": {
|
||||
"name": "idm",
|
||||
"type": "text(16)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_code": {
|
||||
"name": "access_code",
|
||||
"type": "text(20)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"aimedb_felica_mobile_lookup_idm_unique": {
|
||||
"name": "aimedb_felica_mobile_lookup_idm_unique",
|
||||
"columns": [
|
||||
"idm"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"aimedb_felica_mobile_lookup_access_code_unique": {
|
||||
"name": "aimedb_felica_mobile_lookup_access_code_unique",
|
||||
"columns": [
|
||||
"access_code"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"aimedb_user": {
|
||||
"name": "aimedb_user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"created_date": {
|
||||
"name": "created_date",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_login_date": {
|
||||
"name": "last_login_date",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"suspend_expiration_date": {
|
||||
"name": "suspend_expiration_date",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"allnet_arcade": {
|
||||
"name": "allnet_arcade",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text(60)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'Random arcade at nowhere'"
|
||||
},
|
||||
"nickname": {
|
||||
"name": "nickname",
|
||||
"type": "text(40)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'Please send help'"
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "text(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'JPN'"
|
||||
},
|
||||
"region_id": {
|
||||
"name": "region_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"region_name0": {
|
||||
"name": "region_name0",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'W'"
|
||||
},
|
||||
"region_name1": {
|
||||
"name": "region_name1",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"region_name2": {
|
||||
"name": "region_name2",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"region_name3": {
|
||||
"name": "region_name3",
|
||||
"type": "text(48)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"utc_offset": {
|
||||
"name": "utc_offset",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 9
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"allnet_arcade_ip": {
|
||||
"name": "allnet_arcade_ip",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"arcade_id": {
|
||||
"name": "arcade_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ip": {
|
||||
"name": "ip",
|
||||
"type": "text(15)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"allnet_arcade_ip_ip_unique": {
|
||||
"name": "allnet_arcade_ip_ip_unique",
|
||||
"columns": [
|
||||
"ip"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"allnet_arcade_ip_arcade_id_allnet_arcade_id_fk": {
|
||||
"name": "allnet_arcade_ip_arcade_id_allnet_arcade_id_fk",
|
||||
"tableFrom": "allnet_arcade_ip",
|
||||
"tableTo": "allnet_arcade",
|
||||
"columnsFrom": [
|
||||
"arcade_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"allnet_machine": {
|
||||
"name": "allnet_machine",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"arcade_id": {
|
||||
"name": "arcade_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"serial": {
|
||||
"name": "serial",
|
||||
"type": "text(11)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"game": {
|
||||
"name": "game",
|
||||
"type": "text(5)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"can_venue_hop": {
|
||||
"name": "can_venue_hop",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_authenticated": {
|
||||
"name": "last_authenticated",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"allnet_machine_serial_unique": {
|
||||
"name": "allnet_machine_serial_unique",
|
||||
"columns": [
|
||||
"serial"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"allnet_machine_arcade_id_allnet_arcade_id_fk": {
|
||||
"name": "allnet_machine_arcade_id_allnet_arcade_id_fk",
|
||||
"tableFrom": "allnet_machine",
|
||||
"tableTo": "allnet_arcade",
|
||||
"columnsFrom": [
|
||||
"arcade_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"event_log": {
|
||||
"name": "event_log",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"system": {
|
||||
"name": "system",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"severity": {
|
||||
"name": "severity",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"details": {
|
||||
"name": "details",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1700532948929,
|
||||
"tag": "0000_ambitious_gorilla_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1700570200478,
|
||||
"tag": "0001_modern_stryfe",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const felicaCardLookup = sqliteTable(
|
||||
"aimedb_felica_card_lookup",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
idm: text("idm", { length: 16 }).notNull(),
|
||||
accessCode: text("access_code", { length: 20 }).notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
unqIdm: unique().on(t.idm),
|
||||
unqAc: unique().on(t.accessCode),
|
||||
})
|
||||
);
|
||||
|
||||
export type FelicaCardLookup = typeof felicaMobileLookup.$inferSelect;
|
||||
export type NewFelicaCardLookup = typeof felicaMobileLookup.$inferSelect;
|
||||
|
||||
export const felicaMobileLookup = sqliteTable(
|
||||
"aimedb_felica_mobile_lookup",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
idm: text("idm", { length: 16 }).notNull(),
|
||||
accessCode: text("access_code", { length: 20 }).notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
unqIdm: unique().on(t.idm),
|
||||
unqAc: unique().on(t.accessCode),
|
||||
})
|
||||
);
|
||||
|
||||
export type FelicaMobileLookup = typeof felicaMobileLookup.$inferSelect;
|
||||
export type NewFelicaMobileLookup = typeof felicaMobileLookup.$inferInsert;
|
||||
|
||||
export const user = sqliteTable("aimedb_user", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
createdDate: integer("created_date", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$default(() => new Date()),
|
||||
lastLoginDate: integer("last_login_date", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$default(() => new Date()),
|
||||
suspendExpirationDate: integer("suspend_expiration_date", { mode: "timestamp" }),
|
||||
});
|
||||
|
||||
export type AimeUser = typeof user.$inferSelect;
|
||||
export type NewAimeUser = typeof user.$inferInsert;
|
||||
|
||||
export const card = sqliteTable(
|
||||
"aimedb_card",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: integer("user_id").references(() => user.id),
|
||||
accessCode: text("access_code", { length: 20 }).notNull(),
|
||||
createdDate: integer("created_date", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$default(() => new Date()),
|
||||
lastLoginDate: integer("last_login_date", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$default(() => new Date()),
|
||||
isLocked: integer("is_locked", { mode: "boolean" }).default(false).notNull(),
|
||||
isBanned: integer("is_banned", { mode: "boolean" }).default(false).notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
unqAc: unique().on(t.accessCode),
|
||||
})
|
||||
);
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* In theory you should be able to just modify the imports here if you want to
|
||||
* use a different database driver.
|
||||
*/
|
||||
import { integer, text, real, sqliteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const arcade = sqliteTable("allnet_arcade", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
|
||||
name: text("name", { length: 60 }).default("Random arcade at nowhere"),
|
||||
nickname: text("nickname", { length: 40 }).default("Please send help"),
|
||||
country: text("country", { length: 3 }).default("JPN"),
|
||||
|
||||
/**
|
||||
* Largest to smallest units of administrative division
|
||||
*/
|
||||
regionId: integer("region_id").default(1),
|
||||
regionName0: text("region_name0", { length: 48 }).default("W"),
|
||||
regionName1: text("region_name1", { length: 48 }).default(""),
|
||||
regionName2: text("region_name2", { length: 48 }).default(""),
|
||||
regionName3: text("region_name3", { length: 48 }).default(""),
|
||||
|
||||
/**
|
||||
* Client timezone. There's probably no arcades that span
|
||||
* 2 timezones, right...?
|
||||
*/
|
||||
utcOffset: real("utc_offset").default(9),
|
||||
});
|
||||
|
||||
export type Arcade = typeof arcade.$inferSelect;
|
||||
export type NewArcade = typeof arcade.$inferInsert;
|
||||
|
||||
export const arcadeIp = sqliteTable("allnet_arcade_ip", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
|
||||
arcade_id: integer("arcade_id").references(() => arcade.id),
|
||||
ip: text("ip", { length: 15 }).unique(),
|
||||
});
|
||||
|
||||
export type ArcadeIp = typeof arcadeIp.$inferSelect;
|
||||
export type NewArcadeIp = typeof arcadeIp.$inferInsert;
|
||||
|
||||
export const machine = sqliteTable("allnet_machine", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
arcade_id: integer("arcade_id").references(() => arcade.id),
|
||||
|
||||
serial: text("serial", { length: 11 }).unique(),
|
||||
game: text("game", { length: 5 }),
|
||||
|
||||
canVenueHop: integer("can_venue_hop", { mode: "boolean" }),
|
||||
|
||||
lastAuthenticated: integer("last_authenticated", { mode: "timestamp" }),
|
||||
});
|
||||
|
||||
export type Machine = typeof machine.$inferSelect;
|
||||
export type NewMachine = typeof machine.$inferInsert;
|
|
@ -0,0 +1,16 @@
|
|||
import { integer, text, sqliteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const eventLog = sqliteTable("event_log", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
timestamp: integer("timestamp", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$default(() => new Date()),
|
||||
system: text("system").notNull(),
|
||||
type: text("type").notNull(),
|
||||
severity: text("severity").notNull(),
|
||||
message: text("message"),
|
||||
details: text("details", { mode: "json" }),
|
||||
});
|
||||
|
||||
export type EventLog = typeof eventLog.$inferSelect;
|
||||
export type NewEventLog = typeof eventLog.$inferInsert;
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./aimedb";
|
||||
export * from "./allnet";
|
||||
export * from "./base";
|
|
@ -0,0 +1,195 @@
|
|||
import { Config } from "lib/setup/config";
|
||||
import SafeJSONStringify from "safe-json-stringify";
|
||||
import { EscapeStringRegexp } from "utils/misc";
|
||||
import winston, { format, transports } from "winston";
|
||||
import "winston-daily-rotate-file";
|
||||
|
||||
const level = process.env.LOG_LEVEL ?? Config.LOGGER_CONFIG.LOG_LEVEL;
|
||||
|
||||
const formatExcessProperties = (meta: Record<string, unknown>, limit = false) => {
|
||||
let i = 0;
|
||||
|
||||
for (const [key, val] of Object.entries(meta)) {
|
||||
// this is probably fine
|
||||
// eslint-disable-next-line cadence/no-instanceof
|
||||
if (val instanceof Error) {
|
||||
meta[key] = { message: val.message, stack: val.stack };
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!i) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const content = SafeJSONStringify(meta);
|
||||
|
||||
return ` ${limit ? StrCap(content) : content}`;
|
||||
};
|
||||
|
||||
const formatExcessPropertiesNoStack = (
|
||||
meta: Record<string, unknown>,
|
||||
omitKeys: Array<string> = [],
|
||||
limit = false
|
||||
) => {
|
||||
const realMeta: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, val] of Object.entries(meta)) {
|
||||
if (omitKeys.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is probably fine
|
||||
// eslint-disable-next-line cadence/no-instanceof
|
||||
if (val instanceof Error) {
|
||||
realMeta[key] = { message: val.message };
|
||||
} else if (!key.startsWith("__") && !key.startsWith("!")) {
|
||||
realMeta[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(realMeta).length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const content = SafeJSONStringify(realMeta);
|
||||
|
||||
return ` ${limit ? StrCap(content) : content}`;
|
||||
};
|
||||
|
||||
function StrCap(string: string) {
|
||||
if (string.length > 300) {
|
||||
return `${string.slice(0, 297)}...`;
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
const printf = format.printf(
|
||||
({ level, message, context = "toa-root", timestamp, ...meta }) =>
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessProperties(meta)}`
|
||||
);
|
||||
|
||||
const consolePrintf = format.printf(
|
||||
({ level, message, context = "toa-root", timestamp, hideFromConsole, ...meta }) =>
|
||||
`${timestamp} [${
|
||||
Array.isArray(context) ? context.join(" | ") : context
|
||||
}] ${level}: ${message}${formatExcessPropertiesNoStack(
|
||||
meta,
|
||||
hideFromConsole as Array<string>,
|
||||
true
|
||||
)}`
|
||||
);
|
||||
|
||||
winston.addColors({
|
||||
crit: ["bgRed", "black"],
|
||||
error: ["red"],
|
||||
warn: ["yellow"],
|
||||
info: ["blue"],
|
||||
verbose: ["cyan"],
|
||||
debug: ["white"],
|
||||
});
|
||||
|
||||
const baseFormatter = format.combine(
|
||||
format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
})
|
||||
);
|
||||
|
||||
const defaultFormatter = format.combine(baseFormatter, format.errors({ stack: false }), printf);
|
||||
|
||||
const consoleFormatter = format.combine(
|
||||
baseFormatter,
|
||||
format.errors({ stack: false }),
|
||||
consolePrintf,
|
||||
format.colorize({
|
||||
all: true,
|
||||
})
|
||||
);
|
||||
|
||||
const tports: Array<winston.transport> = [];
|
||||
|
||||
if (Config.LOGGER_CONFIG.CONSOLE || process.env.FORCE_CONSOLE_LOG) {
|
||||
tports.push(
|
||||
new transports.Console({
|
||||
format: consoleFormatter,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (Config.LOGGER_CONFIG.FOLDER) {
|
||||
tports.push(
|
||||
new transports.DailyRotateFile({
|
||||
filename: `${Config.LOGGER_CONFIG.FOLDER}/toa-%DATE%.log`,
|
||||
datePattern: "YYYY-MM-DD-HH",
|
||||
zippedArchive: true,
|
||||
maxSize: "20m",
|
||||
maxFiles: "14d",
|
||||
createSymlink: true,
|
||||
symlinkName: "toa.log",
|
||||
format: defaultFormatter,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const rootLogger = winston.createLogger({
|
||||
level,
|
||||
format: defaultFormatter,
|
||||
transports: tports,
|
||||
defaultMeta: {
|
||||
__ServerName: Config.NAME,
|
||||
},
|
||||
});
|
||||
|
||||
if (tports.length === 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"You have no transports set. Absolutely no logs will be saved. This is a terrible idea!"
|
||||
);
|
||||
}
|
||||
|
||||
export default function CreateLogCtx(filename: string, lg = rootLogger): winston.Logger {
|
||||
const replacedFilename = filename.replace(
|
||||
new RegExp(`^${EscapeStringRegexp(process.cwd())}[\\\\/]((js|src)[\\\\/])?`, "u"),
|
||||
""
|
||||
);
|
||||
|
||||
const logger = lg.child({
|
||||
context: [replacedFilename],
|
||||
});
|
||||
|
||||
// @hack, defaultMeta isn't reactive -- won't be updated unless we do this.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
logger.defaultMeta = { ...(logger.defaultMeta ?? {}), context: [replacedFilename] };
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
export function AppendLogCtx(context: string, lg: winston.Logger): winston.Logger {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const newContext = [...lg.defaultMeta.context, context];
|
||||
|
||||
return lg.child({ context: newContext });
|
||||
}
|
||||
|
||||
export function ChangeRootLogLevel(
|
||||
level: "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn"
|
||||
) {
|
||||
rootLogger.info(`Changing log level to ${level}.`);
|
||||
|
||||
for (const tp of tports) {
|
||||
tp.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
export function GetLogLevel() {
|
||||
return (
|
||||
tports.map((e) => e.level).find((e) => typeof e === "string") ??
|
||||
Config.LOGGER_CONFIG.LOG_LEVEL
|
||||
);
|
||||
}
|
||||
|
||||
export const Transports = tports;
|
|
@ -0,0 +1,66 @@
|
|||
import dotenv from "dotenv";
|
||||
import JSON5 from "json5";
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import fs from "fs";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const logger = console;
|
||||
|
||||
const confLocation = process.env.TOA_CONFIG ?? "./config.json5";
|
||||
|
||||
let configFile;
|
||||
|
||||
try {
|
||||
configFile = fs.readFileSync(confLocation, "utf-8");
|
||||
} catch (err) {
|
||||
logger.error("Error while trying to open conf.json5. Is one present?", { err });
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config: unknown = JSON5.parse(configFile);
|
||||
|
||||
const zod16bitNumber = z.number().gt(0).lte(65535);
|
||||
const zodLogLevel = z.enum(["crit", "debug", "error", "info", "verbose", "warn"]);
|
||||
const zodOptionalHexString16 = z
|
||||
.string()
|
||||
.length(16)
|
||||
.regex(/^[0-9a-z]+$/iu, "value is not a hex string")
|
||||
.optional();
|
||||
|
||||
const configSchema = z.object({
|
||||
NAME: z.string(),
|
||||
DATABASE_PATH: z.string(),
|
||||
LISTEN_ADDRESS: z.string().ip(),
|
||||
LOGGER_CONFIG: z.object({
|
||||
LOG_LEVEL: zodLogLevel,
|
||||
CONSOLE: z.boolean(),
|
||||
FOLDER: z.string().optional(),
|
||||
}),
|
||||
ALLNET_CONFIG: z.object({
|
||||
ENABLED: z.boolean(),
|
||||
PORT: zod16bitNumber,
|
||||
ALLOW_UNREGISTERED_SERIALS: z.boolean(),
|
||||
UPDATE_CFG_FOLDER: z.string().optional(),
|
||||
}),
|
||||
AIMEDB_CONFIG: z.object({
|
||||
ENABLED: z.boolean(),
|
||||
PORT: zod16bitNumber,
|
||||
KEY: z.string().length(16).optional(),
|
||||
AIME_MOBILE_CARD_KEY: zodOptionalHexString16,
|
||||
RESERVED_CARD_PREFIX: z.string().length(5).optional(),
|
||||
RESERVED_CARD_KEY: zodOptionalHexString16,
|
||||
}),
|
||||
});
|
||||
|
||||
const parseResult = configSchema.safeParse(config);
|
||||
|
||||
if (!parseResult.success) {
|
||||
const humanFriendlyMessage = fromZodError(parseResult.error);
|
||||
|
||||
throw new Error(`Invalid config.json5 file: ${humanFriendlyMessage}`);
|
||||
}
|
||||
|
||||
export const Config = parseResult.data;
|
|
@ -0,0 +1,19 @@
|
|||
import { aimeDbServerFactory, allnetServer } from "./servers/index";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Config } from "lib/setup/config";
|
||||
import net from "net";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
logger.info(`Booting ${Config.NAME}.`, { bootInfo: true });
|
||||
logger.info(`Log level is set to ${Config.LOGGER_CONFIG.LOG_LEVEL}.`, { bootInfo: true });
|
||||
|
||||
if (Config.ALLNET_CONFIG.ENABLED) {
|
||||
allnetServer.listen(Config.ALLNET_CONFIG.PORT, Config.LISTEN_ADDRESS);
|
||||
}
|
||||
|
||||
if (Config.AIMEDB_CONFIG.ENABLED) {
|
||||
const server = net.createServer(aimeDbServerFactory());
|
||||
|
||||
server.listen(Config.AIMEDB_CONFIG.PORT, Config.LISTEN_ADDRESS);
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
import {
|
||||
AimeAccountQueryStruct,
|
||||
AimeAccountResponseStruct,
|
||||
AimeAccountExtendedResponseStruct,
|
||||
AimeAccountExtendedQueryStruct,
|
||||
} from "../types/aime-account";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, PortalRegistration, ResultCodes } from "../utils/misc";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "external/db/db";
|
||||
import { card } from "external/db/schemas";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export const GetAimeAccountHandler: AimeDBHandlerFn<"AimeAccountResponse"> = async (
|
||||
header,
|
||||
data
|
||||
) => {
|
||||
header.length = PacketHeaderStruct.baseSize + AimeAccountResponseStruct.baseSize;
|
||||
header.commandId = CommandId.AIME_ACCOUNT_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
|
||||
const req = new AimeAccountQueryStruct(data);
|
||||
const resp = new AimeAccountResponseStruct();
|
||||
|
||||
// TODO: Actually handle portal state when we get a webUI
|
||||
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
||||
resp.accountId = -1;
|
||||
|
||||
if (req.companyCode < 0 || req.companyCode > 4) {
|
||||
logger.error("Received unknown company code. Expected a value between 0 and 4 inclusive.", {
|
||||
req,
|
||||
});
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||
|
||||
const cardRow = await db
|
||||
.select()
|
||||
.from(card)
|
||||
.where(eq(card.accessCode, accessCode))
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (!cardRow) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (cardRow.isBanned && cardRow.isLocked) {
|
||||
header.result = ResultCodes.BAN_SYSTEM_AND_USER_LOCK;
|
||||
} else if (cardRow.isBanned) {
|
||||
header.result = ResultCodes.BAN_SYSTEM_LOCK;
|
||||
} else if (cardRow.isLocked) {
|
||||
header.result = ResultCodes.USER_LOCK;
|
||||
}
|
||||
|
||||
resp.accountId = cardRow.id;
|
||||
return resp;
|
||||
};
|
||||
|
||||
export const RegisterAimeAccountHandler: AimeDBHandlerFn<"AimeAccountResponse"> = async (
|
||||
header,
|
||||
data
|
||||
) => {
|
||||
header.length = PacketHeaderStruct.baseSize + AimeAccountResponseStruct.baseSize;
|
||||
header.commandId = CommandId.AIME_ACCOUNT_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
|
||||
const req = new AimeAccountQueryStruct(data);
|
||||
const resp = new AimeAccountResponseStruct();
|
||||
|
||||
// TODO: Actually handle portal state when we get a webUI
|
||||
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
||||
resp.accountId = -1;
|
||||
|
||||
if (req.companyCode < 0 || req.companyCode > 4) {
|
||||
logger.error("Received unknown company code. Expected a value between 0 and 4 inclusive.", {
|
||||
req,
|
||||
});
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||
|
||||
const cardRow = await db
|
||||
.select()
|
||||
.from(card)
|
||||
.where(eq(card.accessCode, accessCode))
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (cardRow) {
|
||||
header.result = ResultCodes.ID_ALREADY_REGISTERED;
|
||||
resp.accountId = cardRow.id;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const newCardRow = await db
|
||||
.insert(card)
|
||||
.values({ accessCode })
|
||||
.returning()
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (!newCardRow) {
|
||||
logger.crit("Failed to insert new lookup entry into the database.", { accessCode });
|
||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||
return resp;
|
||||
}
|
||||
|
||||
resp.accountId = newCardRow.id;
|
||||
return resp;
|
||||
};
|
||||
|
||||
export const GetAimeAccountExtendedHandler: AimeDBHandlerFn<"AimeAccountExtendedResponse"> = async (
|
||||
header,
|
||||
data
|
||||
) => {
|
||||
header.length = PacketHeaderStruct.baseSize + AimeAccountExtendedResponseStruct.baseSize;
|
||||
header.commandId = CommandId.EXTENDED_ACCOUNT_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
|
||||
const req = new AimeAccountExtendedQueryStruct(data);
|
||||
const resp = new AimeAccountExtendedResponseStruct();
|
||||
|
||||
// TODO: Actually handle portal state when we get a webUI
|
||||
// TODO: What the fuck is an auth key
|
||||
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
||||
resp.accountId = -1;
|
||||
|
||||
if (req.companyCode < 0 || req.companyCode > 4) {
|
||||
logger.error("Received unknown company code. Expected a value between 0 and 4 inclusive.", {
|
||||
req,
|
||||
});
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||
|
||||
const cardRow = await db
|
||||
.select()
|
||||
.from(card)
|
||||
.where(eq(card.accessCode, accessCode))
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (!cardRow) {
|
||||
resp.accountId = -1;
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (cardRow.isBanned && cardRow.isLocked) {
|
||||
header.result = ResultCodes.BAN_SYSTEM_AND_USER_LOCK;
|
||||
} else if (cardRow.isBanned) {
|
||||
header.result = ResultCodes.BAN_SYSTEM_LOCK;
|
||||
} else if (cardRow.isLocked) {
|
||||
header.result = ResultCodes.USER_LOCK;
|
||||
}
|
||||
|
||||
resp.accountId = cardRow.id;
|
||||
return resp;
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
import {
|
||||
AimeLogStruct,
|
||||
AimeLogExtendedResponseStruct,
|
||||
ExtendedAimeLogStruct,
|
||||
StatusLogStruct,
|
||||
} from "../types/aime-log";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, LogStatus, ResultCodes } from "../utils/misc";
|
||||
import { db } from "external/db/db";
|
||||
import { eventLog } from "external/db/schemas";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export const StatusLogHandler: AimeDBHandlerFn = async (header, data) => {
|
||||
header.commandId = CommandId.STATUS_LOG_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
header.length = PacketHeaderStruct.baseSize;
|
||||
|
||||
const req = new StatusLogStruct(data);
|
||||
const statusName = LogStatus[req.status];
|
||||
|
||||
if (!statusName) {
|
||||
logger.error("Unknown status for logging. Expected a value between 0 and 4.", { req });
|
||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||
return null;
|
||||
}
|
||||
|
||||
await db.insert(eventLog).values({
|
||||
system: "aimedb",
|
||||
type: `AIMEDB_LOG_${statusName}`,
|
||||
severity: "info",
|
||||
details: { aimeId: req.aimeId },
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const AimeLogHandler: AimeDBHandlerFn = async (header, data) => {
|
||||
header.commandId = CommandId.AIME_LOG_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
header.length = PacketHeaderStruct.baseSize;
|
||||
|
||||
const req = new AimeLogStruct(data);
|
||||
const statusName = LogStatus[req.status];
|
||||
|
||||
if (!statusName) {
|
||||
logger.error("Unknown status for logging. Expected a value between 0 and 4.", { req });
|
||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||
return null;
|
||||
}
|
||||
|
||||
await db.insert(eventLog).values({
|
||||
system: "aimedb",
|
||||
type: `AIMEDB_LOG_${statusName}`,
|
||||
severity: "info",
|
||||
details: {
|
||||
aimeId: req.aimeId,
|
||||
userId: req.userId,
|
||||
creditCount: req.creditCount,
|
||||
betCount: req.betCount,
|
||||
wonCount: req.wonCount,
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const AimeExtendedLogHandler: AimeDBHandlerFn<"AimeLogExtendedResponse"> = async (
|
||||
header,
|
||||
data
|
||||
) => {
|
||||
header.commandId = CommandId.EXTENDED_AIME_LOG_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
header.length = PacketHeaderStruct.baseSize;
|
||||
|
||||
const req = new ExtendedAimeLogStruct(data);
|
||||
const resp = new AimeLogExtendedResponseStruct();
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const ops = [];
|
||||
|
||||
for (let i = 0; i < req.count; i++) {
|
||||
const entry = req.entries[i];
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
"There was an undefined value in the log entries. This should not be possible!"
|
||||
);
|
||||
}
|
||||
|
||||
const statusName = LogStatus[entry.status];
|
||||
|
||||
if (!statusName) {
|
||||
logger.error(
|
||||
"Unknown status for logging. Expected a value between 0 and 4.",
|
||||
entry
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
ops.push(
|
||||
tx.insert(eventLog).values({
|
||||
system: "aimedb",
|
||||
type: `AIMEDB_LOG_${statusName}`,
|
||||
severity: "info",
|
||||
details: {
|
||||
aimeId: entry.aimeId,
|
||||
userId: entry.userId.toString(),
|
||||
creditCount: entry.creditCount,
|
||||
betCount: entry.betCount,
|
||||
wonCount: entry.wonCount,
|
||||
},
|
||||
})
|
||||
);
|
||||
resp.result[i] = 1;
|
||||
}
|
||||
|
||||
await Promise.all(ops);
|
||||
});
|
||||
|
||||
return resp;
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
// TODO: Actually support campaigns
|
||||
import {
|
||||
CampaignClearInfoResponseStruct,
|
||||
CampaignResponseStruct,
|
||||
OldCampaignResponseStruct,
|
||||
} from "../types/campaign";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, ResultCodes } from "../utils/misc";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
||||
export const GetCampaignInfoHandler: AimeDBHandlerFn<"CampaignResponse" | "OldCampaignResponse"> = (
|
||||
header,
|
||||
_
|
||||
) => {
|
||||
header.commandId = CommandId.CAMPAIGN_INFO_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
|
||||
if (header.version < 0x3030) {
|
||||
header.length = PacketHeaderStruct.baseSize + OldCampaignResponseStruct.baseSize;
|
||||
return new OldCampaignResponseStruct();
|
||||
}
|
||||
|
||||
header.length = PacketHeaderStruct.baseSize + CampaignResponseStruct.baseSize;
|
||||
return new CampaignResponseStruct();
|
||||
};
|
||||
|
||||
export const GetCampaignClearInfoHandler: AimeDBHandlerFn<"CampaignClearInfoResponse"> = (
|
||||
header,
|
||||
_
|
||||
) => {
|
||||
header.commandId = CommandId.CAMPAIGN_CLEAR_INFO_RESPONSE;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
header.length = PacketHeaderStruct.baseSize + CampaignClearInfoResponseStruct.baseSize;
|
||||
|
||||
return new CampaignClearInfoResponseStruct();
|
||||
};
|
|
@ -0,0 +1,271 @@
|
|||
// On real ALL.Net, there's just a massive database of every supported FeliCa's
|
||||
// IDm -> access code mappings. Since we don't have access to such information,
|
||||
// every FeliCa card will be treated like FeliCa mobile: an access code is
|
||||
// generated, and then stored with its IDm for future lookups.
|
||||
//
|
||||
// This also means you'll need to get some more keys, but if you're lazy
|
||||
// you can probably just set
|
||||
// - RESERVED_CARD_PREFIX to something not starting with 0 or 3
|
||||
// - RESERVED_CARD_KEY to a random 16-digit hex string, where each digit displays
|
||||
// exactly once.
|
||||
import {
|
||||
FelicaExtendedLookupRequestStruct,
|
||||
FelicaExtendedLookupResponseStruct,
|
||||
FelicaLookupRequestStruct,
|
||||
FelicaLookupResponseStruct,
|
||||
} from "../types/felica-conversion";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CalculateAccessCode } from "../utils/access-code";
|
||||
import { IsSupportedFelicaMobile, IsSupportedFelica } from "../utils/felica";
|
||||
import { CommandId, CompanyCodes, PortalRegistration, ResultCodes } from "../utils/misc";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { db } from "external/db/db";
|
||||
import { card, felicaCardLookup, felicaMobileLookup } from "external/db/schemas/index";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Config } from "lib/setup/config";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export const FelicaLookupHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = async (
|
||||
header,
|
||||
data
|
||||
) => {
|
||||
header.commandId = CommandId.FELICA_LOOKUP_RESPONSE;
|
||||
header.length = PacketHeaderStruct.baseSize + FelicaLookupResponseStruct.baseSize;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
|
||||
const req = new FelicaLookupRequestStruct(data);
|
||||
const resp = new FelicaLookupResponseStruct();
|
||||
|
||||
if (!IsSupportedFelica(req.osVer)) {
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
resp.felicaIndex = -1;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const table = IsSupportedFelicaMobile(req.osVer) ? felicaMobileLookup : felicaCardLookup;
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(eq(table.idm, req.idm.toString(16)))
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (!result) {
|
||||
resp.felicaIndex = -1;
|
||||
} else {
|
||||
resp.felicaIndex = result.id;
|
||||
resp.accessCode.set(Buffer.from(result.accessCode, "hex"));
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
export const FelicaExtendedLookupHandler: AimeDBHandlerFn<"FelicaExtendedLookupResponse"> = async (
|
||||
header,
|
||||
data
|
||||
) => {
|
||||
header.commandId = CommandId.EXTENDED_FELICA_ACCOUNT_RESPONSE;
|
||||
header.length = PacketHeaderStruct.baseSize + FelicaExtendedLookupResponseStruct.baseSize;
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
|
||||
const req = new FelicaExtendedLookupRequestStruct(data);
|
||||
const resp = new FelicaExtendedLookupResponseStruct();
|
||||
|
||||
resp.accountId = -1;
|
||||
|
||||
logger.debug("Parsed response body.", { req });
|
||||
|
||||
if (req.companyCode < 0 || req.companyCode > 4) {
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (!IsSupportedFelica(req.osVer)) {
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
return resp;
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (IsSupportedFelicaMobile(req.osVer)) {
|
||||
result = await db
|
||||
.select()
|
||||
.from(felicaMobileLookup)
|
||||
.where(eq(felicaMobileLookup.idm, req.idm.toString(16)))
|
||||
.leftJoin(card, eq(card.accessCode, felicaMobileLookup.accessCode))
|
||||
.then((r) => r[0]);
|
||||
} else {
|
||||
result = await db
|
||||
.select()
|
||||
.from(felicaCardLookup)
|
||||
.where(eq(felicaCardLookup.idm, req.idm.toString(16)))
|
||||
.leftJoin(card, eq(card.accessCode, felicaCardLookup.accessCode))
|
||||
.then((r) => r[0]);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const lookupResult =
|
||||
"aimedb_felica_mobile_lookup" in result
|
||||
? result.aimedb_felica_mobile_lookup
|
||||
: result.aimedb_felica_card_lookup;
|
||||
const cardResult = result.aimedb_card;
|
||||
|
||||
resp.accessCode.set(Buffer.from(lookupResult.accessCode, "hex"));
|
||||
|
||||
if (cardResult) {
|
||||
resp.accountId = cardResult.id;
|
||||
resp.portalRegistered = PortalRegistration.UNREGISTERED;
|
||||
|
||||
// HACK: Since we cannot possibly know who made it (even AICC cards have
|
||||
// the same manufacturer code `01:2e`!), we're just going to treat everything
|
||||
// as a SEGA card.
|
||||
resp.companyCode = CompanyCodes.SEGA;
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Assuming that FeliCa Mobile is handled by their own registration endpoint...
|
||||
if (IsSupportedFelicaMobile(req.osVer)) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Card is not in the lookup tables, register a new card...
|
||||
if (!Config.AIMEDB_CONFIG.RESERVED_CARD_KEY || !Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX) {
|
||||
logger.error(
|
||||
"AIMEDB_CONFIG.RESERVED_CARD_KEY or AIMEDB_CONFIG.RESERVED_CARD_PREFIX is not set in config file. Cannot generate a new access code.",
|
||||
{ req }
|
||||
);
|
||||
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const mostRecentRow = await db
|
||||
.select()
|
||||
.from(felicaCardLookup)
|
||||
.orderBy(desc(felicaCardLookup.id))
|
||||
.limit(1)
|
||||
.then((r) => r[0]);
|
||||
|
||||
const nextId = mostRecentRow ? mostRecentRow.id + 1 : 1;
|
||||
|
||||
const accessCode = CalculateAccessCode(
|
||||
nextId,
|
||||
Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX,
|
||||
Config.AIMEDB_CONFIG.RESERVED_CARD_KEY
|
||||
);
|
||||
|
||||
logger.verbose(`Created FeliCa Card access code for serial ${nextId}.`, {
|
||||
nextId,
|
||||
accessCode,
|
||||
});
|
||||
|
||||
const value = { idm: req.idm.toString(16), accessCode };
|
||||
const row = await db
|
||||
.insert(felicaCardLookup)
|
||||
.values(value)
|
||||
.returning()
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (!row) {
|
||||
logger.crit("Failed to insert new lookup entry into the database.", value);
|
||||
|
||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||
return resp;
|
||||
}
|
||||
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
resp.accessCode.set(Buffer.from(row.accessCode, "hex"));
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
export const FelicaRegisterHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = async (
|
||||
header,
|
||||
data
|
||||
) => {
|
||||
header.commandId = CommandId.FELICA_LOOKUP_RESPONSE;
|
||||
header.length = PacketHeaderStruct.baseSize + FelicaLookupResponseStruct.baseSize;
|
||||
|
||||
const req = new FelicaLookupRequestStruct(data);
|
||||
const resp = new FelicaLookupResponseStruct();
|
||||
|
||||
if (!IsSupportedFelica(req.osVer)) {
|
||||
logger.verbose("Rejecting card of unknown OS version.", {
|
||||
req,
|
||||
});
|
||||
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
resp.felicaIndex = -1;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(felicaMobileLookup)
|
||||
.where(eq(felicaMobileLookup.idm, req.idm.toString(16)))
|
||||
.limit(1)
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (result) {
|
||||
header.result = ResultCodes.ID_ALREADY_REGISTERED;
|
||||
resp.felicaIndex = result.id;
|
||||
resp.accessCode.set(Buffer.from(result.accessCode, "hex"));
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (!Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY) {
|
||||
logger.error(
|
||||
"AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY is not set in config file. Cannot register FeliCa Mobile ID.",
|
||||
{ req }
|
||||
);
|
||||
|
||||
header.result = ResultCodes.INVALID_AIME_ID;
|
||||
resp.felicaIndex = -1;
|
||||
return resp;
|
||||
}
|
||||
|
||||
const mostRecentRow = await db
|
||||
.select()
|
||||
.from(felicaMobileLookup)
|
||||
.orderBy(desc(felicaMobileLookup.id))
|
||||
.limit(1)
|
||||
.then((r) => r[0]);
|
||||
|
||||
const nextId = mostRecentRow ? mostRecentRow.id + 1 : 1;
|
||||
|
||||
const accessCode = CalculateAccessCode(
|
||||
nextId,
|
||||
"01035",
|
||||
Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY
|
||||
);
|
||||
|
||||
logger.verbose(`Created FeliCa Mobile access code for serial ${nextId}.`, {
|
||||
nextId,
|
||||
accessCode,
|
||||
});
|
||||
|
||||
const value = { idm: req.idm.toString(16), accessCode };
|
||||
const row = await db
|
||||
.insert(felicaMobileLookup)
|
||||
.values(value)
|
||||
.returning()
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (!row) {
|
||||
logger.crit("Failed to insert new lookup entry into the database.", value);
|
||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||
resp.felicaIndex = -1;
|
||||
return resp;
|
||||
}
|
||||
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
resp.felicaIndex = row.id;
|
||||
resp.accessCode.set(Buffer.from(row.accessCode, "hex"));
|
||||
|
||||
return resp;
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
GetAimeAccountHandler,
|
||||
GetAimeAccountExtendedHandler,
|
||||
RegisterAimeAccountHandler,
|
||||
} from "./aime-account";
|
||||
import { AimeExtendedLogHandler, AimeLogHandler, StatusLogHandler } from "./aime-log";
|
||||
import { GetCampaignClearInfoHandler, GetCampaignInfoHandler } from "./campaign";
|
||||
import {
|
||||
FelicaExtendedLookupHandler,
|
||||
FelicaLookupHandler,
|
||||
FelicaRegisterHandler,
|
||||
} from "./felica-conversion";
|
||||
import { ServiceHealthCheckHandler } from "./status-check";
|
||||
import { CommandId } from "../utils/misc";
|
||||
import type { AimeDBHandlerFn, AimeDBReturnTypes } from "../types/handlers";
|
||||
|
||||
type CommandIdRequest = {
|
||||
[K in keyof typeof CommandId]: K extends `${infer _}_REQUEST` ? typeof CommandId[K] : never;
|
||||
}[keyof typeof CommandId];
|
||||
|
||||
// We need to make the key type wider so that the request handler can still index with
|
||||
// an arbitrary command ID. On the other hand, the `satisfies` clause makes TypeScript
|
||||
// yells at us for any known unimplemented AimeDB commands.
|
||||
export const AIMEDB_HANDLERS: Record<number, AimeDBHandlerFn<AimeDBReturnTypes>> = {
|
||||
[CommandId.FELICA_LOOKUP_REQUEST]: FelicaLookupHandler,
|
||||
[CommandId.FELICA_REGISTER_REQUEST]: FelicaRegisterHandler,
|
||||
[CommandId.AIME_ACCOUNT_REQUEST]: GetAimeAccountHandler,
|
||||
[CommandId.REGISTER_AIME_ACCOUNT_REQUEST]: RegisterAimeAccountHandler,
|
||||
[CommandId.STATUS_LOG_REQUEST]: StatusLogHandler,
|
||||
[CommandId.AIME_LOG_REQUEST]: AimeLogHandler,
|
||||
[CommandId.EXTENDED_AIME_LOG_REQUEST]: AimeExtendedLogHandler,
|
||||
[CommandId.CAMPAIGN_INFO_REQUEST]: GetCampaignInfoHandler,
|
||||
[CommandId.CAMPAIGN_CLEAR_INFO_REQUEST]: GetCampaignClearInfoHandler,
|
||||
[CommandId.EXTENDED_ACCOUNT_REQUEST]: GetAimeAccountExtendedHandler,
|
||||
[CommandId.EXTENDED_FELICA_ACCOUNT_REQUEST]: FelicaExtendedLookupHandler,
|
||||
[CommandId.SERVICE_HEALTH_REQUEST]: ServiceHealthCheckHandler,
|
||||
} satisfies Record<
|
||||
Exclude<CommandIdRequest, CommandId.CLIENT_END_REQUEST>,
|
||||
AimeDBHandlerFn<AimeDBReturnTypes>
|
||||
>;
|
||||
|
||||
export const EXPECTED_PACKET_LENGTH: Record<number, number> = {
|
||||
[CommandId.FELICA_LOOKUP_REQUEST]: 48,
|
||||
[CommandId.FELICA_REGISTER_REQUEST]: 48,
|
||||
[CommandId.AIME_ACCOUNT_REQUEST]: 48,
|
||||
[CommandId.REGISTER_AIME_ACCOUNT_REQUEST]: 48,
|
||||
[CommandId.STATUS_LOG_REQUEST]: 48,
|
||||
[CommandId.AIME_LOG_REQUEST]: 64,
|
||||
[CommandId.EXTENDED_AIME_LOG_REQUEST]: 1008,
|
||||
[CommandId.CAMPAIGN_INFO_REQUEST]: 48,
|
||||
[CommandId.CAMPAIGN_CLEAR_INFO_REQUEST]: 48,
|
||||
[CommandId.EXTENDED_ACCOUNT_REQUEST]: 48,
|
||||
[CommandId.EXTENDED_FELICA_ACCOUNT_REQUEST]: 112,
|
||||
[CommandId.SERVICE_HEALTH_REQUEST]: 32,
|
||||
[CommandId.CLIENT_END_REQUEST]: 32,
|
||||
} satisfies Record<CommandIdRequest, number>;
|
|
@ -0,0 +1,11 @@
|
|||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, ResultCodes } from "../utils/misc";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
||||
export const ServiceHealthCheckHandler: AimeDBHandlerFn = (header, _) => {
|
||||
header.result = ResultCodes.SUCCESS;
|
||||
header.commandId = CommandId.SERVICE_HEALTH_RESPONSE;
|
||||
header.length = PacketHeaderStruct.baseSize;
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,178 @@
|
|||
import { AIMEDB_HANDLERS, EXPECTED_PACKET_LENGTH } from "./handlers";
|
||||
import { PacketHeaderStruct } from "./types/header";
|
||||
import { decryptPacket, encryptPacket } from "./utils/crypto";
|
||||
import { CommandId, ResultCodes } from "./utils/misc";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Config } from "lib/setup/config";
|
||||
import { createHash } from "crypto";
|
||||
import type { Socket } from "net";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
/**
|
||||
* For legal reasons, we probably should not include the actual AimeDB key here.
|
||||
* However, we can still check the key for correctness, so that users get feedback.
|
||||
*/
|
||||
const AIMEDB_KEY_SHA256 = "4968f79897cd9517f2b6050a951456106ca98b23535b3e985d62a800b66f3240";
|
||||
|
||||
const AimeDBServerFactory = () => {
|
||||
if (!Config.AIMEDB_CONFIG.KEY) {
|
||||
throw new Error("AimeDB key not set.");
|
||||
}
|
||||
|
||||
const hash = createHash("sha256");
|
||||
const digest = hash.update(Config.AIMEDB_CONFIG.KEY).digest("hex");
|
||||
|
||||
if (digest !== AIMEDB_KEY_SHA256) {
|
||||
logger.warn(
|
||||
"AimeDB key seems incorrect. Allowing it anyways, though games will probably break.",
|
||||
{
|
||||
key: Config.AIMEDB_CONFIG.KEY,
|
||||
expectedSha256: AIMEDB_KEY_SHA256,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const AIMEDB_KEY = Buffer.from(Config.AIMEDB_CONFIG.KEY, "utf-8");
|
||||
|
||||
const logResponse = (header: InstanceType<typeof PacketHeaderStruct>, body: unknown) => {
|
||||
if (header.result !== 1) {
|
||||
logger.info(`Returned result code ${header.result}.`, {
|
||||
header,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
logger.verbose(`Returned result code ${header.result}.`, {
|
||||
header,
|
||||
body,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const writeResponse = (
|
||||
socket: Socket,
|
||||
header: InstanceType<typeof PacketHeaderStruct>,
|
||||
body: unknown
|
||||
) => {
|
||||
const headerBuf = PacketHeaderStruct.raw(header);
|
||||
const chunks = [headerBuf];
|
||||
|
||||
// https://github.com/sarakusha/typed-struct/blob/main/src/struct.ts#L799
|
||||
if (body !== null && typeof body === "object" && "$raw" in body) {
|
||||
const bodyBuf = (body as { $raw: Buffer }).$raw;
|
||||
|
||||
chunks.push(bodyBuf);
|
||||
}
|
||||
|
||||
const raw = Buffer.concat(chunks);
|
||||
|
||||
let encrypted: Buffer;
|
||||
|
||||
try {
|
||||
encrypted = encryptPacket(raw, AIMEDB_KEY);
|
||||
} catch (err) {
|
||||
logger.error("Could not encrypt AimeDB response.", { err });
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write(encrypted);
|
||||
};
|
||||
|
||||
return (socket: Socket) => {
|
||||
socket.on("connect", () => {
|
||||
logger.debug(`${socket.remoteAddress} connected.`);
|
||||
});
|
||||
|
||||
socket.on("data", async (data) => {
|
||||
logger.verbose(`Received packet: ${data.toString("hex")}.`);
|
||||
|
||||
let packet: Buffer;
|
||||
|
||||
try {
|
||||
packet = decryptPacket(data, AIMEDB_KEY);
|
||||
} catch (err) {
|
||||
logger.error("Could not decrypt AimeDB packet.", {
|
||||
err,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let header;
|
||||
|
||||
try {
|
||||
header = new PacketHeaderStruct(packet.slice(0, 32));
|
||||
} catch (err) {
|
||||
logger.error("Decoding AimeDB header failed.", {
|
||||
err,
|
||||
data: packet.toString("hex"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Received AimeDB request.", {
|
||||
header,
|
||||
data: packet.slice(32).toString("hex"),
|
||||
});
|
||||
|
||||
if (header.magic !== 0xa13e) {
|
||||
logger.error("Request's magic bytes did not match expected value 0xA13E.", {
|
||||
header,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (header.keychipId === "ABCD1234567" || header.storeId === 0xfff0) {
|
||||
logger.warning("Received request from uninitialized AMLib.", { header });
|
||||
}
|
||||
|
||||
if (header.commandId === CommandId.CLIENT_END_REQUEST) {
|
||||
logger.debug("Client ended the session.", { header });
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedLength = EXPECTED_PACKET_LENGTH[header.commandId];
|
||||
|
||||
if (!expectedLength) {
|
||||
logger.warn(
|
||||
`Packet 0x${header.commandId.toString(
|
||||
16
|
||||
)} does not declare an expected size. Allowing all packets.`,
|
||||
{ header }
|
||||
);
|
||||
} else if (expectedLength !== header.length || packet.length !== header.length) {
|
||||
logger.error("Packet does not have expected size.", {
|
||||
expectedLength,
|
||||
actualLength: packet.length,
|
||||
declaredLength: header.length,
|
||||
});
|
||||
|
||||
header.result = ResultCodes.UNKNOWN_ERROR;
|
||||
header.length = 32;
|
||||
|
||||
logResponse(header, null);
|
||||
writeResponse(socket, header, null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = AIMEDB_HANDLERS[header.commandId];
|
||||
|
||||
if (!handler) {
|
||||
logger.error("No handlers available for the requested command ID.", { header });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await handler(header, packet.slice(32));
|
||||
|
||||
logResponse(header, body);
|
||||
writeResponse(socket, header, body);
|
||||
});
|
||||
|
||||
socket.on("end", () => {
|
||||
logger.debug(`${socket.remoteAddress} disconnected.`);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export default AimeDBServerFactory;
|
|
@ -0,0 +1,30 @@
|
|||
import Struct from "typed-struct";
|
||||
|
||||
export const AimeAccountQueryStruct = new Struct("AimeAccountQuery")
|
||||
.UInt8Array("accessCode", 10)
|
||||
.UInt8("companyCode")
|
||||
.UInt8("firmwareVersion")
|
||||
.UInt32LE("serialNumber")
|
||||
.compile();
|
||||
|
||||
export const AimeAccountResponseStruct = new Struct("AimeAccountResponse")
|
||||
.UInt32LE("accountId")
|
||||
.UInt8("portalRegistered")
|
||||
.UInt8Array("padding", 11)
|
||||
.compile();
|
||||
|
||||
export const AimeAccountExtendedQueryStruct = new Struct("AimeAccountExtendedQuery")
|
||||
.UInt8Array("accessCode", 10)
|
||||
.UInt8("companyCode")
|
||||
.UInt8("readerFirmwareVersion")
|
||||
.UInt32LE("cardSerialNumber")
|
||||
.compile();
|
||||
|
||||
export const AimeAccountExtendedResponseStruct = new Struct("AimeAccountExtendedResponse")
|
||||
.UInt32LE("accountId")
|
||||
.UInt8("portalRegistered")
|
||||
.UInt8Array("padding", 3)
|
||||
.UInt8Array("authKey", 256)
|
||||
.UInt32LE("relationId1")
|
||||
.UInt32LE("relationId2")
|
||||
.compile();
|
|
@ -0,0 +1,41 @@
|
|||
import Struct from "typed-struct";
|
||||
|
||||
export const StatusLogStruct = new Struct("StatusLog")
|
||||
.UInt32LE("aimeId")
|
||||
.UInt32LE("status")
|
||||
.UInt8Array("padding", 8)
|
||||
.compile();
|
||||
|
||||
export const AimeLogStruct = new Struct("AimeLog")
|
||||
.UInt32LE("aimeId")
|
||||
.UInt32LE("status")
|
||||
.BigUInt64LE("userId")
|
||||
.Int32LE("creditCount")
|
||||
.Int32LE("betCount")
|
||||
.Int32LE("wonCount")
|
||||
.UInt8Array("padding", 4)
|
||||
.compile();
|
||||
|
||||
export const ExtendedAimeLogEntryStruct = new Struct("ExtendedAimeLogEntry")
|
||||
.UInt32LE("aimeId")
|
||||
.UInt32LE("status")
|
||||
.BigUInt64LE("userId")
|
||||
.Int32LE("creditCount")
|
||||
.Int32LE("betCount")
|
||||
.Int32LE("wonCount")
|
||||
.UInt8Array("padding", 4)
|
||||
.BigUInt64LE("localTime")
|
||||
.Int32LE("tSeq")
|
||||
.UInt32LE("placeId")
|
||||
.compile();
|
||||
|
||||
export const ExtendedAimeLogStruct = new Struct("ExtendedAimeLog")
|
||||
.StructArray("entries", ExtendedAimeLogEntryStruct, 20)
|
||||
.UInt32LE("count")
|
||||
.UInt8Array("padding", 12)
|
||||
.compile();
|
||||
|
||||
export const AimeLogExtendedResponseStruct = new Struct("AimeLogExtendedResponse")
|
||||
.UInt8Array("result", 20)
|
||||
.UInt8Array("padding", 12)
|
||||
.compile();
|
|
@ -0,0 +1,47 @@
|
|||
import Struct from "typed-struct";
|
||||
|
||||
/**
|
||||
* Retrieve information regarding currently active campaigns.
|
||||
*/
|
||||
export const OldCampaignRequestStruct = new Struct("OldCampaignRequest")
|
||||
.UInt32LE("campaignId")
|
||||
.UInt8Array("padding", 12)
|
||||
.compile();
|
||||
|
||||
export const OldCampaignResponseStruct = new Struct("OldCampaignResponse")
|
||||
.Int32LE("info0")
|
||||
.Int32LE("info1")
|
||||
.Int32LE("info2")
|
||||
.Int32LE("info3")
|
||||
.compile();
|
||||
|
||||
const CampaignStruct = new Struct("Campaign")
|
||||
.UInt32LE("campaignId")
|
||||
.String("campaignName", 128)
|
||||
.UInt32LE("announceDate")
|
||||
.UInt32LE("startDate")
|
||||
.UInt32LE("endDate")
|
||||
.UInt32LE("distributionStartDate")
|
||||
.UInt32LE("distributionEndDate")
|
||||
.UInt8Array("padding", 8)
|
||||
.compile();
|
||||
|
||||
export const CampaignResponseStruct = new Struct("CampaignResponse")
|
||||
.StructArray("campaigns", CampaignStruct, 3)
|
||||
.compile();
|
||||
|
||||
export const CampaignClearInfoRequestStruct = new Struct("CampaignClearInfoRequest")
|
||||
.UInt32LE("aimeId")
|
||||
.UInt8Array("padding", 12)
|
||||
.compile();
|
||||
|
||||
export const CampaignClearInfoStruct = new Struct("CampaignClearInfo")
|
||||
.UInt32LE("campaignId")
|
||||
.UInt32LE("entryFlag")
|
||||
.UInt32LE("clearFlag")
|
||||
.UInt8Array("padding", 4)
|
||||
.compile();
|
||||
|
||||
export const CampaignClearInfoResponseStruct = new Struct("CampaignClearInfoResponse")
|
||||
.StructArray("clearInfos", CampaignClearInfoStruct, 3)
|
||||
.compile();
|
|
@ -0,0 +1,40 @@
|
|||
import Struct from "typed-struct";
|
||||
|
||||
export const FelicaLookupRequestStruct = new Struct("FelicaLookupRequest")
|
||||
.BigUInt64LE("idm")
|
||||
.UInt8("chipCode")
|
||||
.UInt8("osVer")
|
||||
.UInt8Array("timing", 6)
|
||||
.compile();
|
||||
|
||||
export const FelicaLookupResponseStruct = new Struct("FelicaLookupResponse")
|
||||
.UInt32LE("felicaIndex")
|
||||
.UInt8Array("accessCode", 10)
|
||||
.UInt16LE("padding")
|
||||
.compile();
|
||||
|
||||
export const FelicaExtendedLookupRequestStruct = new Struct("FelicaExtendedLookupRequest")
|
||||
.UInt8Array("randomChallenge", 16)
|
||||
.BigUInt64LE("idm")
|
||||
.UInt8("chipCode")
|
||||
.UInt8("osVer")
|
||||
.UInt8Array("timing", 6)
|
||||
.UInt8Array("cardKeyVersion", 16)
|
||||
.UInt8Array("writeCount", 16)
|
||||
.BigUInt64LE("maca")
|
||||
.UInt8("companyCode")
|
||||
.UInt8("readerFirmwareVersion")
|
||||
.UInt16LE("DFC")
|
||||
.UInt8Array("padding", 4)
|
||||
.compile();
|
||||
|
||||
export const FelicaExtendedLookupResponseStruct = new Struct("FelicaExtendedLookupResponse")
|
||||
.UInt32LE("accountId")
|
||||
.UInt32LE("relationId1")
|
||||
.UInt32LE("relationId2")
|
||||
.UInt8Array("accessCode", 10)
|
||||
.UInt8("portalRegistered")
|
||||
.UInt8("companyCode")
|
||||
.UInt8Array("padding", 8)
|
||||
.UInt8Array("authKey", 256)
|
||||
.compile();
|
|
@ -0,0 +1,31 @@
|
|||
import type { PacketHeaderStruct } from "./header";
|
||||
import type { StructConstructor } from "typed-struct";
|
||||
|
||||
export type AimeDBHandlerReturnType<S extends string> = InstanceType<
|
||||
StructConstructor<Record<string, unknown>, S>
|
||||
>;
|
||||
|
||||
export type AimeDBReturnTypes =
|
||||
| "__no_body"
|
||||
| "AimeAccountExtendedResponse"
|
||||
| "AimeAccountResponse"
|
||||
| "AimeLogExtendedResponse"
|
||||
| "CampaignClearInfoResponse"
|
||||
| "CampaignResponse"
|
||||
| "FelicaExtendedLookupResponse"
|
||||
| "FelicaLookupResponse"
|
||||
| "OldCampaignResponse";
|
||||
|
||||
type MaybePromise<T> = Promise<T> | T;
|
||||
|
||||
/**
|
||||
* Base type for all AimeDB command handlers.
|
||||
*
|
||||
* All AimeDB handlers must modify the request header and only return the request body.
|
||||
* The base handler will merge them into one.
|
||||
*/
|
||||
|
||||
export type AimeDBHandlerFn<S extends AimeDBReturnTypes = "__no_body"> = (
|
||||
header: InstanceType<typeof PacketHeaderStruct>,
|
||||
data: Buffer
|
||||
) => MaybePromise<(S extends "__no_body" ? never : AimeDBHandlerReturnType<S>) | null | undefined>;
|
|
@ -0,0 +1,12 @@
|
|||
import Struct from "typed-struct";
|
||||
|
||||
export const PacketHeaderStruct = new Struct("PacketHeader")
|
||||
.UInt16LE("magic")
|
||||
.UInt16LE("version")
|
||||
.UInt16LE("commandId")
|
||||
.UInt16LE("length")
|
||||
.UInt16LE("result")
|
||||
.String("gameId", 6)
|
||||
.UInt32LE("storeId")
|
||||
.String("keychipId", 12)
|
||||
.compile();
|
|
@ -0,0 +1,53 @@
|
|||
import { Solitaire } from "./crypto";
|
||||
import { createHash } from "crypto";
|
||||
import type { integer } from "types/misc";
|
||||
|
||||
function reverseString(data: string) {
|
||||
return Array.from(data).reverse().join("");
|
||||
}
|
||||
|
||||
function CalculateCardKey(serial: integer, key: string) {
|
||||
const paddedSerial = serial.toString().padStart(8, "0");
|
||||
const realDigest = createHash("md5").update(paddedSerial).digest();
|
||||
|
||||
const digest = new Array(16);
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const idx = Number(`0x${key[i]}`);
|
||||
const nib = realDigest[idx];
|
||||
|
||||
if (nib === undefined) {
|
||||
throw new Error(
|
||||
"crypto.createHash returned an undefined value in a Buffer. this should not happen."
|
||||
);
|
||||
}
|
||||
|
||||
digest[i] = nib;
|
||||
}
|
||||
|
||||
// nasty ass bit string hacks that i am not good enough at math to replace
|
||||
let bitstring = reverseString(
|
||||
digest.map((n) => reverseString(n.toString(2).padStart(8, "0"))).join("")
|
||||
).padStart(6 * 23, "0");
|
||||
let computed = 0;
|
||||
|
||||
while (bitstring) {
|
||||
const work = Number(`0b${bitstring.slice(0, 23)}`);
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
computed = computed ^ work;
|
||||
|
||||
bitstring = bitstring.slice(23);
|
||||
}
|
||||
|
||||
return computed.toString().padStart(7, "0");
|
||||
}
|
||||
|
||||
export function CalculateAccessCode(serial: integer, prefix: string, key: string): string {
|
||||
const digest = CalculateCardKey(serial, key);
|
||||
|
||||
const paddedSerial = serial.toString().padStart(8, "0");
|
||||
const hashedId = Solitaire.encrypt(paddedSerial, digest);
|
||||
|
||||
return `${prefix}${hashedId}${digest}`;
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
import { createCipheriv, createDecipheriv } from "crypto";
|
||||
import type { integer } from "types/misc";
|
||||
|
||||
const ASCII_0 = 48;
|
||||
|
||||
export function encryptPacket(packet: Buffer, key: Buffer) {
|
||||
const cipher = createCipheriv("aes-128-ecb", key, null).setAutoPadding(false);
|
||||
|
||||
return Buffer.concat([cipher.update(packet), cipher.final()]);
|
||||
}
|
||||
|
||||
export function decryptPacket(packet: Buffer, key: Buffer) {
|
||||
const cipher = createDecipheriv("aes-128-ecb", key, null).setAutoPadding(false);
|
||||
|
||||
return Buffer.concat([cipher.update(packet), cipher.final()]);
|
||||
}
|
||||
|
||||
function char2num(char: string) {
|
||||
return char.charCodeAt(0) - ASCII_0 + 1;
|
||||
}
|
||||
|
||||
function num2char(num: integer) {
|
||||
let inner = num;
|
||||
|
||||
while (inner < 1) {
|
||||
inner = inner + 10;
|
||||
}
|
||||
|
||||
// 48 is ASCII 0
|
||||
return String.fromCharCode(((inner - 1) % 10) + ASCII_0);
|
||||
}
|
||||
|
||||
/**
|
||||
* A modified version of the [Solitaire cipher](https://en.wikipedia.org/wiki/Solitaire_(cipher)),
|
||||
* used for encrypting and decrypting access codes.
|
||||
*/
|
||||
export class Solitaire {
|
||||
private deck: Array<integer>;
|
||||
private readonly deckSize: integer;
|
||||
readonly jokerA: integer;
|
||||
readonly jokerB: integer;
|
||||
|
||||
constructor(deckSize: integer, jokerA?: integer, jokerB?: integer) {
|
||||
if (jokerA && jokerA > deckSize) {
|
||||
throw new Error(
|
||||
"Invalid jokerA. jokerA must not be larger than the largest card on deck."
|
||||
);
|
||||
}
|
||||
|
||||
if (jokerB && jokerB > deckSize) {
|
||||
throw new Error(
|
||||
"Invalid jokerB. jokerB must not be larger than the largest card on deck."
|
||||
);
|
||||
}
|
||||
|
||||
this.jokerA = jokerA ?? deckSize - 1;
|
||||
this.jokerB = jokerB ?? deckSize;
|
||||
this.deck = [...Array(deckSize).keys()].map((v) => v + 1);
|
||||
this.deckSize = deckSize;
|
||||
}
|
||||
|
||||
static encrypt(src: string, key: string) {
|
||||
const deck = new Solitaire(22, 21, 22);
|
||||
|
||||
for (const char of key) {
|
||||
deck.deckHash();
|
||||
deck.cutDeck(char2num(char));
|
||||
}
|
||||
|
||||
let output = "";
|
||||
|
||||
for (const char of src) {
|
||||
deck.deckHash();
|
||||
|
||||
const p = deck.getTopCardNumber() ?? 0;
|
||||
|
||||
output = output + num2char(char2num(char) + p);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static decrypt(src: string, key: string) {
|
||||
const deck = new Solitaire(22, 21, 22);
|
||||
|
||||
for (const char of key) {
|
||||
deck.deckHash();
|
||||
deck.cutDeck(char2num(char));
|
||||
}
|
||||
|
||||
let output = "";
|
||||
|
||||
for (const char of src) {
|
||||
deck.deckHash();
|
||||
|
||||
const p = deck.getTopCardNumber() ?? 0;
|
||||
|
||||
output = output + num2char(char2num(char) - p);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private moveCardDown(card: integer) {
|
||||
const cardLocation = this.deck.indexOf(card);
|
||||
|
||||
if (cardLocation === -1) {
|
||||
throw new Error("Card was not in the deck.");
|
||||
}
|
||||
|
||||
if (cardLocation < this.deckSize - 1) {
|
||||
const nextCard = this.deck[cardLocation + 1];
|
||||
|
||||
if (!nextCard) {
|
||||
throw new Error("Next card is undefined. This should not be possible.");
|
||||
}
|
||||
|
||||
this.deck[cardLocation] = nextCard;
|
||||
this.deck[cardLocation + 1] = card;
|
||||
} else {
|
||||
for (let i = this.deckSize - 1; i > 1; i--) {
|
||||
const prevCard = this.deck[i - 1];
|
||||
|
||||
if (!prevCard) {
|
||||
throw new Error("Previous card is undefined. This should not be possible.");
|
||||
}
|
||||
|
||||
this.deck[i] = prevCard;
|
||||
}
|
||||
|
||||
this.deck[1] = card;
|
||||
}
|
||||
}
|
||||
|
||||
private cutDeck(point: integer) {
|
||||
const tmpDeck = this.deck.slice(0);
|
||||
|
||||
let idx = 0;
|
||||
|
||||
for (let i = point; i < this.deckSize - 1; i++) {
|
||||
const item = tmpDeck[i];
|
||||
|
||||
if (!item) {
|
||||
throw new Error("not happening.");
|
||||
}
|
||||
|
||||
this.deck[idx] = item;
|
||||
idx++;
|
||||
}
|
||||
|
||||
for (let i = 0; i < point; i++) {
|
||||
const item = tmpDeck[i];
|
||||
|
||||
if (!item) {
|
||||
throw new Error("not happening.");
|
||||
}
|
||||
|
||||
this.deck[idx] = item;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
private swapOutsideJoker() {
|
||||
let joker1 = -1;
|
||||
let joker1Value = -1;
|
||||
let joker2 = -1;
|
||||
let joker2Value = -1;
|
||||
|
||||
for (let i = 0; i < this.deckSize; i++) {
|
||||
const card = this.deck[i];
|
||||
|
||||
if (!card) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (card === this.jokerA || card === this.jokerB) {
|
||||
if (joker1 === -1) {
|
||||
joker1 = i;
|
||||
joker1Value = card;
|
||||
} else {
|
||||
joker2 = i;
|
||||
joker2Value = card;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.deck = [
|
||||
...this.deck.slice(joker2 + 1),
|
||||
joker1Value,
|
||||
...this.deck.slice(joker1 + 1, joker2),
|
||||
joker2Value,
|
||||
...this.deck.slice(0, joker1),
|
||||
];
|
||||
}
|
||||
|
||||
private cutByBottomCard() {
|
||||
const bottom = this.deck[this.deckSize - 1];
|
||||
|
||||
if (!bottom) {
|
||||
throw new Error("bottom card is undefined (not possbile)");
|
||||
}
|
||||
|
||||
this.cutDeck(bottom === this.jokerB ? this.jokerA : bottom);
|
||||
}
|
||||
|
||||
private getTopCardNumber() {
|
||||
const top = this.deck[0];
|
||||
|
||||
if (!top) {
|
||||
throw new Error("top card is undefined (not possbile)");
|
||||
}
|
||||
|
||||
return this.deck[top === this.jokerB ? this.jokerA : top];
|
||||
}
|
||||
|
||||
private deckHash() {
|
||||
let p = -1;
|
||||
|
||||
do {
|
||||
this.moveCardDown(this.jokerA);
|
||||
this.moveCardDown(this.jokerB);
|
||||
this.moveCardDown(this.jokerB);
|
||||
this.swapOutsideJoker();
|
||||
this.cutByBottomCard();
|
||||
|
||||
p = this.getTopCardNumber() ?? -1;
|
||||
} while (p === this.jokerA || p === this.jokerB);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
const FELICA_MOBILE_OS_VERSIONS = [0x06, 0x07, 0x10, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18] as const;
|
||||
const FELICA_CARD_OS_VERSIONS = [0x20, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7] as const;
|
||||
|
||||
export function IsSupportedFelicaMobile(
|
||||
osVer: number
|
||||
): osVer is typeof FELICA_MOBILE_OS_VERSIONS[number] {
|
||||
return (FELICA_MOBILE_OS_VERSIONS as ReadonlyArray<number>).includes(osVer);
|
||||
}
|
||||
|
||||
export function IsSupportedFelicaCard(
|
||||
osVer: number
|
||||
): osVer is typeof FELICA_CARD_OS_VERSIONS[number] {
|
||||
return (FELICA_CARD_OS_VERSIONS as ReadonlyArray<number>).includes(osVer);
|
||||
}
|
||||
|
||||
export function IsSupportedFelica(
|
||||
osVer: number
|
||||
): osVer is typeof FELICA_CARD_OS_VERSIONS[number] | typeof FELICA_MOBILE_OS_VERSIONS[number] {
|
||||
return IsSupportedFelicaMobile(osVer) || IsSupportedFelicaCard(osVer);
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
export const enum ResultCodes {
|
||||
UNKNOWN_ERROR = 0,
|
||||
SUCCESS = 1,
|
||||
INVALID_AIME_ID = 2,
|
||||
ID_ALREADY_REGISTERED = 3,
|
||||
BAN_SYSTEM_AND_USER_LOCK = 4,
|
||||
BAN_SYSTEM_LOCK = 5,
|
||||
BAN_USER_LOCK = 6,
|
||||
BAN_GENERIC_LOCK = 7,
|
||||
SYSTEM_AND_USER_LOCK = 8,
|
||||
SYSTEM_LOCK = 9,
|
||||
USER_LOCK = 10,
|
||||
}
|
||||
|
||||
export const enum CommandId {
|
||||
FELICA_LOOKUP_REQUEST = 1,
|
||||
FELICA_REGISTER_REQUEST = 2,
|
||||
FELICA_LOOKUP_RESPONSE = 3,
|
||||
|
||||
AIME_ACCOUNT_REQUEST = 4,
|
||||
REGISTER_AIME_ACCOUNT_REQUEST = 5,
|
||||
AIME_ACCOUNT_RESPONSE = 6,
|
||||
|
||||
STATUS_LOG_REQUEST = 7,
|
||||
STATUS_LOG_RESPONSE = 8,
|
||||
|
||||
AIME_LOG_REQUEST = 9,
|
||||
AIME_LOG_RESPONSE = 10,
|
||||
|
||||
CAMPAIGN_INFO_REQUEST = 11,
|
||||
CAMPAIGN_INFO_RESPONSE = 12,
|
||||
|
||||
CAMPAIGN_CLEAR_INFO_REQUEST = 13,
|
||||
CAMPAIGN_CLEAR_INFO_RESPONSE = 14,
|
||||
|
||||
EXTENDED_ACCOUNT_REQUEST = 15,
|
||||
EXTENDED_ACCOUNT_RESPONSE = 16,
|
||||
|
||||
EXTENDED_FELICA_ACCOUNT_REQUEST = 17,
|
||||
EXTENDED_FELICA_ACCOUNT_RESPONSE = 18,
|
||||
|
||||
EXTENDED_AIME_LOG_REQUEST = 19,
|
||||
EXTENDED_AIME_LOG_RESPONSE = 20,
|
||||
|
||||
SERVICE_HEALTH_REQUEST = 100,
|
||||
SERVICE_HEALTH_RESPONSE = 101,
|
||||
|
||||
CLIENT_END_REQUEST = 102,
|
||||
}
|
||||
|
||||
export enum LogStatus {
|
||||
NONE = 0,
|
||||
GAME_START = 1,
|
||||
GAME_CONTINUE = 2,
|
||||
GAME_END = 3,
|
||||
OTHERS = 4,
|
||||
}
|
||||
|
||||
export const enum PortalRegistration {
|
||||
UNREGISTERED = 0,
|
||||
REGISTERED_ON_PORTAL = 1,
|
||||
LINKED_TO_ID = 2,
|
||||
}
|
||||
|
||||
export const enum CompanyCodes {
|
||||
SEGA = 1,
|
||||
BANDAI_NAMCO = 2,
|
||||
KONAMI = 3,
|
||||
TAITO = 4,
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// THIS IMPORT **MUST** GO HERE. DO NOT MOVE IT. IT MUST OCCUR BEFORE ANYTHING HAPPENS WITH EXPRESS
|
||||
// BUT AFTER EXPRESS IS IMPORTED.
|
||||
/* eslint-disable import/order */
|
||||
import express from "express";
|
||||
import "express-async-errors";
|
||||
|
||||
// eslint-disable-next-line import/order
|
||||
import mainRouter from "./router/router";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { DFIRequestResponse } from "./middleware/dfi";
|
||||
import type { Express } from "express";
|
||||
import { RequestLoggerMiddleware } from "./middleware/request-logger";
|
||||
import { IsRecord } from "utils/misc";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const app: Express = express();
|
||||
|
||||
// Pass the IP of the user up our increasingly insane chain of nginx/docker nonsense
|
||||
app.set("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
|
||||
|
||||
// we don't allow nesting in query strings.
|
||||
app.set("query parser", "simple");
|
||||
|
||||
// taken from https://nodejs.org/api/process.html#process_event_unhandledrejection
|
||||
// to avoid future deprecation.
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
// @ts-expect-error reason is an error, and the logger can handle errors
|
||||
// it just refuses.
|
||||
logger.error(reason, { promise });
|
||||
});
|
||||
|
||||
// ALL.Net sends a form body that is sometimes deflated and base64-encoded.
|
||||
// This also does some special handling to prevent keys/values from being
|
||||
// URL-escaped, which ALL.Net doesn't do. Pray they don't need an & or a =.
|
||||
app.use(DFIRequestResponse);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.method !== "GET" && (typeof req.body !== "object" || req.body === null)) {
|
||||
req.body = {};
|
||||
}
|
||||
|
||||
req.safeBody = req.body as Record<string, unknown>;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(RequestLoggerMiddleware);
|
||||
|
||||
app.use("/", mainRouter);
|
||||
|
||||
const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, _next) => {
|
||||
logger.info(`MAIN_ERR_HANDLER hit by request.`, { url: req.originalUrl });
|
||||
|
||||
const unknownErr = err as unknown;
|
||||
|
||||
if (IsRecord(unknownErr) && unknownErr.type === "entity.too.large") {
|
||||
return res.status(413).json({
|
||||
success: false,
|
||||
description: "Your request body was too large. The limit is 4MB.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.error("Fatal error propagated to server root? ", {
|
||||
err: unknownErr,
|
||||
url: req.originalUrl,
|
||||
});
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
description: "A fatal internal server error has occured.",
|
||||
});
|
||||
};
|
||||
|
||||
app.use(MAIN_ERR_HANDLER);
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,64 @@
|
|||
import iconv from "iconv-lite";
|
||||
import getRawBody from "raw-body";
|
||||
import { promisify } from "util";
|
||||
import { inflate } from "zlib";
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
const inflateAsync = promisify(inflate);
|
||||
|
||||
export const DFIRequestResponse: RequestHandler = async (req, res, next) => {
|
||||
if (Number(req.headers["content-length"] ?? 0) === 0) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const isUsingDfi = req.headers.pragma?.toUpperCase() === "DFI";
|
||||
|
||||
const rawBody = await getRawBody(req, { encoding: "utf-8" });
|
||||
|
||||
let body: string;
|
||||
|
||||
if (isUsingDfi) {
|
||||
const compressedBuffer = Buffer.from(rawBody, "base64");
|
||||
const buffer = await inflateAsync(compressedBuffer);
|
||||
|
||||
body = buffer.toString("utf-8").trim();
|
||||
} else {
|
||||
body = rawBody.trim();
|
||||
}
|
||||
|
||||
// Keys and values are not URL escaped.
|
||||
// This should be fine. I think.
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
req.body = Object.fromEntries(body.split("&").map((s) => s.split("=")));
|
||||
|
||||
const originalSend = res.send;
|
||||
|
||||
res.send = (params) => {
|
||||
const body =
|
||||
typeof params === "object"
|
||||
? `${Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("&")}\n`
|
||||
: params;
|
||||
|
||||
const encoding = req.body.encode ?? "EUC-JP";
|
||||
const encodedBody = iconv.encode(body, encoding);
|
||||
|
||||
res.header("Content-Type", `text/plain; charset=${encoding}`);
|
||||
|
||||
// TODO: I don't know what black magic SEGA did, but I have been woefully
|
||||
// unable to DFI-encode my responses...
|
||||
return originalSend.apply(res, [encodedBody]);
|
||||
|
||||
// if (req.headers.pragma?.toUpperCase() !== "DFI") {
|
||||
// return originalSend.apply(res, [encodedBody]);
|
||||
// }
|
||||
|
||||
// res.header("Pragma", "DFI");
|
||||
|
||||
// return originalSend.apply(res, [deflateSync(encodedBody).toString("base64")]);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
import CreateLogCtx from "lib/logger/logger";
|
||||
import type { RequestHandler, Response } from "express";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
// https://stackoverflow.com/a/64546368/11885828
|
||||
const ResSendInterceptor = (res: Response, send: Response["send"]) => (content: unknown) => {
|
||||
// @ts-expect-error general monkeypatching error
|
||||
res.contentBody = content;
|
||||
res.send = send;
|
||||
res.send(content);
|
||||
};
|
||||
|
||||
export const RequestLoggerMiddleware: RequestHandler = (req, res, next) => {
|
||||
logger.debug(`Received request ${req.method} ${req.originalUrl}.`, {
|
||||
query: req.query,
|
||||
body: req.body,
|
||||
});
|
||||
|
||||
// @ts-expect-error we're doing some wacky monkey patching
|
||||
res.send = ResSendInterceptor(res, res.send);
|
||||
|
||||
res.on("finish", () => {
|
||||
const contents = {
|
||||
// @ts-expect-error we're doing some monkey patching - contentBody is what we're returning.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
body: res.contentBody,
|
||||
statusCode: res.statusCode,
|
||||
requestQuery: req.query,
|
||||
requestBody: req.body,
|
||||
fromIp: req.ip,
|
||||
};
|
||||
|
||||
// special overrides
|
||||
// This stuff is spam, so we'll just not log it.
|
||||
if (res.statusCode === 429) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.statusCode < 400 || res.statusCode === 404) {
|
||||
logger.verbose(
|
||||
`(${req.method} ${req.originalUrl}) Returned ${res.statusCode}.`,
|
||||
contents
|
||||
);
|
||||
} else if (res.statusCode < 500) {
|
||||
logger.info(`(${req.method} ${req.originalUrl}) Returned ${res.statusCode}.`, contents);
|
||||
} else {
|
||||
logger.error(
|
||||
`(${req.method} ${req.originalUrl}) Returned ${res.statusCode}.`,
|
||||
contents
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import sysServletRouter from "./sys/servlet/router";
|
||||
import { Router } from "express";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.all("/naomitest.html", (_, res) => {
|
||||
return res.status(200).send("naomi ok").contentType("text/html");
|
||||
});
|
||||
|
||||
router.use("/sys/servlet", sysServletRouter);
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,13 @@
|
|||
import { Router } from "express";
|
||||
import iconv from "iconv-lite";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.all("/", (_, res) => {
|
||||
return res
|
||||
.status(200)
|
||||
.contentType("text/plain; charset=EUC-JP")
|
||||
.send(iconv.encode("OK", "EUC-JP"));
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,25 @@
|
|||
import { Router } from "express";
|
||||
import {
|
||||
DownloadOrderRequestSchema,
|
||||
DownloadOrderStatus,
|
||||
} from "servers/allnet/types/download-order";
|
||||
import type { DownloadOrderResponse } from "servers/allnet/types/download-order";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
const parseResult = DownloadOrderRequestSchema.safeParse(req.safeBody);
|
||||
|
||||
if (!parseResult.success) {
|
||||
return res.status(403).send("");
|
||||
}
|
||||
|
||||
// TODO: Allow network delivery.
|
||||
const response = {
|
||||
stat: DownloadOrderStatus.FAILURE,
|
||||
} satisfies DownloadOrderResponse;
|
||||
|
||||
return res.status(200).send(response);
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,226 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { Router } from "express";
|
||||
import { db } from "external/db/db";
|
||||
import { arcade, machine } from "external/db/schemas";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Config } from "lib/setup/config";
|
||||
import { DateTime } from "luxon";
|
||||
import {
|
||||
PowerOnRequestChinaSchema,
|
||||
PowerOnRequestV1Schema,
|
||||
PowerOnRequestV2Schema,
|
||||
PowerOnRequestV3Schema,
|
||||
PowerOnRequestV5Schema,
|
||||
PowerOnRequestV6Schema,
|
||||
PowerOnStat,
|
||||
} from "servers/allnet/types/power-on";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import type {
|
||||
PowerOnResponseChina,
|
||||
PowerOnResponseV1,
|
||||
PowerOnResponseV2,
|
||||
PowerOnResponseV3,
|
||||
PowerOnResponseV5,
|
||||
} from "servers/allnet/types/power-on";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
if (!req.ip) {
|
||||
logger.error("Request does not have an IP associated with it?");
|
||||
return res.status(500).send();
|
||||
}
|
||||
|
||||
const formatVer = req.safeBody.format_ver ?? "1";
|
||||
|
||||
let parseResult;
|
||||
|
||||
// TODO: ALL.Net China
|
||||
if (formatVer === "1") {
|
||||
parseResult = PowerOnRequestV1Schema.safeParse(req.safeBody);
|
||||
} else if (formatVer === "2") {
|
||||
parseResult = PowerOnRequestV2Schema.safeParse(req.safeBody);
|
||||
} else if (formatVer === "3") {
|
||||
parseResult =
|
||||
"game_ver" in req.safeBody
|
||||
? PowerOnRequestChinaSchema.safeParse(req.safeBody)
|
||||
: PowerOnRequestV3Schema.safeParse(req.safeBody);
|
||||
} else if (formatVer === "5") {
|
||||
parseResult = PowerOnRequestV5Schema.safeParse(req.safeBody);
|
||||
} else if (formatVer === "6") {
|
||||
parseResult = PowerOnRequestV6Schema.safeParse(req.safeBody);
|
||||
} else {
|
||||
logger.error("Received PowerOn request of unknown version.", { formatVer });
|
||||
return res.status(400).send("");
|
||||
}
|
||||
|
||||
if (!parseResult.success) {
|
||||
logger.error(`Received invalid PowerOn request: ${fromZodError(parseResult.error)}`, {
|
||||
formatVer,
|
||||
});
|
||||
return res.status(400).send("");
|
||||
}
|
||||
|
||||
const request = parseResult.data;
|
||||
const gameId = "game_id" in request ? request.game_id : request.title_id;
|
||||
const serial = "serial" in request ? request.serial : request.machine;
|
||||
const titleVer =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
"ver" in request
|
||||
? request.ver
|
||||
: "game_ver" in request
|
||||
? request.game_ver
|
||||
: request.title_ver;
|
||||
|
||||
// We reject non-DFI requests late, so we can find the offending
|
||||
// "machine".
|
||||
if (req.headers.pragma?.toUpperCase() !== "DFI") {
|
||||
logger.error("Received non-DFI-encoded request.", {
|
||||
gameId,
|
||||
serial,
|
||||
ip: req.ip,
|
||||
});
|
||||
return res.status(400).send("");
|
||||
}
|
||||
|
||||
if (titleVer.length === 0) {
|
||||
logger.error("Received PowerOn request with empty title version.", {
|
||||
formatVer,
|
||||
titleVer,
|
||||
});
|
||||
return res.status(400).send("");
|
||||
}
|
||||
|
||||
// TODO: Implement store authentication + fetch arcade information
|
||||
// Reference implementation: https://sega.bsnk.me/allnet/auth/power-on
|
||||
// For now, we just check if there's a cab registered in the database.
|
||||
const cabAndStore = await db
|
||||
.select()
|
||||
.from(machine)
|
||||
.leftJoin(arcade, eq(arcade.id, machine.arcade_id))
|
||||
.where(eq(machine.serial, serial))
|
||||
.then((r) => r[0]);
|
||||
|
||||
if (!cabAndStore && !Config.ALLNET_CONFIG.ALLOW_UNREGISTERED_SERIALS) {
|
||||
logger.error("Unregistered serial attempted ALL.Net authentication.", {
|
||||
gameId,
|
||||
serial,
|
||||
ip: req.ip,
|
||||
});
|
||||
return res.status(400).send({ stat: PowerOnStat.BOARD_ERROR });
|
||||
}
|
||||
|
||||
// TODO: Verify that title exists and is enabled.
|
||||
const serverTime = DateTime.now().setZone("Asia/Tokyo");
|
||||
const baseResponse = {
|
||||
// Same thing
|
||||
stat: 1,
|
||||
|
||||
place_id: (0x123).toString(16),
|
||||
// uri: `http://localhost:8080/${gameId}/${titleVer.replace(/\./u, "")}/`,
|
||||
uri: `http://localhost:8080/${titleVer.replace(/\./u, "")}/`,
|
||||
host: "localhost:8080",
|
||||
name: cabAndStore?.allnet_arcade?.name ?? Config.NAME,
|
||||
nickname: cabAndStore?.allnet_arcade?.nickname ?? "kozukata-toa",
|
||||
setting: 1,
|
||||
region0: cabAndStore?.allnet_arcade?.regionId ?? 1,
|
||||
region_name0: cabAndStore?.allnet_arcade?.regionName0 ?? "W",
|
||||
region_name1: cabAndStore?.allnet_arcade?.regionName1 ?? "",
|
||||
region_name2: cabAndStore?.allnet_arcade?.regionName2 ?? "",
|
||||
region_name3: cabAndStore?.allnet_arcade?.regionName3 ?? "",
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
if (request.format_ver === "1") {
|
||||
response = {
|
||||
...baseResponse,
|
||||
year: serverTime.year,
|
||||
month: serverTime.month,
|
||||
day: serverTime.day,
|
||||
hour: serverTime.hour,
|
||||
minute: serverTime.minute,
|
||||
second: serverTime.second,
|
||||
} satisfies PowerOnResponseV1;
|
||||
} else if (request.format_ver === "2") {
|
||||
response = {
|
||||
...baseResponse,
|
||||
year: serverTime.year,
|
||||
month: serverTime.month,
|
||||
day: serverTime.day,
|
||||
hour: serverTime.hour,
|
||||
minute: serverTime.minute,
|
||||
second: serverTime.second,
|
||||
country: "JPN",
|
||||
timezone: "+09:00",
|
||||
res_class: "PowerOnResponseVer2",
|
||||
} satisfies PowerOnResponseV2;
|
||||
} else if (request.format_ver === "3" && "ver" in request) {
|
||||
response = {
|
||||
...baseResponse,
|
||||
res_ver: 3,
|
||||
allnet_id: 456,
|
||||
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||
country: "JPN",
|
||||
client_timezone: "+0900",
|
||||
token: request.token ?? "null",
|
||||
} satisfies PowerOnResponseV3;
|
||||
} else if (request.format_ver === "3" && "game_ver" in request) {
|
||||
response = {
|
||||
result: baseResponse.stat,
|
||||
place_id: baseResponse.place_id,
|
||||
uri1: baseResponse.uri,
|
||||
uri2: baseResponse.uri,
|
||||
name: baseResponse.name,
|
||||
nickname: baseResponse.nickname,
|
||||
setting: baseResponse.setting,
|
||||
region0: baseResponse.region0,
|
||||
region_name0: baseResponse.region_name0,
|
||||
region_name1: baseResponse.region_name1,
|
||||
region_name2: baseResponse.region_name2,
|
||||
region_name3: baseResponse.region_name3,
|
||||
country: "CHN",
|
||||
client_timezone: "+800",
|
||||
token: request.token ?? "null",
|
||||
res_ver: 3,
|
||||
allnet_id: 456,
|
||||
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||
} satisfies PowerOnResponseChina;
|
||||
} else if (request.format_ver === "5") {
|
||||
response = {
|
||||
result: baseResponse.stat,
|
||||
place_id: baseResponse.place_id,
|
||||
title_uri: baseResponse.uri,
|
||||
title_host: baseResponse.host,
|
||||
name: baseResponse.name,
|
||||
nickname: baseResponse.nickname,
|
||||
setting: baseResponse.setting,
|
||||
region0: baseResponse.region0,
|
||||
region_name0: baseResponse.region_name0,
|
||||
region_name1: baseResponse.region_name1,
|
||||
region_name2: baseResponse.region_name2,
|
||||
region_name3: baseResponse.region_name3,
|
||||
res_ver: 5,
|
||||
allnet_id: 456,
|
||||
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
||||
country: "JPN",
|
||||
client_timezone: "+0900",
|
||||
token: request.token ?? "null",
|
||||
} satisfies PowerOnResponseV5;
|
||||
} else {
|
||||
logger.error("Received auth20 PowerOn request, which is currently unsupported.", {
|
||||
gameId,
|
||||
serial,
|
||||
ip: req.ip,
|
||||
});
|
||||
return res.status(403).send({ stat: 0 });
|
||||
}
|
||||
|
||||
logger.info("Authenticated machine with ALL.Net.", { serial, gameId, ip: req.ip });
|
||||
|
||||
return res.status(200).send(response);
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,12 @@
|
|||
import aliveRouter from "./Alive/router";
|
||||
import downloadOrderRouter from "./DownloadOrder/router";
|
||||
import powerOnRouter from "./PowerOn/router";
|
||||
import { Router } from "express";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.use("/Alive", aliveRouter);
|
||||
router.use("/DownloadOrder", downloadOrderRouter);
|
||||
router.use("/PowerOn", powerOnRouter);
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,22 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const DownloadOrderRequestSchema = z.object({
|
||||
game_id: z.string().max(5),
|
||||
ver: z.string().max(5),
|
||||
serial: z.string().max(11),
|
||||
ip: z.string().ip().optional(),
|
||||
encode: z.enum(["Shift_JIS", "EUC-JP", "UTF-8"]).default("EUC-JP"),
|
||||
});
|
||||
|
||||
export type DownloadOrderRequest = z.infer<typeof DownloadOrderRequestSchema>;
|
||||
|
||||
export const enum DownloadOrderStatus {
|
||||
FAILURE = 0,
|
||||
SUCCESS = 1,
|
||||
}
|
||||
|
||||
export interface DownloadOrderResponse {
|
||||
stat: DownloadOrderStatus;
|
||||
serial?: string;
|
||||
uri?: string;
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
import { z } from "zod";
|
||||
import type { integer } from "types/misc";
|
||||
|
||||
export const PowerOnRequestV1Schema = z.object({
|
||||
game_id: z.string().max(5),
|
||||
ver: z.string().max(5),
|
||||
serial: z.string().max(11),
|
||||
ip: z.string().ip(),
|
||||
firm_ver: z.coerce.number().optional(),
|
||||
boot_ver: z.coerce.number().optional(),
|
||||
encode: z.enum(["Shift_JIS", "EUC-JP", "UTF-8"]).default("EUC-JP"),
|
||||
format_ver: z.literal("1"),
|
||||
hops: z.coerce.number().default(-1),
|
||||
});
|
||||
|
||||
export type PowerOnRequestV1 = z.infer<typeof PowerOnRequestV1Schema>;
|
||||
|
||||
export const PowerOnRequestV2Schema = PowerOnRequestV1Schema.extend({
|
||||
format_ver: z.literal("2"),
|
||||
});
|
||||
|
||||
export type PowerOnRequestV2 = z.infer<typeof PowerOnRequestV2Schema>;
|
||||
|
||||
export const PowerOnRequestV3Schema = PowerOnRequestV2Schema.extend({
|
||||
format_ver: z.literal("3"),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
export type PowerOnRequestV3 = z.infer<typeof PowerOnRequestV3Schema>;
|
||||
|
||||
export const PowerOnRequestV5Schema = z.object({
|
||||
title_id: z.string().max(5),
|
||||
title_ver: z.string().max(5),
|
||||
machine: z.string().max(11),
|
||||
firm_ver: z.coerce.number().optional(),
|
||||
boot_ver: z.coerce.number().optional(),
|
||||
encode: z.enum(["Shift_JIS", "EUC-JP", "UTF-8"]).default("EUC-JP"),
|
||||
format_ver: z.literal("5"),
|
||||
hops: z.coerce.number().default(-1),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
export type PowerOnRequestV5 = z.infer<typeof PowerOnRequestV5Schema>;
|
||||
|
||||
export const PowerOnRequestChinaSchema = z.object({
|
||||
game_id: z.string().max(5),
|
||||
game_ver: z.string().max(5),
|
||||
machine: z.string().max(11),
|
||||
server: z.string(),
|
||||
firm_ver: z.coerce.number().optional(),
|
||||
boot_ver: z.coerce.number().optional(),
|
||||
encode: z.enum(["Shift_JIS", "EUC-JP", "UTF-8"]).default("EUC-JP"),
|
||||
format_ver: z.literal("3"),
|
||||
hops: z.coerce.number().default(-1),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
export type PowerOnRequestChina = z.infer<typeof PowerOnRequestChinaSchema>;
|
||||
|
||||
export const PowerOnRequestV6Schema = PowerOnRequestV3Schema.extend({
|
||||
format_ver: z.literal("6"),
|
||||
auth_data: z.string().max(1535),
|
||||
});
|
||||
|
||||
export type PowerOnRequestV6 = z.infer<typeof PowerOnRequestV6Schema>;
|
||||
|
||||
export const enum PowerOnStat {
|
||||
SUCCESS = 1,
|
||||
GAME_ERROR = -1,
|
||||
BOARD_ERROR = -2,
|
||||
LOCATION_ERROR = -3,
|
||||
}
|
||||
|
||||
export interface PowerOnResponseV1 {
|
||||
/**
|
||||
* ALL.Net authentication status
|
||||
*/
|
||||
stat: PowerOnStat;
|
||||
|
||||
/**
|
||||
* The arcade's place ID, encoded as an uppercase hexadecimal string.
|
||||
*/
|
||||
place_id: string;
|
||||
|
||||
/**
|
||||
* Title server URI. Will be present, but empty, if authentication
|
||||
* is not successful.
|
||||
*/
|
||||
uri: string;
|
||||
|
||||
/**
|
||||
* Title server hostname. Will be present, but empty, if authentication
|
||||
* is not successful.
|
||||
*
|
||||
* @note The hostname (if present) in URI is only used for name resolution.
|
||||
* This value is passed to the title server in the `Host` header, and can be
|
||||
* utilized as an extra authentication step.
|
||||
*/
|
||||
host: string;
|
||||
|
||||
/**
|
||||
* ALL.Net location name
|
||||
*
|
||||
* @note URL-encoded UTF-8.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* ALL.Net location nickname
|
||||
*
|
||||
* @note URL-encoded UTF-8.
|
||||
*/
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* Server time's year
|
||||
*/
|
||||
year: integer;
|
||||
|
||||
/**
|
||||
* Server time's month
|
||||
*/
|
||||
month: integer;
|
||||
|
||||
/**
|
||||
* Server time's day
|
||||
*/
|
||||
day: integer;
|
||||
|
||||
/**
|
||||
* Server time's hour
|
||||
*/
|
||||
hour: integer;
|
||||
|
||||
/**
|
||||
* Server time's minute
|
||||
*/
|
||||
minute: integer;
|
||||
|
||||
/**
|
||||
* Server time's second
|
||||
*/
|
||||
second: integer;
|
||||
|
||||
/**
|
||||
* Game-specific setting.
|
||||
*
|
||||
* @note In practice this appears to be used as a "network allowed" flag.
|
||||
* If ever unsure, specify this field as `1`.
|
||||
*/
|
||||
setting: integer;
|
||||
|
||||
/**
|
||||
* Region code
|
||||
*/
|
||||
region0: integer;
|
||||
|
||||
/**
|
||||
* Region 1
|
||||
*
|
||||
* @note URL-encoded UTF-8.
|
||||
*/
|
||||
region_name0: string;
|
||||
|
||||
/**
|
||||
* Region 2
|
||||
*
|
||||
* @note URL-encoded UTF-8.
|
||||
*/
|
||||
region_name1: string;
|
||||
|
||||
/**
|
||||
* Region 3
|
||||
*
|
||||
* @note URL-encoded UTF-8.
|
||||
*/
|
||||
region_name2: string;
|
||||
|
||||
/**
|
||||
* Region 4
|
||||
*
|
||||
* @note URL-encoded UTF-8.
|
||||
*/
|
||||
region_name3: string;
|
||||
}
|
||||
|
||||
export interface PowerOnResponseV2 extends PowerOnResponseV1 {
|
||||
/**
|
||||
* Country code
|
||||
*/
|
||||
country: string;
|
||||
|
||||
timezone: "+09:00";
|
||||
|
||||
res_class: "PowerOnResponseVer2";
|
||||
}
|
||||
|
||||
export interface PowerOnResponseV3
|
||||
extends Omit<
|
||||
PowerOnResponseV2,
|
||||
"day" | "hour" | "minute" | "month" | "res_class" | "second" | "timezone" | "year"
|
||||
> {
|
||||
res_ver: 3;
|
||||
|
||||
/**
|
||||
* ALL.Net ID
|
||||
*/
|
||||
allnet_id: integer;
|
||||
|
||||
/**
|
||||
* Authentication time. Format must be `yyyy-MM-dd'T'HH:mm:ss'Z'`.
|
||||
*/
|
||||
utc_time: string;
|
||||
|
||||
/**
|
||||
* @example `+0900`
|
||||
*/
|
||||
client_timezone: string;
|
||||
|
||||
/**
|
||||
* Echoes the request token. The literal `null` will be used if token is not
|
||||
* present in request.
|
||||
*/
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface PowerOnResponseV5
|
||||
extends Omit<PowerOnResponseV3, "host" | "res_ver" | "stat" | "uri"> {
|
||||
res_ver: 5;
|
||||
|
||||
/**
|
||||
* ALL.Net authentication status
|
||||
*/
|
||||
result: PowerOnStat;
|
||||
|
||||
/**
|
||||
* Title server URI. Will be present, but empty, if authentication
|
||||
* is not successful.
|
||||
*/
|
||||
title_uri: string;
|
||||
|
||||
/**
|
||||
* Title server hostname. Will be present, but empty, if authentication
|
||||
* is not successful.
|
||||
*
|
||||
* @note The hostname (if present) in URI is only used for name resolution.
|
||||
* This value is passed to the title server in the `Host` header, and can be
|
||||
* utilized as an extra authentication step.
|
||||
*/
|
||||
title_host: string;
|
||||
}
|
||||
|
||||
export interface PowerOnResponseChina
|
||||
extends Omit<PowerOnResponseV3, "client_timezone" | "country" | "host" | "stat" | "uri"> {
|
||||
/**
|
||||
* ALL.Net authentication status
|
||||
*/
|
||||
result: PowerOnStat;
|
||||
|
||||
/**
|
||||
* Title server URI 1
|
||||
*/
|
||||
uri1: string;
|
||||
|
||||
/**
|
||||
* Title server URI 2
|
||||
*/
|
||||
uri2: string;
|
||||
|
||||
country: "CHN";
|
||||
|
||||
client_timezone: "+800";
|
||||
}
|
||||
|
||||
export interface PowerOnResponseV6 {
|
||||
auth_data: string;
|
||||
packet_data: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default as allnetServer } from "./allnet";
|
||||
export { default as aimeDbServerFactory } from "./aimedb";
|
|
@ -0,0 +1 @@
|
|||
export type integer = number;
|
|
@ -0,0 +1,13 @@
|
|||
export function EscapeStringRegexp(string: string) {
|
||||
if (typeof string !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
|
||||
// Escape characters with special meaning either inside or outside character sets.
|
||||
// Use a simple backslash escape when it's always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns' stricter grammar.
|
||||
return string.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&").replace(/-/gu, "\\x2d");
|
||||
}
|
||||
|
||||
export function IsRecord(maybeRecord: unknown): maybeRecord is Record<string, unknown> {
|
||||
return typeof maybeRecord === "object" && maybeRecord !== null;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// This TSConfig file is used when pnpm build is ran. It omits out
|
||||
// unecessary files, such as anything involved in testing.
|
||||
// This is a separate TSConfig file because VSCode (by default) uses
|
||||
// the exclude list to find out what it should care about for typechecking
|
||||
// while writing code.
|
||||
// Since I still want my tests to take advantage of static typing, this is
|
||||
// a necessary hack.
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"src/test-utils",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
"incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
"tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": ["ES2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
"typeRoots": ["@types", "node_modules/@types"], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": false, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./js", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
"noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue