@voltro/plugin-saves 0.0.0-stage → 0.88.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 +24492 -0
- package/LICENSE +61 -0
- package/README.md +90 -2
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +5603 -0
- package/dist/contract.js +78 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +5 -0
- package/dist/inspect.d.ts +82 -0
- package/dist/inspect.js +48 -0
- package/dist/plugin.js +335 -0
- package/dist/port.d.ts +103 -0
- package/dist/port.js +33 -0
- package/dist/rpc.d.ts +128 -0
- package/dist/rpc.js +85 -0
- package/dist/server.d.ts +286 -0
- package/dist/server.js +7 -0
- package/dist/service.server.js +412 -0
- package/dist/strategies.d.ts +76 -0
- package/dist/strategies.js +47 -0
- package/dist/tables.entity.js +60 -0
- package/dist/testing.d.ts +213 -0
- package/dist/testing.js +116 -0
- package/dist/web.d.ts +116 -0
- package/dist/web.js +4 -0
- package/package.json +88 -4
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { SaveConflict as e, SavesError as t, savesError as n } from "./errors.js";
|
|
2
|
+
import { SaveHeaderSchema as r, captureSavePut as i, saveData as a, saveId as o, saveInteger as s, saveOwner as c, saveSummary as l } from "./contract.js";
|
|
3
|
+
import { resolveSaveValidation as u, resolveSavesOptions as d } from "./strategies.js";
|
|
4
|
+
import { savesHistoryTable as f, savesRequestsTable as p, savesSubjectsTable as m, savesTable as h } from "./tables.entity.js";
|
|
5
|
+
import { Effect as g, Result as _, Schema as v } from "effect";
|
|
6
|
+
import { createHash as y } from "node:crypto";
|
|
7
|
+
import { and as b, eq as x, queryFor as S } from "@voltro/database";
|
|
8
|
+
// src/service.server.ts
|
|
9
|
+
var C = (e) => Array.isArray(e) ? e.map(C) : e && typeof e == "object" ? Object.fromEntries(Object.keys(e).sort().map((t) => [t, C(e[t])])) : e, w = (e) => y("sha256").update(JSON.stringify(C(e))).digest("hex"), T = (e) => y("sha256").update(e).digest("hex"), E = (e, ...t) => e + w(t).slice(0, 48), D = (r) => r instanceof t || r instanceof e ? r : n("unavailable"), O = (e) => g.tryPromise({
|
|
10
|
+
try: e,
|
|
11
|
+
catch: D
|
|
12
|
+
}), k = (e) => {
|
|
13
|
+
let t = e instanceof Date ? e.getTime() : typeof e == "string" ? Date.parse(e) : e;
|
|
14
|
+
try {
|
|
15
|
+
return s(t, 0, 864e13);
|
|
16
|
+
} catch {
|
|
17
|
+
throw n("invalid_record");
|
|
18
|
+
}
|
|
19
|
+
}, A = (e) => {
|
|
20
|
+
try {
|
|
21
|
+
let t = v.decodeUnknownSync(r)({
|
|
22
|
+
slot: e.slot,
|
|
23
|
+
version: e.version,
|
|
24
|
+
schemaVersion: e.schemaVersion,
|
|
25
|
+
byteLength: e.byteLength,
|
|
26
|
+
checksum: e.checksum,
|
|
27
|
+
deviceId: e.deviceId,
|
|
28
|
+
clientVersion: e.clientVersion,
|
|
29
|
+
savedAt: k(e.savedAt),
|
|
30
|
+
summary: e.summary,
|
|
31
|
+
deletedUntil: e.deletedUntil === null ? null : k(e.deletedUntil)
|
|
32
|
+
});
|
|
33
|
+
if (t.version < 1 || t.schemaVersion < 1 || t.byteLength > 1048576 || !/^[a-f0-9]{64}$/.test(t.checksum)) throw n("invalid_record");
|
|
34
|
+
return o(t.slot), o(t.deviceId), o(t.clientVersion), Object.freeze({
|
|
35
|
+
...t,
|
|
36
|
+
summary: l(t.summary)
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
throw n("invalid_record");
|
|
40
|
+
}
|
|
41
|
+
}, j = (e) => {
|
|
42
|
+
let t = A(e);
|
|
43
|
+
if (!(e.bytes instanceof Uint8Array) || e.bytes.byteLength !== t.byteLength || T(e.bytes) !== t.checksum) throw n("invalid_record");
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
...t,
|
|
46
|
+
bytes: Uint8Array.from(e.bytes)
|
|
47
|
+
});
|
|
48
|
+
}, M = [
|
|
49
|
+
"id",
|
|
50
|
+
"tenantId",
|
|
51
|
+
"gameId",
|
|
52
|
+
"ownerId",
|
|
53
|
+
"ownerKey",
|
|
54
|
+
"slot",
|
|
55
|
+
"version",
|
|
56
|
+
"schemaVersion",
|
|
57
|
+
"byteLength",
|
|
58
|
+
"checksum",
|
|
59
|
+
"deviceId",
|
|
60
|
+
"clientVersion",
|
|
61
|
+
"savedAt",
|
|
62
|
+
"summary",
|
|
63
|
+
"deletedUntil"
|
|
64
|
+
], N = (l, y, C = () => {}) => {
|
|
65
|
+
let k = a(y, [
|
|
66
|
+
"gameId",
|
|
67
|
+
"slots",
|
|
68
|
+
"maxBytes",
|
|
69
|
+
"historyVersions",
|
|
70
|
+
"maxRequests",
|
|
71
|
+
"deleteGraceMs",
|
|
72
|
+
"version",
|
|
73
|
+
"validator",
|
|
74
|
+
"schema",
|
|
75
|
+
"schemaVersion",
|
|
76
|
+
"now",
|
|
77
|
+
"admitWrite"
|
|
78
|
+
]), N = k.admitWrite, P = Object.fromEntries(Object.entries(k).filter(([e]) => e !== "admitWrite")), F = d(P);
|
|
79
|
+
if (typeof N != "function" || typeof C != "function") throw n("invalid_input");
|
|
80
|
+
let I = (e) => E("svs_", e.tenantId, F.gameId, e.ownerId), L = (e, t) => E("sv_", I(e), t), R = (e, t, n) => E("svh_", I(e), t, n), z = (e) => b(x("tenantId", e.tenantId), x("gameId", F.gameId), x("ownerKey", I(e)), x("ownerId", e.ownerId)), B = (e, t) => {
|
|
81
|
+
if (e.tenantId !== t.tenantId || e.gameId !== F.gameId || e.ownerId !== t.ownerId || e.ownerKey !== I(t)) throw n("invalid_record");
|
|
82
|
+
}, V = (e) => {
|
|
83
|
+
let t = o(e);
|
|
84
|
+
if (!F.slots.includes(t)) throw n("not_found");
|
|
85
|
+
return t;
|
|
86
|
+
}, H = () => (C(), s(F.now(), 0, 864e13)), U = async (e) => {
|
|
87
|
+
C(), await l.insertIgnore(m.tableName, {
|
|
88
|
+
id: I(e),
|
|
89
|
+
tenantId: e.tenantId,
|
|
90
|
+
gameId: F.gameId,
|
|
91
|
+
ownerId: e.ownerId,
|
|
92
|
+
erased: !1,
|
|
93
|
+
admission: 0
|
|
94
|
+
}, { conflictColumns: ["id"] });
|
|
95
|
+
}, W = async (e, t, r = !1, i = !1) => {
|
|
96
|
+
if (C(), r && await e.updateMany(m.tableName, { admission: 0 }, { where: x("id", I(t)) }) !== 1) throw n("unavailable");
|
|
97
|
+
let a = (await e.query(S(m).where(x("id", I(t))).limit(1).descriptor))[0];
|
|
98
|
+
if (!a) return null;
|
|
99
|
+
if (a.id !== I(t) || a.tenantId !== t.tenantId || a.gameId !== F.gameId || a.admission !== 0 || typeof a.erased != "boolean") throw n("invalid_record");
|
|
100
|
+
if (a.erased) {
|
|
101
|
+
if (i && a.ownerId === null) return a;
|
|
102
|
+
throw n("erased");
|
|
103
|
+
}
|
|
104
|
+
if (a.ownerId !== t.ownerId) throw n("invalid_record");
|
|
105
|
+
return a;
|
|
106
|
+
}, G = async (e, t, r) => {
|
|
107
|
+
let i = (await e.query(S(h).where(x("id", L(t, r))).limit(1).descriptor))[0];
|
|
108
|
+
if (!i) return null;
|
|
109
|
+
if (B(i, t), i.slot !== r || i.id !== L(t, r)) throw n("invalid_record");
|
|
110
|
+
return i;
|
|
111
|
+
}, K = async (e, t, r, i) => {
|
|
112
|
+
let a = (await e.query(S(f).where(x("id", R(t, r, i))).limit(1).descriptor))[0];
|
|
113
|
+
if (!a) throw n("not_found");
|
|
114
|
+
if (B(a, t), a.slot !== r || a.version !== i) throw n("invalid_record");
|
|
115
|
+
return j(a);
|
|
116
|
+
}, q = async (e, t, i, a) => {
|
|
117
|
+
let o = (await e.query(S(p).where(x("id", i)).limit(1).descriptor))[0];
|
|
118
|
+
if (!o) return null;
|
|
119
|
+
if (B(o, t), o.digest !== a) throw n("request_conflict");
|
|
120
|
+
try {
|
|
121
|
+
return Object.freeze(v.decodeUnknownSync(r)(o.receipt));
|
|
122
|
+
} catch {
|
|
123
|
+
throw n("invalid_record");
|
|
124
|
+
}
|
|
125
|
+
}, J = async (e, t, r, i) => {
|
|
126
|
+
let a = await e.query(S(r).select(...M).where(z(t)).orderBy("id", "asc").limit(i + 1).descriptor);
|
|
127
|
+
if (a.length > i) throw n("limit_exceeded");
|
|
128
|
+
for (let e of a) B(e, t), A(e);
|
|
129
|
+
return a;
|
|
130
|
+
}, Y = (e, r) => {
|
|
131
|
+
let i = null;
|
|
132
|
+
if (F.schema) {
|
|
133
|
+
if (r.schemaVersion !== F.schemaVersion) throw n("invalid_save");
|
|
134
|
+
try {
|
|
135
|
+
i = v.decodeUnknownSync(v.toType(F.schema))(JSON.parse(new TextDecoder("utf-8", { fatal: !0 }).decode(r.bytes)));
|
|
136
|
+
} catch {
|
|
137
|
+
throw n("invalid_save");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
u(Object.freeze({
|
|
142
|
+
previous: e === null ? null : structuredClone(e),
|
|
143
|
+
proposed: structuredClone(r),
|
|
144
|
+
decoded: i
|
|
145
|
+
}), F.validator);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
throw e instanceof t ? e : n("invalid_save");
|
|
148
|
+
}
|
|
149
|
+
}, X = (t, r, i) => O(async () => {
|
|
150
|
+
let a = w([r.kind, r.intent]), o = E("svr_", I(t), r.requestId);
|
|
151
|
+
await U(t);
|
|
152
|
+
let c = await l.transactional(async (n) => {
|
|
153
|
+
await W(n, t, !0);
|
|
154
|
+
let i = await q(n, t, o, a);
|
|
155
|
+
if (i) return { accepted: i };
|
|
156
|
+
let s = await G(n, t, r.slot), c = s ? j(s) : null;
|
|
157
|
+
if ((c?.version ?? 0) !== r.baseVersion) throw new e({
|
|
158
|
+
serverVersion: c?.version ?? 0,
|
|
159
|
+
server: c?.summary ?? {},
|
|
160
|
+
client: r.summary
|
|
161
|
+
});
|
|
162
|
+
return { previous: c };
|
|
163
|
+
});
|
|
164
|
+
if ("accepted" in c) return c.accepted;
|
|
165
|
+
let u = H(), d = await g.runPromise(g.result(N(I(t), u)));
|
|
166
|
+
if (_.isFailure(d)) throw d.failure;
|
|
167
|
+
let m = await i(l, c.previous, u);
|
|
168
|
+
return r.kind !== "remove" && Y(c.previous, m), C(), l.transactional(async (i) => {
|
|
169
|
+
await W(i, t, !0);
|
|
170
|
+
let l = await q(i, t, o, a);
|
|
171
|
+
if (l) return l;
|
|
172
|
+
let d = await G(i, t, r.slot), g = d ? j(d) : null;
|
|
173
|
+
if ((g?.version ?? 0) !== r.baseVersion) throw new e({
|
|
174
|
+
serverVersion: g?.version ?? 0,
|
|
175
|
+
server: g?.summary ?? {},
|
|
176
|
+
client: r.summary
|
|
177
|
+
});
|
|
178
|
+
if (g && c.previous && (g.checksum !== c.previous.checksum || g.deletedUntil !== c.previous.deletedUntil)) throw n("request_conflict");
|
|
179
|
+
if (r.kind === "rollback") {
|
|
180
|
+
let e = s(r.intent.version, 1);
|
|
181
|
+
if ((await K(i, t, r.slot, e)).checksum !== T(m.bytes)) throw n("request_conflict");
|
|
182
|
+
}
|
|
183
|
+
if ((await i.query(S(p).select("id").where(z(t)).limit(F.maxRequests + 1).descriptor)).length >= F.maxRequests) throw n("limit_exceeded");
|
|
184
|
+
let _ = s(r.baseVersion + 1, 1), v = r.kind === "remove" ? s(u + F.deleteGraceMs, 0, 864e13) : null, y = Object.freeze({
|
|
185
|
+
slot: r.slot,
|
|
186
|
+
version: _,
|
|
187
|
+
schemaVersion: m.schemaVersion,
|
|
188
|
+
byteLength: m.bytes.byteLength,
|
|
189
|
+
checksum: T(m.bytes),
|
|
190
|
+
deviceId: m.deviceId,
|
|
191
|
+
clientVersion: m.clientVersion,
|
|
192
|
+
savedAt: u,
|
|
193
|
+
summary: Object.freeze({ ...m.summary }),
|
|
194
|
+
deletedUntil: v
|
|
195
|
+
}), w = {
|
|
196
|
+
tenantId: t.tenantId,
|
|
197
|
+
gameId: F.gameId,
|
|
198
|
+
ownerId: t.ownerId,
|
|
199
|
+
ownerKey: I(t),
|
|
200
|
+
...y,
|
|
201
|
+
bytes: Uint8Array.from(m.bytes),
|
|
202
|
+
savedAt: new Date(u),
|
|
203
|
+
deletedUntil: v === null ? null : new Date(v)
|
|
204
|
+
};
|
|
205
|
+
d ? await i.updateMany(h.tableName, w, { where: x("id", L(t, r.slot)) }) : await i.insert(h.tableName, {
|
|
206
|
+
id: L(t, r.slot),
|
|
207
|
+
...w
|
|
208
|
+
}), await i.insert(f.tableName, {
|
|
209
|
+
id: R(t, r.slot, _),
|
|
210
|
+
...w
|
|
211
|
+
});
|
|
212
|
+
let E = await i.query(S(f).select("id", "version").where(b(z(t), x("slot", r.slot))).orderBy("version", "desc").limit(102).descriptor);
|
|
213
|
+
if (E.length > 101) throw n("invalid_record");
|
|
214
|
+
for (let e of E.slice(F.historyVersions)) await i.deleteMany(f.tableName, { where: x("id", e.id) });
|
|
215
|
+
return await i.insert(p.tableName, {
|
|
216
|
+
id: o,
|
|
217
|
+
tenantId: t.tenantId,
|
|
218
|
+
gameId: F.gameId,
|
|
219
|
+
ownerId: t.ownerId,
|
|
220
|
+
ownerKey: I(t),
|
|
221
|
+
digest: a,
|
|
222
|
+
receipt: y
|
|
223
|
+
}), C(), y;
|
|
224
|
+
});
|
|
225
|
+
}), Z = (e) => {
|
|
226
|
+
let t = a(e, [
|
|
227
|
+
"subjectId",
|
|
228
|
+
"tenantId",
|
|
229
|
+
"kind"
|
|
230
|
+
]);
|
|
231
|
+
if (t.kind !== "user" && t.kind !== "guest") throw n("invalid_input");
|
|
232
|
+
return {
|
|
233
|
+
tenantId: o(t.tenantId),
|
|
234
|
+
ownerId: o(t.subjectId)
|
|
235
|
+
};
|
|
236
|
+
}, Q = (e, t) => {
|
|
237
|
+
try {
|
|
238
|
+
return t(e());
|
|
239
|
+
} catch (e) {
|
|
240
|
+
return g.fail(D(e));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
get: (e, t) => {
|
|
245
|
+
try {
|
|
246
|
+
let n = c(e), r = V(t);
|
|
247
|
+
return O(() => l.transactional(async (e) => {
|
|
248
|
+
if (!await W(e, n)) return null;
|
|
249
|
+
let t = await G(e, n, r);
|
|
250
|
+
if (!t) return null;
|
|
251
|
+
let i = j(t);
|
|
252
|
+
return i.deletedUntil === null ? i : null;
|
|
253
|
+
}));
|
|
254
|
+
} catch (e) {
|
|
255
|
+
return g.fail(D(e));
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
list: (e) => {
|
|
259
|
+
try {
|
|
260
|
+
let t = c(e);
|
|
261
|
+
return O(() => l.transactional(async (e) => await W(e, t) ? (await J(e, t, h, 64)).map(A).filter((e) => e.deletedUntil === null) : []));
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return g.fail(D(e));
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
history: (e, t) => {
|
|
267
|
+
try {
|
|
268
|
+
let n = c(e), r = V(t);
|
|
269
|
+
return O(() => l.transactional(async (e) => await W(e, n) ? (await J(e, n, f, 6400)).filter((e) => e.slot === r).map(A).sort((e, t) => t.version - e.version) : []));
|
|
270
|
+
} catch (e) {
|
|
271
|
+
return g.fail(D(e));
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
put: (e, t) => {
|
|
275
|
+
try {
|
|
276
|
+
let n = c(e), r = i(t, F.maxBytes);
|
|
277
|
+
V(r.slot);
|
|
278
|
+
let a = {
|
|
279
|
+
...r,
|
|
280
|
+
bytes: T(r.bytes)
|
|
281
|
+
};
|
|
282
|
+
return X(n, {
|
|
283
|
+
kind: "put",
|
|
284
|
+
...r,
|
|
285
|
+
intent: a
|
|
286
|
+
}, async () => r);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
return g.fail(D(e));
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
rollback: (e, t) => {
|
|
292
|
+
try {
|
|
293
|
+
let r = c(e), l = a(t, [
|
|
294
|
+
"slot",
|
|
295
|
+
"requestId",
|
|
296
|
+
"baseVersion",
|
|
297
|
+
"version"
|
|
298
|
+
]), u = {
|
|
299
|
+
slot: V(l.slot),
|
|
300
|
+
requestId: o(l.requestId),
|
|
301
|
+
baseVersion: s(l.baseVersion),
|
|
302
|
+
version: s(l.version, 1)
|
|
303
|
+
};
|
|
304
|
+
return X(r, {
|
|
305
|
+
kind: "rollback",
|
|
306
|
+
...u,
|
|
307
|
+
summary: {},
|
|
308
|
+
intent: u
|
|
309
|
+
}, async (e, t, a) => {
|
|
310
|
+
if (!t || t.deletedUntil !== null && t.deletedUntil <= a) throw n("not_found");
|
|
311
|
+
let o = await K(e, r, u.slot, u.version);
|
|
312
|
+
return i({
|
|
313
|
+
slot: u.slot,
|
|
314
|
+
requestId: u.requestId,
|
|
315
|
+
baseVersion: u.baseVersion,
|
|
316
|
+
schemaVersion: o.schemaVersion,
|
|
317
|
+
bytes: o.bytes,
|
|
318
|
+
deviceId: o.deviceId,
|
|
319
|
+
clientVersion: o.clientVersion,
|
|
320
|
+
summary: o.summary
|
|
321
|
+
}, F.maxBytes);
|
|
322
|
+
});
|
|
323
|
+
} catch (e) {
|
|
324
|
+
return g.fail(D(e));
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
remove: (e, t) => {
|
|
328
|
+
try {
|
|
329
|
+
let r = c(e), i = a(t, [
|
|
330
|
+
"slot",
|
|
331
|
+
"requestId",
|
|
332
|
+
"baseVersion"
|
|
333
|
+
]), l = {
|
|
334
|
+
slot: V(i.slot),
|
|
335
|
+
requestId: o(i.requestId),
|
|
336
|
+
baseVersion: s(i.baseVersion)
|
|
337
|
+
};
|
|
338
|
+
return X(r, {
|
|
339
|
+
kind: "remove",
|
|
340
|
+
...l,
|
|
341
|
+
summary: {},
|
|
342
|
+
intent: l
|
|
343
|
+
}, async (e, t) => {
|
|
344
|
+
if (!t || t.deletedUntil !== null) throw n("not_found");
|
|
345
|
+
return {
|
|
346
|
+
...l,
|
|
347
|
+
schemaVersion: t.schemaVersion,
|
|
348
|
+
bytes: t.bytes,
|
|
349
|
+
deviceId: t.deviceId,
|
|
350
|
+
clientVersion: t.clientVersion,
|
|
351
|
+
summary: t.summary
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
} catch (e) {
|
|
355
|
+
return g.fail(D(e));
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
exportSubject: (e) => Q(() => Z(e), (e) => O(() => l.transactional(async (t) => {
|
|
359
|
+
let r = await W(t, e, !1, !0);
|
|
360
|
+
if (!r || r.erased) return {
|
|
361
|
+
saves: [],
|
|
362
|
+
history: []
|
|
363
|
+
};
|
|
364
|
+
let i = await J(t, e, h, 64), a = await J(t, e, f, 6400);
|
|
365
|
+
if ([...i, ...a].reduce((e, t) => e + A(t).byteLength, 0) > 67108864) throw n("limit_exceeded");
|
|
366
|
+
let o = async (r, i) => {
|
|
367
|
+
let a = [];
|
|
368
|
+
for (let o of i) {
|
|
369
|
+
let i = (await t.query(S(r).where(x("id", o.id)).limit(1).descriptor))[0];
|
|
370
|
+
if (!i) throw n("invalid_record");
|
|
371
|
+
B(i, e);
|
|
372
|
+
let s = j(i);
|
|
373
|
+
a.push({
|
|
374
|
+
...s,
|
|
375
|
+
bytes: Buffer.from(s.bytes).toString("base64")
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return a;
|
|
379
|
+
};
|
|
380
|
+
return {
|
|
381
|
+
saves: await o(h, i),
|
|
382
|
+
history: await o(f, a)
|
|
383
|
+
};
|
|
384
|
+
}))),
|
|
385
|
+
eraseSubject: (e) => {
|
|
386
|
+
try {
|
|
387
|
+
let t = Z(e);
|
|
388
|
+
return O(async () => {
|
|
389
|
+
await U(t), await l.transactional(async (e) => {
|
|
390
|
+
await W(e, t, !0, !0);
|
|
391
|
+
for (let [r, i] of [
|
|
392
|
+
[h, 64],
|
|
393
|
+
[f, 6400],
|
|
394
|
+
[p, 1e5]
|
|
395
|
+
]) {
|
|
396
|
+
let a = await e.query(S(r).select("id", "tenantId", "gameId", "ownerId", "ownerKey").where(z(t)).limit(i + 1).descriptor);
|
|
397
|
+
if (a.length > i) throw n("limit_exceeded");
|
|
398
|
+
for (let n of a) B(n, t), await e.deleteMany(r.tableName, { where: x("id", n.id) });
|
|
399
|
+
}
|
|
400
|
+
await e.updateMany(m.tableName, {
|
|
401
|
+
erased: !0,
|
|
402
|
+
ownerId: null
|
|
403
|
+
}, { where: x("id", I(t)) }), C();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
} catch (e) {
|
|
407
|
+
return g.fail(D(e));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
export { N as dataStoreSaves };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
|
|
3
|
+
/** Opaque saves convey no Wallet, Inventory, ranked-result or Room-state authority. */
|
|
4
|
+
export declare const defaultSaveValidator: SaveValidator;
|
|
5
|
+
|
|
6
|
+
export declare const resolveSavesOptions: (input: SavesOptions) => Readonly<{
|
|
7
|
+
gameId: string;
|
|
8
|
+
slots: readonly string[];
|
|
9
|
+
maxBytes: number;
|
|
10
|
+
historyVersions: number;
|
|
11
|
+
maxRequests: number;
|
|
12
|
+
deleteGraceMs: number;
|
|
13
|
+
version: string;
|
|
14
|
+
validator: SaveValidator;
|
|
15
|
+
now: () => number;
|
|
16
|
+
schema: Schema.Top | undefined;
|
|
17
|
+
schemaVersion: number | null;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export declare const resolveSaveValidation: (context: SaveValidationContext, validator?: SaveValidator) => void;
|
|
21
|
+
|
|
22
|
+
declare type SaveDocument = typeof SaveDocumentSchema.Type;
|
|
23
|
+
|
|
24
|
+
declare const SaveDocumentSchema: Schema.Struct<{
|
|
25
|
+
readonly bytes: Schema.Uint8ArrayFromBase64;
|
|
26
|
+
readonly slot: Schema.String;
|
|
27
|
+
readonly version: Schema.Number;
|
|
28
|
+
readonly schemaVersion: Schema.Number;
|
|
29
|
+
readonly byteLength: Schema.Number;
|
|
30
|
+
readonly checksum: Schema.String;
|
|
31
|
+
readonly deviceId: Schema.String;
|
|
32
|
+
readonly clientVersion: Schema.String;
|
|
33
|
+
readonly savedAt: Schema.Number;
|
|
34
|
+
readonly summary: Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean, Schema.Null]>>;
|
|
35
|
+
readonly deletedUntil: Schema.NullOr<Schema.Number>;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
declare interface SavePutIntent {
|
|
39
|
+
readonly slot: string;
|
|
40
|
+
readonly requestId: string;
|
|
41
|
+
readonly baseVersion: number;
|
|
42
|
+
readonly schemaVersion: number;
|
|
43
|
+
readonly bytes: Uint8Array;
|
|
44
|
+
readonly deviceId: string;
|
|
45
|
+
readonly clientVersion: string;
|
|
46
|
+
readonly summary: SaveSummary;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export declare interface SavesOptions {
|
|
50
|
+
/** Server configuration, never a game/owner selector from the player. */
|
|
51
|
+
readonly gameId: string;
|
|
52
|
+
readonly slots?: readonly string[];
|
|
53
|
+
readonly maxBytes?: number;
|
|
54
|
+
readonly historyVersions?: number;
|
|
55
|
+
readonly maxRequests?: number;
|
|
56
|
+
readonly deleteGraceMs?: number;
|
|
57
|
+
readonly version?: string;
|
|
58
|
+
readonly validator?: SaveValidator;
|
|
59
|
+
readonly schema?: Schema.Top | undefined;
|
|
60
|
+
readonly schemaVersion?: number | undefined;
|
|
61
|
+
readonly now?: (() => number) | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare type SaveSummary = typeof SaveSummarySchema.Type;
|
|
65
|
+
|
|
66
|
+
declare const SaveSummarySchema: Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean, Schema.Null]>>;
|
|
67
|
+
|
|
68
|
+
export declare interface SaveValidationContext {
|
|
69
|
+
readonly previous: SaveDocument | null;
|
|
70
|
+
readonly proposed: SavePutIntent;
|
|
71
|
+
readonly decoded: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export declare type SaveValidator = (context: SaveValidationContext) => boolean;
|
|
75
|
+
|
|
76
|
+
export { }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { savesError as e } from "./errors.js";
|
|
2
|
+
import { saveData as t, saveId as n, saveInteger as r } from "./contract.js";
|
|
3
|
+
import { Schema as i } from "effect";
|
|
4
|
+
// src/strategies.ts
|
|
5
|
+
var a = () => !0, o = (t, n = a) => {
|
|
6
|
+
if (typeof n != "function" || n(t) !== !0) throw e("invalid_save");
|
|
7
|
+
}, s = (o) => {
|
|
8
|
+
let s = t(o, [
|
|
9
|
+
"gameId",
|
|
10
|
+
"slots",
|
|
11
|
+
"maxBytes",
|
|
12
|
+
"historyVersions",
|
|
13
|
+
"maxRequests",
|
|
14
|
+
"deleteGraceMs",
|
|
15
|
+
"version",
|
|
16
|
+
"validator",
|
|
17
|
+
"schema",
|
|
18
|
+
"schemaVersion",
|
|
19
|
+
"now"
|
|
20
|
+
]), c = s.slots ?? ["main"];
|
|
21
|
+
if (!Array.isArray(c) || c.length < 1 || c.length > 64 || Reflect.ownKeys(c).length !== c.length + 1) throw e("invalid_input");
|
|
22
|
+
let l = [];
|
|
23
|
+
for (let t = 0; t < c.length; t++) {
|
|
24
|
+
let r = Object.getOwnPropertyDescriptor(c, String(t));
|
|
25
|
+
if (!r || !("value" in r)) throw e("invalid_input");
|
|
26
|
+
l.push(n(r.value));
|
|
27
|
+
}
|
|
28
|
+
if (new Set(l).size !== l.length) throw e("invalid_input");
|
|
29
|
+
let u = s.validator ?? a, d = s.now ?? Date.now, f = n(s.version ?? "default.v1");
|
|
30
|
+
if (typeof u != "function" || typeof d != "function" || u !== a && f === "default.v1" || s.schema !== void 0 && !i.isSchema(s.schema)) throw e("invalid_input");
|
|
31
|
+
let p = s.schema === void 0 ? null : r(s.schemaVersion, 1);
|
|
32
|
+
if (s.schema === void 0 && s.schemaVersion !== void 0) throw e("invalid_input");
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
gameId: n(s.gameId),
|
|
35
|
+
slots: Object.freeze(l),
|
|
36
|
+
maxBytes: r(s.maxBytes ?? 1048576, 1, 1048576),
|
|
37
|
+
historyVersions: r(s.historyVersions ?? 5, 1, 100),
|
|
38
|
+
maxRequests: r(s.maxRequests ?? 1e4, 1, 1e5),
|
|
39
|
+
deleteGraceMs: r(s.deleteGraceMs ?? 6048e5, 0, 31536e6),
|
|
40
|
+
version: f,
|
|
41
|
+
validator: u,
|
|
42
|
+
now: d,
|
|
43
|
+
schema: s.schema,
|
|
44
|
+
schemaVersion: p
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
export { a as defaultSaveValidator, o as resolveSaveValidation, s as resolveSavesOptions };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { SAVE_HISTORY_TABLE as e, SAVE_REQUEST_TABLE as t, SAVE_SUBJECT_TABLE as n, SAVE_TABLE as r } from "./contract.js";
|
|
2
|
+
import { boolean as i, bytes as a, id as o, integer as s, json as c, table as l, text as u, timestamp as d } from "@voltro/database";
|
|
3
|
+
// src/tables.entity.ts
|
|
4
|
+
var f = () => ({
|
|
5
|
+
tenantId: u().maxLength(120).externalId().sensitive("pii"),
|
|
6
|
+
gameId: u().maxLength(120).serverOnly(),
|
|
7
|
+
ownerId: u().maxLength(120).externalId().sensitive("pii"),
|
|
8
|
+
ownerKey: u().maxLength(64).serverOnly()
|
|
9
|
+
}), p = () => ({
|
|
10
|
+
slot: u().maxLength(120).safe(),
|
|
11
|
+
version: s().safe(),
|
|
12
|
+
schemaVersion: s().safe(),
|
|
13
|
+
bytes: a().sensitive("pii"),
|
|
14
|
+
byteLength: s().safe(),
|
|
15
|
+
checksum: u().maxLength(64).safe(),
|
|
16
|
+
deviceId: u().maxLength(120).sensitive("pii"),
|
|
17
|
+
clientVersion: u().maxLength(120).safe(),
|
|
18
|
+
savedAt: d().safe(),
|
|
19
|
+
summary: c().sensitive("pii"),
|
|
20
|
+
deletedUntil: d().nullable().serverOnly()
|
|
21
|
+
}), m = l(r, {
|
|
22
|
+
id: o({ prefix: "sv" }),
|
|
23
|
+
...f(),
|
|
24
|
+
...p()
|
|
25
|
+
}).index([
|
|
26
|
+
"tenantId",
|
|
27
|
+
"gameId",
|
|
28
|
+
"ownerId",
|
|
29
|
+
"slot"
|
|
30
|
+
]), h = l(e, {
|
|
31
|
+
id: o({ prefix: "svh" }),
|
|
32
|
+
...f(),
|
|
33
|
+
...p()
|
|
34
|
+
}).index([
|
|
35
|
+
"ownerKey",
|
|
36
|
+
"slot",
|
|
37
|
+
"version"
|
|
38
|
+
]), g = l(t, {
|
|
39
|
+
id: o({ prefix: "svr" }),
|
|
40
|
+
...f(),
|
|
41
|
+
digest: u().maxLength(64).serverOnly(),
|
|
42
|
+
receipt: c().sensitive("pii")
|
|
43
|
+
}).index(["ownerKey"]), _ = l(n, {
|
|
44
|
+
id: o({ prefix: "svs" }),
|
|
45
|
+
tenantId: u().maxLength(120).externalId().sensitive("pii"),
|
|
46
|
+
gameId: u().maxLength(120).serverOnly(),
|
|
47
|
+
ownerId: u().maxLength(120).nullable().externalId().sensitive("pii"),
|
|
48
|
+
erased: i().serverOnly(),
|
|
49
|
+
admission: s().serverOnly()
|
|
50
|
+
}).index([
|
|
51
|
+
"tenantId",
|
|
52
|
+
"gameId",
|
|
53
|
+
"ownerId"
|
|
54
|
+
]), v = [
|
|
55
|
+
m,
|
|
56
|
+
h,
|
|
57
|
+
g,
|
|
58
|
+
_
|
|
59
|
+
];
|
|
60
|
+
export { h as savesHistoryTable, g as savesRequestsTable, _ as savesSubjectsTable, m as savesTable, v as savesTables };
|