@kavtuai/guildgate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +399 -0
- package/README.tr.md +397 -0
- package/SECURITY.md +36 -0
- package/TEST_REPORT.md +50 -0
- package/bin/guildgate-doctor.mjs +91 -0
- package/bin/guildgate-writing-check.mjs +73 -0
- package/dist/action.d.ts +65 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +4 -0
- package/dist/action.js.map +1 -0
- package/dist/cache.d.ts +23 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +53 -0
- package/dist/cache.js.map +1 -0
- package/dist/crypto.d.ts +15 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +87 -0
- package/dist/crypto.js.map +1 -0
- package/dist/discord/authorizer.d.ts +41 -0
- package/dist/discord/authorizer.d.ts.map +1 -0
- package/dist/discord/authorizer.js +101 -0
- package/dist/discord/authorizer.js.map +1 -0
- package/dist/discord/oauth.d.ts +64 -0
- package/dist/discord/oauth.d.ts.map +1 -0
- package/dist/discord/oauth.js +226 -0
- package/dist/discord/oauth.js.map +1 -0
- package/dist/discord/permissions.d.ts +85 -0
- package/dist/discord/permissions.d.ts.map +1 -0
- package/dist/discord/permissions.js +99 -0
- package/dist/discord/permissions.js.map +1 -0
- package/dist/discord.d.ts +4 -0
- package/dist/discord.d.ts.map +1 -0
- package/dist/discord.js +4 -0
- package/dist/discord.js.map +1 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +57 -0
- package/dist/errors.js.map +1 -0
- package/dist/express.d.ts +3 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +3 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.d.ts +3 -0
- package/dist/fastify.d.ts.map +1 -0
- package/dist/fastify.js +3 -0
- package/dist/fastify.js.map +1 -0
- package/dist/http/express.d.ts +24 -0
- package/dist/http/express.d.ts.map +1 -0
- package/dist/http/express.js +60 -0
- package/dist/http/express.js.map +1 -0
- package/dist/http/fastify.d.ts +25 -0
- package/dist/http/fastify.d.ts.map +1 -0
- package/dist/http/fastify.js +57 -0
- package/dist/http/fastify.js.map +1 -0
- package/dist/i18n.d.ts +5 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +66 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel.d.ts +83 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +395 -0
- package/dist/kernel.js.map +1 -0
- package/dist/memory-store.d.ts +13 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +261 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/realtime/hub.d.ts +42 -0
- package/dist/realtime/hub.d.ts.map +1 -0
- package/dist/realtime/hub.js +152 -0
- package/dist/realtime/hub.js.map +1 -0
- package/dist/realtime/outbox.d.ts +12 -0
- package/dist/realtime/outbox.d.ts.map +1 -0
- package/dist/realtime/outbox.js +18 -0
- package/dist/realtime/outbox.js.map +1 -0
- package/dist/realtime.d.ts +3 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +3 -0
- package/dist/realtime.js.map +1 -0
- package/dist/redis.d.ts +49 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +233 -0
- package/dist/redis.js.map +1 -0
- package/dist/resilience.d.ts +23 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +85 -0
- package/dist/resilience.js.map +1 -0
- package/dist/security.d.ts +21 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +94 -0
- package/dist/security.js.map +1 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +97 -0
- package/dist/session.js.map +1 -0
- package/dist/stores.d.ts +73 -0
- package/dist/stores.d.ts.map +1 -0
- package/dist/stores.js +4 -0
- package/dist/stores.js.map +1 -0
- package/dist/testing.d.ts +32 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +60 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +183 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/docs/WRITING_STYLE.md +118 -0
- package/docs/architecture.md +83 -0
- package/docs/custom-stores.md +111 -0
- package/docs/owner-management.md +44 -0
- package/docs/research-notes.md +61 -0
- package/docs/setup.md +104 -0
- package/docs/threat-model.md +79 -0
- package/docs/tr/arastirma-notlari.md +41 -0
- package/docs/tr/guvenlik-bildirimi.md +7 -0
- package/docs/tr/kurulum.md +63 -0
- package/docs/tr/mimari.md +59 -0
- package/docs/tr/sahip-yonetimi.md +7 -0
- package/docs/tr/tehdit-modeli.md +39 -0
- package/docs/tr/veritabani-suruculeri.md +23 -0
- package/docs/tr/yazim-kilavuzu.md +116 -0
- package/docs/uml/action-sequence.mmd +39 -0
- package/docs/uml/components.mmd +84 -0
- package/docs/uml/deployment.mmd +19 -0
- package/docs/uml/oauth-sequence.mmd +30 -0
- package/docs/uml/realtime-sequence.mmd +28 -0
- package/package.json +90 -0
package/README.tr.md
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# GuildGate
|
|
2
|
+
|
|
3
|
+
[English documentation](./README.md)
|
|
4
|
+
|
|
5
|
+
GuildGate, Discord bot panellerinin sunucu tarafı için hazırlanmış bir TypeScript kütüphanesidir. Oturum kayıtlarını, Discord OAuth state doğrulamasını, CSRF ve origin kontrollerini, sunucu izinlerini, istek sınırlarını, idempotency kayıtlarını, kaynak kilitlerini, zaman aşımını, cache temizliğini, denetim kayıtlarını, outbox olaylarını ve yetkili gerçek zamanlı abonelikleri yönetir.
|
|
6
|
+
|
|
7
|
+
Arayüz üretmez ve veritabanınızı seçmez. Her depolama bölümü bir TypeScript arayüzüdür. PostgreSQL, MySQL, MongoDB, SQLite, Redis, mevcut ORM yapınız veya özel bir veri servisi kullanılabilir.
|
|
8
|
+
|
|
9
|
+
Paket: `@kavtuai/guildgate`
|
|
10
|
+
Sürüm: `0.1.0`
|
|
11
|
+
Çalışma ortamı: Node.js 22 veya üzeri
|
|
12
|
+
|
|
13
|
+
## Durum
|
|
14
|
+
|
|
15
|
+
Bu depo ilk açık kaynak sürüm adayıdır. TypeScript derlemesi ve paketteki testler geçmektedir. Discord OAuth, üretim Redis sunucusu ve üçüncü taraf veritabanı sürücüleri, yayına alınacak uygulamada ayrıca entegrasyon testinden geçirilmelidir.
|
|
16
|
+
|
|
17
|
+
Hiçbir kütüphane, uygulamanın güvenlik hatası içermediğini garanti edemez. GuildGate tekrar edilen güvenlik işlerini azaltır, üretimde tehlikeli ayarları reddeder ve politika yönetimini uygulama sahibine bırakır. Üretim ortamında kod incelemesi, bağımlılık kontrolü, izleme, yedekleme ve risk seviyesine göre dış güvenlik incelemesi yine gereklidir.
|
|
18
|
+
|
|
19
|
+
## Kurulum
|
|
20
|
+
|
|
21
|
+
Paket npm üzerinde yayımlandıktan sonra:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kavtuai/guildgate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Bu kaynak paketini yayımlamadan önce denemek için:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
npm test
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Pakette bulunanlar
|
|
35
|
+
|
|
36
|
+
- Sunucu tarafında tutulan, içeriği anlamsız oturum kimlikleri.
|
|
37
|
+
- Oturum süresi, boşta kalma süresi, kimlik yenileme, kullanıcı başına oturum sınırı ve iptal.
|
|
38
|
+
- Tek kullanımlık `state` kaydı ve tarayıcı nonce çerezi kullanan Discord authorization-code girişi.
|
|
39
|
+
- Uygulamanın verdiği anahtar takımıyla şifrelenen OAuth access ve refresh token kayıtları.
|
|
40
|
+
- Yazma isteklerinde tam origin kontrolü ve oturuma bağlı CSRF tokenı.
|
|
41
|
+
- Discord izinleri için `BigInt` işlemleri.
|
|
42
|
+
- Kullanıcı ve botun sunucu izinlerini ayrı ayrı kontrol etme.
|
|
43
|
+
- Panel okuma ve yazma işlemleri için denetimli `action()` API’si.
|
|
44
|
+
- İşlem bazlı limit, idempotency, kaynak kilidi, zaman aşımı, denetim kaydı, cache etiketi ve gerçek zamanlı olay desteği.
|
|
45
|
+
- Test ve yerel geliştirme için bellek sürücüleri.
|
|
46
|
+
- Kısa ömürlü ve dağıtık veriler için Redis sürücüleri.
|
|
47
|
+
- Runtime bağımlılığı eklemeyen Fastify ve Express bağlayıcıları.
|
|
48
|
+
- WebSocket bağlayıcıları için oturum kontrollü gerçek zamanlı merkez.
|
|
49
|
+
- Birbirine karışmayan, uygulama tarafından değiştirilebilen Türkçe ve İngilizce hata metinleri.
|
|
50
|
+
- Mermaid UML dosyaları, tehdit modeli, veri sürücüsü sözleşmeleri ve yayın iş akışları.
|
|
51
|
+
|
|
52
|
+
## Beş dakikalık yerel örnek
|
|
53
|
+
|
|
54
|
+
Bellek sürücüsü test ve tek süreçli yerel geliştirme içindir. Üretim veritabanı değildir.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
createGuildGate,
|
|
59
|
+
createMemoryStoreBundle,
|
|
60
|
+
} from "@kavtuai/guildgate";
|
|
61
|
+
|
|
62
|
+
const stores = createMemoryStoreBundle();
|
|
63
|
+
|
|
64
|
+
const gate = createGuildGate({
|
|
65
|
+
app: {
|
|
66
|
+
name: "Discord Bot Panelim",
|
|
67
|
+
environment: "development",
|
|
68
|
+
baseUrl: "http://localhost:3000",
|
|
69
|
+
},
|
|
70
|
+
owners: [process.env.BOT_OWNER_ID!],
|
|
71
|
+
locale: {
|
|
72
|
+
default: "tr",
|
|
73
|
+
messages: {
|
|
74
|
+
tr: { MAINTENANCE_MODE: "Bakım sırasında ayar değiştirilemez." },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
security: {
|
|
78
|
+
allowedOrigins: ["http://localhost:3000"],
|
|
79
|
+
csrfSecret: process.env.GUILDGATE_CSRF_SECRET!,
|
|
80
|
+
auditIpSalt: process.env.GUILDGATE_AUDIT_IP_SALT!,
|
|
81
|
+
session: {
|
|
82
|
+
ttlMs: 12 * 60 * 60_000,
|
|
83
|
+
idleTimeoutMs: 30 * 60_000,
|
|
84
|
+
rotateAfterMs: 15 * 60_000,
|
|
85
|
+
maximumSessionsPerUser: 5,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
stores,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Gizli değerleri kaynak koduna yazmayın. Rastgele değer üretmek için:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Denetimli panel işlemi
|
|
99
|
+
|
|
100
|
+
İşlemi koruyan kurallar aynı tanımda yer alır.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const ayarlariGuncelle = gate.action({
|
|
104
|
+
name: "guild.settings.update",
|
|
105
|
+
|
|
106
|
+
parse(value) {
|
|
107
|
+
const body = value as Record<string, unknown>;
|
|
108
|
+
if (typeof body.guildId !== "string") throw new Error("guildId gerekli");
|
|
109
|
+
if (typeof body.revision !== "number") throw new Error("revision gerekli");
|
|
110
|
+
return {
|
|
111
|
+
guildId: body.guildId,
|
|
112
|
+
revision: body.revision,
|
|
113
|
+
settings: body.settings,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
resource: (input) => ({ type: "guild", id: input.guildId }),
|
|
118
|
+
|
|
119
|
+
rateLimit: {
|
|
120
|
+
limit: 20,
|
|
121
|
+
windowMs: 60_000,
|
|
122
|
+
key: (context, input) => `${context.userId}:${input.guildId}`,
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
idempotency: {
|
|
126
|
+
ttlMs: 10 * 60_000,
|
|
127
|
+
scope: (context, input) => `${context.userId}:${input.guildId}`,
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
concurrency: {
|
|
131
|
+
key: (_context, input) => `guild-settings:${input.guildId}`,
|
|
132
|
+
ttlMs: 8_000,
|
|
133
|
+
waitMs: 500,
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
timeoutMs: 5_000,
|
|
137
|
+
|
|
138
|
+
authorize: async (context) => {
|
|
139
|
+
return context.userId
|
|
140
|
+
? { allowed: true }
|
|
141
|
+
: { allowed: false, code: "AUTHENTICATION_REQUIRED" };
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async execute(context, input) {
|
|
145
|
+
// Transaction ve revision kontrolü uygulamanın veritabanı katmanındadır.
|
|
146
|
+
return database.transaction(async (tx) => {
|
|
147
|
+
return tx.guildSettings.updateWithRevision({
|
|
148
|
+
guildId: input.guildId,
|
|
149
|
+
expectedRevision: input.revision,
|
|
150
|
+
settings: input.settings,
|
|
151
|
+
actorId: context.userId!,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
audit: {
|
|
157
|
+
changes: (result) => ({ revision: result.revision }),
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
cache: {
|
|
161
|
+
invalidateTags: (result) => [`guild:${result.guildId}`],
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
realtime: {
|
|
165
|
+
delivery: "outbox",
|
|
166
|
+
events: (result) => [{
|
|
167
|
+
event: "guild.settings.updated",
|
|
168
|
+
channel: `guild:${result.guildId}`,
|
|
169
|
+
data: { revision: result.revision },
|
|
170
|
+
}],
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Tarayıcıdan gelen yazma isteğinde oturum çerezi, CSRF tokenı ve idempotency anahtarı bulunmalıdır:
|
|
176
|
+
|
|
177
|
+
```http
|
|
178
|
+
PATCH /api/guilds/123/settings
|
|
179
|
+
Origin: https://panel.example.com
|
|
180
|
+
X-CSRF-Token: <oturuma-bagli-token>
|
|
181
|
+
Idempotency-Key: <rastgele-istek-kimligi>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Discord girişi
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { createTokenCipher } from "@kavtuai/guildgate";
|
|
188
|
+
import { createDiscordOAuth } from "@kavtuai/guildgate/discord";
|
|
189
|
+
|
|
190
|
+
const cipher = createTokenCipher({
|
|
191
|
+
activeKeyId: "2026-01",
|
|
192
|
+
keys: {
|
|
193
|
+
"2026-01": process.env.GUILDGATE_TOKEN_KEY_BASE64!,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const discord = createDiscordOAuth({
|
|
198
|
+
kernel: gate,
|
|
199
|
+
cipher,
|
|
200
|
+
config: {
|
|
201
|
+
clientId: process.env.DISCORD_CLIENT_ID!,
|
|
202
|
+
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
|
|
203
|
+
redirectUri: "https://panel.example.com/auth/discord/callback",
|
|
204
|
+
scopes: ["identify", "guilds"],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Girişi başlatma:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const login = await discord.beginLogin({ returnTo: "/dashboard", locale: "tr" });
|
|
213
|
+
response.header("set-cookie", login.stateCookie);
|
|
214
|
+
response.redirect(login.authorizationUrl);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Callback:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const result = await discord.completeLogin({
|
|
221
|
+
code: request.query.code,
|
|
222
|
+
state: request.query.state,
|
|
223
|
+
stateCookie: request.cookies["__Host-guildgate.oauth"],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
response.header("set-cookie", [result.setCookie, result.clearStateCookie]);
|
|
227
|
+
response.redirect(result.returnTo);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Callback yalnızca site içindeki bir path’e yönlendirir. İstekten gelen harici alan adına yönlendirme yapmaz.
|
|
231
|
+
|
|
232
|
+
## Sunucu izinleri
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { createDiscordGuildAuthorizer } from "@kavtuai/guildgate/discord";
|
|
236
|
+
|
|
237
|
+
const guildAccess = createDiscordGuildAuthorizer({
|
|
238
|
+
oauth: discord,
|
|
239
|
+
botToken: process.env.DISCORD_BOT_TOKEN!,
|
|
240
|
+
botUserId: process.env.DISCORD_BOT_USER_ID!,
|
|
241
|
+
cache: gate.cache,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const kaydet = gate.action({
|
|
245
|
+
name: "guild.settings.save",
|
|
246
|
+
parse: ayarlariDogrula,
|
|
247
|
+
resource: (input) => ({ type: "guild", id: input.guildId }),
|
|
248
|
+
authorize: guildAccess.require({
|
|
249
|
+
guildId: (input) => input.guildId,
|
|
250
|
+
userPermissions: ["MANAGE_GUILD"],
|
|
251
|
+
botPermissions: ["VIEW_CHANNEL", "MANAGE_ROLES"],
|
|
252
|
+
consistency: "live",
|
|
253
|
+
}),
|
|
254
|
+
execute: ayarlariKaydet,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Discord’a veya güvenlik ayarlarına yazan işlemlerde canlı kontrol kullanın. Düşük riskli görüntüleme verilerinde kısa süreli cache kullanılabilir. Üye rolleri, sunucu rolleri veya bot üyeliği değiştiğinde izin cache’i temizlenmelidir.
|
|
259
|
+
|
|
260
|
+
## Veritabanı seçimi
|
|
261
|
+
|
|
262
|
+
Tam sözleşme `GuildGateStores` arayüzüdür:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
interface GuildGateStores {
|
|
266
|
+
sessions: SessionStore;
|
|
267
|
+
oauthStates: OAuthStateStore;
|
|
268
|
+
credentials: OAuthCredentialStore;
|
|
269
|
+
rateLimits: RateLimitStore;
|
|
270
|
+
cache: CacheStore;
|
|
271
|
+
idempotency: IdempotencyStore;
|
|
272
|
+
locks: LockStore;
|
|
273
|
+
audit: AuditStore;
|
|
274
|
+
outbox: OutboxStore;
|
|
275
|
+
policies: PolicyStore;
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Kalıcı kayıtlar ana veritabanında, kısa ömürlü kayıtlar Redis’te tutulabilir:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { composeStores, createMemoryStoreBundle } from "@kavtuai/guildgate";
|
|
283
|
+
import {
|
|
284
|
+
createRedisEphemeralStores,
|
|
285
|
+
fromNodeRedis,
|
|
286
|
+
} from "@kavtuai/guildgate/redis";
|
|
287
|
+
|
|
288
|
+
const temporary = createRedisEphemeralStores(fromNodeRedis(redis), {
|
|
289
|
+
prefix: "mybot:guildgate",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const stores = composeStores(createMemoryStoreBundle(), {
|
|
293
|
+
...temporary,
|
|
294
|
+
credentials: postgresCredentialStore,
|
|
295
|
+
audit: postgresAuditStore,
|
|
296
|
+
outbox: postgresOutboxStore,
|
|
297
|
+
policies: postgresPolicyStore,
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Bu örnekteki bellek tabanı yalnızca birleştirme mantığını göstermek içindir. Üretim uygulaması, yeniden başlatma sonrasında korunması gereken her kayıt için kalıcı bir sürücü vermelidir.
|
|
302
|
+
|
|
303
|
+
Ayrıntılar: [Özel veri sürücüleri](./docs/tr/veritabani-suruculeri.md).
|
|
304
|
+
|
|
305
|
+
## Fastify ve Express
|
|
306
|
+
|
|
307
|
+
Bağlayıcılar framework tiplerini yapısal olarak kullanır. GuildGate, Fastify veya Express’i bağımlılık olarak kurmaz.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { fastifyActionHandler } from "@kavtuai/guildgate/fastify";
|
|
311
|
+
|
|
312
|
+
fastify.patch(
|
|
313
|
+
"/api/guilds/:guildId/settings",
|
|
314
|
+
fastifyActionHandler(gate, ayarlariGuncelle),
|
|
315
|
+
);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { expressActionHandler } from "@kavtuai/guildgate/express";
|
|
320
|
+
|
|
321
|
+
app.patch(
|
|
322
|
+
"/api/guilds/:guildId/settings",
|
|
323
|
+
expressActionHandler(gate, ayarlariGuncelle),
|
|
324
|
+
);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Gerçek zamanlı bağlantılar
|
|
328
|
+
|
|
329
|
+
`createRealtimeHub()` kendi başına socket sunucusu açmaz. `RealtimeConnection` arayüzü üzerinden `ws`, uWebSockets.js, Socket.IO, Bun veya başka bir taşıyıcıya bağlanır.
|
|
330
|
+
|
|
331
|
+
Bağlantı tam origin kontrolünden ve oturum doğrulamasından geçer. Her kanal aboneliği ayrı bir yetkilendirme fonksiyonuna sahiptir. Mesaj boyutu, mesaj sayısı, boşta kalma süresi, toplam bağlantı ömrü, abonelik sayısı ve yavaş istemci sınırları da uygulanır.
|
|
332
|
+
|
|
333
|
+
Ayrıntılar: [Mimari](./docs/tr/mimari.md) ve [gerçek zamanlı UML sırası](./docs/uml/realtime-sequence.mmd).
|
|
334
|
+
|
|
335
|
+
## Sahip yönetimi
|
|
336
|
+
|
|
337
|
+
Yapılandırılan sahip kimlikleriyle korunmuş yönetim endpoint’leri hazırlanabilir. Çekirdekte şu işlemler bulunur:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
await gate.owner.setMaintenance({ enabled: true, reason: "veritabanı bakımı" });
|
|
341
|
+
await gate.owner.block({ subjectType: "user", subjectId: "123", reason: "kötüye kullanım" });
|
|
342
|
+
await gate.owner.unblock("user", "123");
|
|
343
|
+
await gate.revokeUserSessions("123");
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Sahip endpoint’leri de oturum, CSRF, origin, rate limit ve denetim kontrollerinden geçmelidir. Bu metotları sahip yetkisi kontrol edilmemiş açık endpoint’lerden çağırmayın.
|
|
347
|
+
|
|
348
|
+
## Üretim kuralları
|
|
349
|
+
|
|
350
|
+
GuildGate üretimde şu ayarları kabul etmez:
|
|
351
|
+
|
|
352
|
+
- HTTPS kullanmayan uygulama adresi.
|
|
353
|
+
- Boş origin izin listesi.
|
|
354
|
+
- HTTP veya localhost origin değeri.
|
|
355
|
+
- `Secure` özelliği kapalı oturum çerezi.
|
|
356
|
+
- HTTPS kullanmayan Discord callback adresi.
|
|
357
|
+
- Kısa CSRF ve IP hashleme sırları.
|
|
358
|
+
|
|
359
|
+
Uygulamanın sorumlulukları:
|
|
360
|
+
|
|
361
|
+
- Reverse proxy güven ayarını doğru kurmak ve gerçek istemci IP’sini güvenli biçimde almak.
|
|
362
|
+
- Bot tokenını, OAuth sırlarını, şifreleme anahtarlarını ve veritabanı bilgilerini kaynak koddan uzak tutmak.
|
|
363
|
+
- Sürücü destekliyorsa veritabanı değişikliklerini transaction içinde yapmak.
|
|
364
|
+
- Aynı kaydı birden fazla yönetici düzenleyebiliyorsa revision kontrolü kullanmak.
|
|
365
|
+
- Denetim ve outbox kayıtları için saklama süresi belirlemek.
|
|
366
|
+
- Kalıcı verileri yedeklemek ve geri yükleme işlemini test etmek.
|
|
367
|
+
- Destekleyen HTTP ve veritabanı istemcilerine iptal sinyali geçirmek.
|
|
368
|
+
- Büyük sürümlerden önce Discord izinlerini ve API değişikliklerini kontrol etmek.
|
|
369
|
+
|
|
370
|
+
## Depo kontrolleri
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
npm run typecheck
|
|
374
|
+
npm test
|
|
375
|
+
npm run pack:check
|
|
376
|
+
node ./bin/guildgate-doctor.mjs
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Doctor komutu ortam değişkenlerini yerelde kontrol eder. Gizli değerleri başka bir servise göndermez.
|
|
380
|
+
|
|
381
|
+
## Belgeler
|
|
382
|
+
|
|
383
|
+
- [Kurulum ve ilk yayın](./docs/tr/kurulum.md)
|
|
384
|
+
- [Mimari](./docs/tr/mimari.md)
|
|
385
|
+
- [Özel veri sürücüleri](./docs/tr/veritabani-suruculeri.md)
|
|
386
|
+
- [Sahip yönetimi](./docs/tr/sahip-yonetimi.md)
|
|
387
|
+
- [Tehdit modeli](./docs/tr/tehdit-modeli.md)
|
|
388
|
+
- [Yazım kuralları](./docs/tr/yazim-kilavuzu.md)
|
|
389
|
+
- [Araştırma notları](./docs/tr/arastirma-notlari.md)
|
|
390
|
+
- [Yerel test raporu](./TEST_REPORT.md)
|
|
391
|
+
- [Sürüm planı](./ROADMAP.md)
|
|
392
|
+
- [Güvenlik bildirimi](./SECURITY.md)
|
|
393
|
+
- [UML dosyaları](./docs/uml)
|
|
394
|
+
|
|
395
|
+
## Lisans
|
|
396
|
+
|
|
397
|
+
MIT. Bkz. [LICENSE](./LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Until `1.0.0`, only the newest released minor version receives security fixes. After `1.0.0`, the support window will be stated here before each branch is opened.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Do not open a public issue for a suspected vulnerability.
|
|
10
|
+
|
|
11
|
+
Use GitHub private vulnerability reporting for `kavtuai/guildgate` when the repository is available. Include:
|
|
12
|
+
|
|
13
|
+
- Affected version or commit.
|
|
14
|
+
- Required configuration.
|
|
15
|
+
- Reproduction steps or a small proof of concept.
|
|
16
|
+
- Expected and observed behavior.
|
|
17
|
+
- Possible impact.
|
|
18
|
+
- Any temporary mitigation already tested.
|
|
19
|
+
|
|
20
|
+
Do not include real bot tokens, OAuth secrets, session cookies, database credentials, or user data. Replace them with test values.
|
|
21
|
+
|
|
22
|
+
## Response process
|
|
23
|
+
|
|
24
|
+
The maintainer will first confirm receipt, reproduce the report, assign severity, and decide whether a private patch branch is needed. Publication timing depends on impact, fix availability, and coordination with affected adapter maintainers.
|
|
25
|
+
|
|
26
|
+
A security release should include:
|
|
27
|
+
|
|
28
|
+
- A fixed package version.
|
|
29
|
+
- Affected version range.
|
|
30
|
+
- Concrete impact and prerequisites.
|
|
31
|
+
- Upgrade or mitigation steps.
|
|
32
|
+
- Credit when the reporter requests it.
|
|
33
|
+
|
|
34
|
+
## Security boundaries
|
|
35
|
+
|
|
36
|
+
The package rejects several unsafe production settings and implements controls described in `docs/threat-model.md`. It does not configure the host, proxy, TLS, firewall, database roles, secret manager, backups, or application-specific authorization policy.
|
package/TEST_REPORT.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Test report
|
|
2
|
+
|
|
3
|
+
Date: 2026-07-25
|
|
4
|
+
Package: `@kavtuai/guildgate@0.1.0`
|
|
5
|
+
|
|
6
|
+
## Local toolchain
|
|
7
|
+
|
|
8
|
+
- Node.js: 22.16.0
|
|
9
|
+
- npm: 10.9.2
|
|
10
|
+
- TypeScript compiler: 5.8.3
|
|
11
|
+
|
|
12
|
+
## Passed checks
|
|
13
|
+
|
|
14
|
+
- Strict TypeScript build.
|
|
15
|
+
- Type-only check without emit.
|
|
16
|
+
- Repository writing check.
|
|
17
|
+
- npm package dry-run.
|
|
18
|
+
- Local offline installation of the generated `.tgz` package into an empty consumer project.
|
|
19
|
+
- Import checks for every public package subpath.
|
|
20
|
+
- Production doctor check with generated test secrets and HTTPS test URLs.
|
|
21
|
+
- Public declaration scan for accidental `Buffer` or Node namespace requirements.
|
|
22
|
+
- Model citation artifact and unfinished placeholder scan.
|
|
23
|
+
|
|
24
|
+
## Automated behavior tests
|
|
25
|
+
|
|
26
|
+
Eleven tests pass:
|
|
27
|
+
|
|
28
|
+
1. Protected actions reject missing sessions and missing CSRF tokens.
|
|
29
|
+
2. Idempotency replays the first result and rejects changed payloads.
|
|
30
|
+
3. Rate limits and owner maintenance policy are applied.
|
|
31
|
+
4. Cache tags invalidate matching entries.
|
|
32
|
+
5. A cache failure after commit does not repeat the domain operation.
|
|
33
|
+
6. Error messages remain in the selected locale and support application overrides.
|
|
34
|
+
7. Discord high-bit permissions retain precision with `BigInt`.
|
|
35
|
+
8. Base guild permissions combine the everyone role and member roles.
|
|
36
|
+
9. Discord OAuth state is browser-bound, single-use, encrypted at rest, and rejects external return URLs.
|
|
37
|
+
10. Realtime subscriptions require authorization and explicit session revocation closes sockets.
|
|
38
|
+
11. Periodic realtime session validation closes a socket revoked outside the hub.
|
|
39
|
+
|
|
40
|
+
## Not run in this container
|
|
41
|
+
|
|
42
|
+
- A real Discord OAuth callback.
|
|
43
|
+
- A real Discord bot permission request.
|
|
44
|
+
- A real Redis server.
|
|
45
|
+
- PostgreSQL, MongoDB, MySQL, or SQLite integration.
|
|
46
|
+
- Fastify and Express example installation with their external framework dependencies.
|
|
47
|
+
- Dependency installation from the npm registry. Registry access from the build container timed out, so this archive does not contain a generated `package-lock.json`.
|
|
48
|
+
- External security review and load test.
|
|
49
|
+
|
|
50
|
+
These tests must be completed in the target repository before a production `1.0.0` release. The package name and GitHub repository name were not published or reserved by this build.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
|
|
4
|
+
const isJson = process.argv.includes("--json");
|
|
5
|
+
const environment = process.env.GUILDGATE_ENVIRONMENT ?? process.env.NODE_ENV ?? "development";
|
|
6
|
+
const results = [];
|
|
7
|
+
|
|
8
|
+
check("Node.js 22 or newer", Number(process.versions.node.split(".")[0]) >= 22, process.versions.node);
|
|
9
|
+
checkRequired("DISCORD_CLIENT_ID");
|
|
10
|
+
checkRequired("DISCORD_CLIENT_SECRET");
|
|
11
|
+
checkRequired("DISCORD_REDIRECT_URI");
|
|
12
|
+
checkRequired("GUILDGATE_CSRF_SECRET");
|
|
13
|
+
checkRequired("GUILDGATE_AUDIT_IP_SALT");
|
|
14
|
+
checkRequired("GUILDGATE_TOKEN_KEY_BASE64");
|
|
15
|
+
|
|
16
|
+
checkLength("GUILDGATE_CSRF_SECRET", 32);
|
|
17
|
+
checkLength("GUILDGATE_AUDIT_IP_SALT", 16);
|
|
18
|
+
checkBase64Key("GUILDGATE_TOKEN_KEY_BASE64", 32);
|
|
19
|
+
|
|
20
|
+
if (process.env.DISCORD_REDIRECT_URI) {
|
|
21
|
+
checkUrl("Discord redirect URI is valid", process.env.DISCORD_REDIRECT_URI, environment === "production");
|
|
22
|
+
}
|
|
23
|
+
if (process.env.GUILDGATE_BASE_URL) {
|
|
24
|
+
checkUrl("Application base URL is valid", process.env.GUILDGATE_BASE_URL, environment === "production");
|
|
25
|
+
} else {
|
|
26
|
+
results.push({ name: "GUILDGATE_BASE_URL", status: environment === "production" ? "fail" : "warn", detail: "not set" });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const origins = (process.env.GUILDGATE_ALLOWED_ORIGINS ?? "")
|
|
30
|
+
.split(",")
|
|
31
|
+
.map((value) => value.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
|
|
34
|
+
if (environment === "production") {
|
|
35
|
+
check("Production origin list is not empty", origins.length > 0, origins.length ? origins.join(", ") : "not set");
|
|
36
|
+
for (const origin of origins) {
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(origin);
|
|
39
|
+
check(`Origin uses HTTPS: ${origin}`, url.protocol === "https:", url.protocol);
|
|
40
|
+
check(`Origin is not localhost: ${origin}`, !["localhost", "127.0.0.1", "::1"].includes(url.hostname), url.hostname);
|
|
41
|
+
} catch {
|
|
42
|
+
check(`Origin is a valid URL: ${origin}`, false, "invalid URL");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isJson) {
|
|
48
|
+
console.log(JSON.stringify({ environment, results }, null, 2));
|
|
49
|
+
} else {
|
|
50
|
+
console.log(`GuildGate doctor (${environment})`);
|
|
51
|
+
for (const result of results) {
|
|
52
|
+
const mark = result.status === "pass" ? "PASS" : result.status === "warn" ? "WARN" : "FAIL";
|
|
53
|
+
console.log(`${mark.padEnd(4)} ${result.name}${result.detail ? ` — ${result.detail}` : ""}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.exitCode = results.some((result) => result.status === "fail") ? 1 : 0;
|
|
58
|
+
|
|
59
|
+
function checkRequired(name) {
|
|
60
|
+
check(`${name} is set`, Boolean(process.env[name]), process.env[name] ? "present" : "missing");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkLength(name, minimumBytes) {
|
|
64
|
+
const value = process.env[name];
|
|
65
|
+
if (!value) return;
|
|
66
|
+
check(`${name} contains at least ${minimumBytes} bytes`, Buffer.byteLength(value) >= minimumBytes, `${Buffer.byteLength(value)} bytes`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function checkBase64Key(name, expectedBytes) {
|
|
70
|
+
const value = process.env[name];
|
|
71
|
+
if (!value) return;
|
|
72
|
+
try {
|
|
73
|
+
const decoded = Buffer.from(value, "base64");
|
|
74
|
+
check(`${name} decodes to ${expectedBytes} bytes`, decoded.length === expectedBytes, `${decoded.length} bytes`);
|
|
75
|
+
} catch {
|
|
76
|
+
check(`${name} is valid base64`, false, "decode failed");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function checkUrl(name, value, requireHttps) {
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(value);
|
|
83
|
+
check(name, !requireHttps || url.protocol === "https:", `${url.protocol}//${url.host}`);
|
|
84
|
+
} catch {
|
|
85
|
+
check(name, false, "invalid URL");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function check(name, passed, detail) {
|
|
90
|
+
results.push({ name, status: passed ? "pass" : "fail", detail });
|
|
91
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { extname, join, relative } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
7
|
+
const includedRoots = [
|
|
8
|
+
"README.md",
|
|
9
|
+
"README.tr.md",
|
|
10
|
+
"CHANGELOG.md",
|
|
11
|
+
"ROADMAP.md",
|
|
12
|
+
"SECURITY.md",
|
|
13
|
+
"CONTRIBUTING.md",
|
|
14
|
+
"CODE_OF_CONDUCT.md",
|
|
15
|
+
"docs",
|
|
16
|
+
];
|
|
17
|
+
const excluded = new Set([
|
|
18
|
+
"docs/WRITING_STYLE.md",
|
|
19
|
+
"docs/tr/yazim-kilavuzu.md",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const hardPatterns = [
|
|
23
|
+
["assistant boilerplate", /\b(hope this helps|certainly[,!]|here is a polished|in today's fast-paced world|it is important to note)\b/i],
|
|
24
|
+
["decorative contrast", /\bnot only\b[^.\n]{0,160}\bbut also\b/i],
|
|
25
|
+
["provider citation artifact", /contentReference|oaicite|turn\d+(?:search|view|fetch)|attributableIndex|ppl-ai-file-upload|grok_card|:::writing/i],
|
|
26
|
+
["tracking query", /[?&]utm_(?:source|medium|campaign|term|content)=/i],
|
|
27
|
+
["unfinished placeholder", /\[(?:insert|add source|add name|placeholder)[^\]]*\]/i],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const watchedWords = /\b(additionally|boasts|bolstered|crucial|delve|enduring|fostering|garner|interplay|intricate|landscape|meticulous|pivotal|robust|showcase|tapestry|testament|underscore|vibrant)\b/gi;
|
|
31
|
+
const failures = [];
|
|
32
|
+
const reviews = [];
|
|
33
|
+
|
|
34
|
+
for (const target of includedRoots) {
|
|
35
|
+
const full = join(root, target);
|
|
36
|
+
for (const file of await filesUnder(full)) {
|
|
37
|
+
const name = relative(root, file).replaceAll("\\", "/");
|
|
38
|
+
if (excluded.has(name) || ![".md", ".mmd"].includes(extname(file))) continue;
|
|
39
|
+
const text = await readFile(file, "utf8");
|
|
40
|
+
for (const [label, pattern] of hardPatterns) {
|
|
41
|
+
const match = text.match(pattern);
|
|
42
|
+
if (match) failures.push({ file: name, label, value: match[0] });
|
|
43
|
+
}
|
|
44
|
+
const matches = [...text.matchAll(watchedWords)].map((match) => match[0]);
|
|
45
|
+
if (matches.length) reviews.push({ file: name, words: [...new Set(matches.map((word) => word.toLowerCase()))] });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const item of failures) console.error(`FAIL ${item.file}: ${item.label}: ${item.value}`);
|
|
50
|
+
for (const item of reviews) console.error(`REVIEW ${item.file}: ${item.words.join(", ")}`);
|
|
51
|
+
|
|
52
|
+
if (failures.length || reviews.length) {
|
|
53
|
+
console.error(`Writing check found ${failures.length} failure(s) and ${reviews.length} review item(s).`);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
} else {
|
|
56
|
+
console.log("Writing check passed.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function filesUnder(path) {
|
|
60
|
+
try {
|
|
61
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
62
|
+
const files = [];
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const child = join(path, entry.name);
|
|
65
|
+
if (entry.isDirectory()) files.push(...await filesUnder(child));
|
|
66
|
+
else if (entry.isFile()) files.push(child);
|
|
67
|
+
}
|
|
68
|
+
return files;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error?.code === "ENOTDIR") return [path];
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|