@zaisma/game-server 1.14.0 → 1.15.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/bench/glass.js +217 -43
- package/package.json +1 -1
package/bench/glass.js
CHANGED
|
@@ -6315,6 +6315,9 @@ var ERROR_CODES = {
|
|
|
6315
6315
|
no_top_up: { httpStatus: 404, meaning: "You have no top-up with that ID.", messageForPeople: "You have no such top-up." },
|
|
6316
6316
|
no_spending_request: { httpStatus: 404, meaning: "You have no spending request with that ID.", messageForPeople: "You have no such spending request." },
|
|
6317
6317
|
top_up_not_refundable: { httpStatus: 409, meaning: "The top-up has nothing left to refund: it's spent, refunded, or over 30 days old.", messageForPeople: "This top-up has nothing left to refund: it's spent, refunded or over 30 days old." },
|
|
6318
|
+
bad_refill_level_cents: { httpStatus: 400, meaning: "refill_level_cents isn't a whole number of cents from 1,000 ($10) to 1,000,000 ($10,000).", messageForPeople: "A monthly plan's level is from $10 to $10,000." },
|
|
6319
|
+
already_credit_plan: { httpStatus: 409, meaning: "The developer already has a monthly credit plan, so there's nothing to start: change its level or stop it.", messageForPeople: "You already have a monthly credit plan." },
|
|
6320
|
+
no_credit_plan: { httpStatus: 409, meaning: "The developer has no monthly credit plan to change or stop.", messageForPeople: "There's no monthly credit plan to change or stop." },
|
|
6318
6321
|
already_pro: { httpStatus: 409, meaning: "The developer already has a Zaisma Pro subscription (running, cancelled until the month ends, or a renewal Stripe is retrying), so there's nothing to start.", messageForPeople: "You already have Zaisma Pro." },
|
|
6319
6322
|
pro_not_refundable: { httpStatus: 409, meaning: "Zaisma Pro's payment can't be refunded: only the first payment, within 14 days of it, and only while it's the only one.", messageForPeople: "Only the first Zaisma Pro payment is refunded, within 14 days of it, while it's the only one." },
|
|
6320
6323
|
not_pro: { httpStatus: 409, meaning: "There's nothing to cancel or resume: the developer has no Zaisma Pro, or it's already cancelled (or isn't cancelled).", messageForPeople: "There's no Zaisma Pro here to cancel or resume." },
|
|
@@ -7466,6 +7469,60 @@ function markSha256(kind, given) {
|
|
|
7466
7469
|
return sha256Hex(new TextEncoder().encode(kind === "email" ? given.trim().toLowerCase() : given));
|
|
7467
7470
|
}
|
|
7468
7471
|
|
|
7472
|
+
// ../core/src/standing.ts
|
|
7473
|
+
var STANDING_LIMITS = {
|
|
7474
|
+
minimumDeveloperAccountAgeDays: 14,
|
|
7475
|
+
minimumReturningPlayerCount: 20,
|
|
7476
|
+
/** Different days a player's plays counted on the developer's games, for the player to be a returning one. */
|
|
7477
|
+
minimumReturningPlayerCountedDayCount: 2,
|
|
7478
|
+
/** A returning player's own account: this old, with plays counted on this many different days on any game. */
|
|
7479
|
+
minimumPlayerAccountAgeDays: 7,
|
|
7480
|
+
minimumPlayerCountedDayCount: 3,
|
|
7481
|
+
/** After a version is rejected or a game taken down, the developer's powers wait this long. */
|
|
7482
|
+
rulingWaitDurationDays: 30,
|
|
7483
|
+
/** A new 16–17 account set to chat with anyone chats only with friends until it's this old, with plays counted on this many different days. */
|
|
7484
|
+
minimumStrangerChatAccountAgeHours: 24,
|
|
7485
|
+
minimumStrangerChatCountedDayCount: 2
|
|
7486
|
+
};
|
|
7487
|
+
var DAY_MS = 864e5;
|
|
7488
|
+
var NO_STANDING = {
|
|
7489
|
+
isEstablishedDeveloper: false,
|
|
7490
|
+
outsideLinksEnabled: false,
|
|
7491
|
+
searchEnginesEnabled: false,
|
|
7492
|
+
returningPlayerCount: 0,
|
|
7493
|
+
accountAgeDays: 0,
|
|
7494
|
+
lastRulingAtMs: null,
|
|
7495
|
+
rulingWaitEndsAtMs: null,
|
|
7496
|
+
hasCardOrStripeAccount: false,
|
|
7497
|
+
isBanned: false
|
|
7498
|
+
};
|
|
7499
|
+
function rulingWaitEndsAt(lastRulingAtMs, nowMs) {
|
|
7500
|
+
if (lastRulingAtMs === null) return null;
|
|
7501
|
+
const endsAtMs = lastRulingAtMs + STANDING_LIMITS.rulingWaitDurationDays * DAY_MS;
|
|
7502
|
+
return endsAtMs > nowMs ? endsAtMs : null;
|
|
7503
|
+
}
|
|
7504
|
+
async function standingOf(database, ownerId, nowMs) {
|
|
7505
|
+
if (!ownerId) return NO_STANDING;
|
|
7506
|
+
const row2 = await database.prepare(
|
|
7507
|
+
`SELECT owners.is_established, owners.is_zaisma_account, owners.returning_player_count, owners.last_ruling_at_ms, owners.created_at_ms AS owner_created_at_ms,
|
|
7508
|
+
owners.stripe_account_id, owners.can_take_stripe_payments, accounts.created_at_ms AS account_created_at_ms, accounts.banned_at_ms,
|
|
7509
|
+
EXISTS (SELECT 1 FROM owner_cards WHERE owner_cards.owner_id = owners.id AND NOT EXISTS (SELECT 1 FROM banned_marks WHERE banned_marks.mark_kind = 'card' AND banned_marks.mark_sha256 = owner_cards.card_fingerprint_sha256)) AS has_card
|
|
7510
|
+
FROM owners LEFT JOIN accounts ON accounts.id = owners.account_id WHERE owners.id = ?`
|
|
7511
|
+
).bind(ownerId).first();
|
|
7512
|
+
if (!row2) return NO_STANDING;
|
|
7513
|
+
const accountAgeDays = Math.max(0, Math.floor((nowMs - (row2.account_created_at_ms ?? row2.owner_created_at_ms)) / DAY_MS));
|
|
7514
|
+
const rulingWaitEndsAtMs = rulingWaitEndsAt(row2.last_ruling_at_ms, nowMs);
|
|
7515
|
+
const hasCardOrStripeAccount = row2.has_card === 1 || row2.stripe_account_id !== null && row2.can_take_stripe_payments === 1;
|
|
7516
|
+
const isBanned = row2.banned_at_ms !== null;
|
|
7517
|
+
const facts2 = { returningPlayerCount: row2.returning_player_count, accountAgeDays, lastRulingAtMs: row2.last_ruling_at_ms, rulingWaitEndsAtMs, hasCardOrStripeAccount, isBanned };
|
|
7518
|
+
if (isBanned || rulingWaitEndsAtMs !== null) return { ...NO_STANDING, ...facts2 };
|
|
7519
|
+
const isEstablishedDeveloper = row2.is_established === 1 || row2.is_zaisma_account === 1;
|
|
7520
|
+
return { ...facts2, isEstablishedDeveloper, outsideLinksEnabled: isEstablishedDeveloper || hasCardOrStripeAccount, searchEnginesEnabled: isEstablishedDeveloper };
|
|
7521
|
+
}
|
|
7522
|
+
async function noteRuling(database, ownerId, nowMs) {
|
|
7523
|
+
await database.prepare("UPDATE owners SET last_ruling_at_ms = ?, is_established = 0 WHERE id = ?").bind(nowMs, ownerId).run();
|
|
7524
|
+
}
|
|
7525
|
+
|
|
7469
7526
|
// ../core/src/blocks.ts
|
|
7470
7527
|
var MAXIMUM_OTHER_PLAYER_COUNT = 100;
|
|
7471
7528
|
|
|
@@ -7714,6 +7771,9 @@ var SECURITY_EVENT_DESCRIPTIONS = {
|
|
|
7714
7771
|
where_used_from_turned_on: "Turned on Keep where my account is used from",
|
|
7715
7772
|
credit_top_up_started: "Started a credit top-up",
|
|
7716
7773
|
credit_refunded: "Refunded hosting credit",
|
|
7774
|
+
credit_plan_started: "Started a monthly credit plan",
|
|
7775
|
+
credit_plan_changed: "Changed the monthly credit plan's level",
|
|
7776
|
+
credit_plan_stopped: "Stopped the monthly credit plan",
|
|
7717
7777
|
pro_checkout_started: "Started Zaisma Pro",
|
|
7718
7778
|
pro_cancelled: "Cancelled Zaisma Pro",
|
|
7719
7779
|
pro_resumed: "Took back cancelling Zaisma Pro",
|
|
@@ -7796,6 +7856,9 @@ var ACCOUNT_EVENT_KINDS = [
|
|
|
7796
7856
|
"account_data_downloaded",
|
|
7797
7857
|
"credit_top_up_started",
|
|
7798
7858
|
"credit_refunded",
|
|
7859
|
+
"credit_plan_started",
|
|
7860
|
+
"credit_plan_changed",
|
|
7861
|
+
"credit_plan_stopped",
|
|
7799
7862
|
"pro_checkout_started",
|
|
7800
7863
|
"pro_cancelled",
|
|
7801
7864
|
"pro_resumed",
|
|
@@ -7866,12 +7929,13 @@ var TRUSTED_LINK_DOMAINS = [
|
|
|
7866
7929
|
var linksOf = (game) => JSON.parse(game.links || "[]");
|
|
7867
7930
|
var isUnder = (host, domain) => host === domain || host.endsWith(`.${domain}`);
|
|
7868
7931
|
var isTrustedLinkHost = (host) => TRUSTED_LINK_DOMAINS.some((domain) => isUnder(host, domain));
|
|
7869
|
-
function linkAudience(link) {
|
|
7932
|
+
function linkAudience(link, outsideLinksEnabled) {
|
|
7870
7933
|
if (link.review_reason) return "nobody";
|
|
7871
|
-
|
|
7934
|
+
if (isTrustedLinkHost(new URL(link.destination_url).hostname)) return "everyone";
|
|
7935
|
+
return outsideLinksEnabled ? "signed_in_adults" : "waiting_for_standing";
|
|
7872
7936
|
}
|
|
7873
|
-
function canOpenLink(link, isSignedInAdult) {
|
|
7874
|
-
const audience = linkAudience(link);
|
|
7937
|
+
function canOpenLink(link, isSignedInAdult, outsideLinksEnabled) {
|
|
7938
|
+
const audience = linkAudience(link, outsideLinksEnabled);
|
|
7875
7939
|
return audience === "everyone" || audience === "signed_in_adults" && isSignedInAdult;
|
|
7876
7940
|
}
|
|
7877
7941
|
var BRAND_DOMAINS = [
|
|
@@ -8464,7 +8528,7 @@ var BROTLI_LIMITS = {
|
|
|
8464
8528
|
var MAXIMUM_DATABASE_SIZE_BYTES = 10 * 1024 ** 3;
|
|
8465
8529
|
|
|
8466
8530
|
// ../core/src/hosting.ts
|
|
8467
|
-
var
|
|
8531
|
+
var DAY_MS2 = 24 * 36e5;
|
|
8468
8532
|
var USAGE_FIELDS = {
|
|
8469
8533
|
room_play_duration_sec: "roomPlayDurationSec",
|
|
8470
8534
|
room_awake_duration_sec: "roomAwakeDurationSec",
|
|
@@ -8686,12 +8750,12 @@ function readCron(cron) {
|
|
|
8686
8750
|
return { cronTimes };
|
|
8687
8751
|
}
|
|
8688
8752
|
var SEARCH_DAY_COUNT = 8 * 366;
|
|
8689
|
-
var
|
|
8753
|
+
var DAY_MS3 = 864e5;
|
|
8690
8754
|
function nextAtMs(cronTimes, afterMs) {
|
|
8691
8755
|
const firstMs = Math.floor(afterMs / 6e4) * 6e4 + 6e4;
|
|
8692
|
-
const firstDayMs = Math.floor(firstMs /
|
|
8756
|
+
const firstDayMs = Math.floor(firstMs / DAY_MS3) * DAY_MS3;
|
|
8693
8757
|
for (let dayIndex = 0; dayIndex < SEARCH_DAY_COUNT; dayIndex++) {
|
|
8694
|
-
const dayMs = firstDayMs + dayIndex *
|
|
8758
|
+
const dayMs = firstDayMs + dayIndex * DAY_MS3;
|
|
8695
8759
|
const date2 = new Date(dayMs);
|
|
8696
8760
|
if (!cronTimes.months[date2.getUTCMonth() + 1]) continue;
|
|
8697
8761
|
const isMonthDay = cronTimes.monthDays[date2.getUTCDate()];
|
|
@@ -26292,7 +26356,7 @@ var MAXIMUM_START_DELAY_DAYS = 90;
|
|
|
26292
26356
|
var START_GRACE_DURATION_MS = 36e5;
|
|
26293
26357
|
var ENDED_SHOWN_DURATION_DAYS = 30;
|
|
26294
26358
|
var DEFAULT_LINK_LABEL = "Read more";
|
|
26295
|
-
var
|
|
26359
|
+
var DAY_MS4 = 864e5;
|
|
26296
26360
|
var shownLinkLabel = (announcement) => announcement.link_url ? announcement.link_label ?? DEFAULT_LINK_LABEL : null;
|
|
26297
26361
|
async function currentAnnouncements(env2, nowMs) {
|
|
26298
26362
|
const { results } = await env2.DB.prepare("SELECT * FROM announcements WHERE ends_at_ms > ? AND starts_at_ms <= ? ORDER BY starts_at_ms DESC, created_at_ms DESC LIMIT 20").bind(nowMs, nowMs).all();
|
|
@@ -26304,7 +26368,7 @@ function announcementStatus(announcement, nowMs) {
|
|
|
26304
26368
|
}
|
|
26305
26369
|
async function listAnnouncements(env2) {
|
|
26306
26370
|
const nowMs = Date.now();
|
|
26307
|
-
const { results } = await env2.DB.prepare("SELECT * FROM announcements WHERE ends_at_ms > ? ORDER BY starts_at_ms DESC, created_at_ms DESC LIMIT 100").bind(nowMs - ENDED_SHOWN_DURATION_DAYS *
|
|
26371
|
+
const { results } = await env2.DB.prepare("SELECT * FROM announcements WHERE ends_at_ms > ? ORDER BY starts_at_ms DESC, created_at_ms DESC LIMIT 100").bind(nowMs - ENDED_SHOWN_DURATION_DAYS * DAY_MS4).all();
|
|
26308
26372
|
const [shown] = await currentAnnouncements(env2, nowMs);
|
|
26309
26373
|
return { announcements: results, shownAnnouncementId: shown?.id ?? null, nowMs };
|
|
26310
26374
|
}
|
|
@@ -26317,7 +26381,7 @@ function readTime(given, field) {
|
|
|
26317
26381
|
const atMs = typeof given === "string" && ISO_TIME.test(given) ? Date.parse(given) : NaN;
|
|
26318
26382
|
if (!Number.isFinite(atMs)) {
|
|
26319
26383
|
const said = typeof given === "string" ? `"${given.slice(0, 40)}"` : `a ${typeof given}`;
|
|
26320
|
-
throw new ApiError("bad_time", { message: `${field} is ${said}, not an ISO time.`, field, fix: `Give an ISO time with Z or an offset, like "${new Date(Date.now() + 7 *
|
|
26384
|
+
throw new ApiError("bad_time", { message: `${field} is ${said}, not an ISO time.`, field, fix: `Give an ISO time with Z or an offset, like "${new Date(Date.now() + 7 * DAY_MS4).toISOString().slice(0, 14)}00:00Z".` });
|
|
26321
26385
|
}
|
|
26322
26386
|
return atMs;
|
|
26323
26387
|
}
|
|
@@ -26356,11 +26420,11 @@ async function postAnnouncement(env2, toPost, actor) {
|
|
|
26356
26420
|
throw new ApiError("bad_link_label", { message: `link_label is ${linkLabel.length} characters, not 1 to ${MAXIMUM_LINK_LABEL_CHARACTER_COUNT}.`, field: "link_label", fix: `Give 1 to ${MAXIMUM_LINK_LABEL_CHARACTER_COUNT} characters, like "See what's new", or leave it out for "${DEFAULT_LINK_LABEL}".` });
|
|
26357
26421
|
}
|
|
26358
26422
|
const startsAtMs = toPost.startsAt === void 0 ? nowMs : readTime(toPost.startsAt, "starts_at");
|
|
26359
|
-
if (startsAtMs < nowMs - START_GRACE_DURATION_MS || startsAtMs > nowMs + MAXIMUM_START_DELAY_DAYS *
|
|
26423
|
+
if (startsAtMs < nowMs - START_GRACE_DURATION_MS || startsAtMs > nowMs + MAXIMUM_START_DELAY_DAYS * DAY_MS4) {
|
|
26360
26424
|
throw new ApiError("bad_time", { message: `starts_at ${iso(startsAtMs)} isn't from an hour ago to ${MAXIMUM_START_DELAY_DAYS} days ahead.`, field: "starts_at", fix: "Leave starts_at out to show it now, or give a time up to 90 days ahead." });
|
|
26361
26425
|
}
|
|
26362
26426
|
const endsAtMs = readTime(toPost.endsAt, "ends_at");
|
|
26363
|
-
const latestEndAtMs = startsAtMs + MAXIMUM_ANNOUNCEMENT_DURATION_DAYS *
|
|
26427
|
+
const latestEndAtMs = startsAtMs + MAXIMUM_ANNOUNCEMENT_DURATION_DAYS * DAY_MS4;
|
|
26364
26428
|
if (endsAtMs <= startsAtMs || endsAtMs <= nowMs || endsAtMs > latestEndAtMs) {
|
|
26365
26429
|
throw new ApiError("bad_time", {
|
|
26366
26430
|
message: endsAtMs > latestEndAtMs ? `ends_at ${iso(endsAtMs)} is more than ${MAXIMUM_ANNOUNCEMENT_DURATION_DAYS} days after starts_at.` : `ends_at ${iso(endsAtMs)} isn't after ${endsAtMs <= nowMs ? "now" : "starts_at"}.`,
|
|
@@ -27588,6 +27652,12 @@ function encodeHeader(text4) {
|
|
|
27588
27652
|
return `=?UTF-8?B?${b64}?=`;
|
|
27589
27653
|
}
|
|
27590
27654
|
|
|
27655
|
+
// ../../apps/site/src/standing.ts
|
|
27656
|
+
async function noteRulingOnGame(env2, gameId, nowMs = Date.now()) {
|
|
27657
|
+
const game = await env2.DB.prepare("SELECT owner_id FROM games WHERE id = ?").bind(gameId).first();
|
|
27658
|
+
if (game?.owner_id) await noteRuling(env2.DB, game.owner_id, nowMs);
|
|
27659
|
+
}
|
|
27660
|
+
|
|
27591
27661
|
// ../../apps/site/src/upload.ts
|
|
27592
27662
|
async function deletePrefix(bucket, prefix) {
|
|
27593
27663
|
let cursor;
|
|
@@ -27824,7 +27894,7 @@ async function reviewPrecedentAction(env2, path, form, actor) {
|
|
|
27824
27894
|
var CODE_DURATION_MIN = 10;
|
|
27825
27895
|
var CODE_DURATION_MS = CODE_DURATION_MIN * 6e4;
|
|
27826
27896
|
var HOUR_MS4 = 36e5;
|
|
27827
|
-
var
|
|
27897
|
+
var DAY_MS5 = 24 * HOUR_MS4;
|
|
27828
27898
|
async function deliverEmail(env2, email, message2) {
|
|
27829
27899
|
try {
|
|
27830
27900
|
if (env2.EMAIL_OUTBOX_URL) {
|
|
@@ -27910,6 +27980,7 @@ async function stopCopy(env2, version, actor) {
|
|
|
27910
27980
|
const rejectedReason = decidedReason("rejected_by_zaisma", "copyright", "A re-upload of another developer's game.");
|
|
27911
27981
|
await saveScan(env2.DB, version.id, "rejected", null);
|
|
27912
27982
|
await env2.DB.prepare("UPDATE versions SET review_reason = ? WHERE id = ?").bind(JSON.stringify(rejectedReason), version.id).run();
|
|
27983
|
+
await noteRulingOnGame(env2, version.game_id);
|
|
27913
27984
|
await audit(env2.DB, version.game_id, "rejected", `${version.id}: a re-upload of another developer's game`, actor, rejectedReason);
|
|
27914
27985
|
await tellWebhooks(env2, "scan_status_changed", version.game_id, { versionId: version.id });
|
|
27915
27986
|
const original = await gameById(env2.DB, report.matched_game.game_id);
|
|
@@ -46696,9 +46767,9 @@ function previewLinkToken(version, nowMs = Date.now()) {
|
|
|
46696
46767
|
}
|
|
46697
46768
|
|
|
46698
46769
|
// ../../apps/site/src/performance-check.ts
|
|
46699
|
-
var
|
|
46770
|
+
var DAY_MS6 = 24 * 3600 * 1e3;
|
|
46700
46771
|
async function dailyCheckCount(env2, ownerId, nowMs = Date.now()) {
|
|
46701
|
-
const counted = await env2.DB.prepare("SELECT COUNT(*) AS check_count FROM performance_checks WHERE owner_id = ? AND queued_at_ms >= ? AND check_status != 'over_daily_limit'").bind(ownerId, nowMs - nowMs %
|
|
46772
|
+
const counted = await env2.DB.prepare("SELECT COUNT(*) AS check_count FROM performance_checks WHERE owner_id = ? AND queued_at_ms >= ? AND check_status != 'over_daily_limit'").bind(ownerId, nowMs - nowMs % DAY_MS6).first();
|
|
46702
46773
|
return counted?.check_count ?? 0;
|
|
46703
46774
|
}
|
|
46704
46775
|
async function queuePerformanceCheck(env2, version) {
|
|
@@ -46796,9 +46867,9 @@ Keep the summary and why short and plain.`;
|
|
|
46796
46867
|
|
|
46797
46868
|
// ../../apps/site/src/security-alerts.ts
|
|
46798
46869
|
var HOUR_MS6 = 36e5;
|
|
46799
|
-
var
|
|
46870
|
+
var DAY_MS7 = 24 * HOUR_MS6;
|
|
46800
46871
|
async function securitySpikesAnswer(env2, nowMs = Date.now()) {
|
|
46801
|
-
const { results } = await env2.DB.prepare("SELECT * FROM security_spikes WHERE alerted_at_ms IS NOT NULL AND last_happened_at_ms > ? ORDER BY hour_number DESC, occurrence_count DESC LIMIT 100").bind(nowMs - SECURITY_SIGNAL_KEPT_DURATION_DAYS *
|
|
46872
|
+
const { results } = await env2.DB.prepare("SELECT * FROM security_spikes WHERE alerted_at_ms IS NOT NULL AND last_happened_at_ms > ? ORDER BY hour_number DESC, occurrence_count DESC LIMIT 100").bind(nowMs - SECURITY_SIGNAL_KEPT_DURATION_DAYS * DAY_MS7).all();
|
|
46802
46873
|
return {
|
|
46803
46874
|
security_spikes: results.map((row2) => ({
|
|
46804
46875
|
spike_kind: row2.spike_kind,
|
|
@@ -46814,7 +46885,7 @@ async function blockedScriptsAnswer(env2, nowMs = Date.now()) {
|
|
|
46814
46885
|
const { results } = await env2.DB.prepare(
|
|
46815
46886
|
`SELECT violated_directive, blocked_source, page_path, SUM(occurrence_count) AS occurrence_count, MIN(first_reported_at_ms) AS first_reported_at_ms, MAX(last_reported_at_ms) AS last_reported_at_ms
|
|
46816
46887
|
FROM blocked_scripts WHERE last_reported_at_ms > ? GROUP BY violated_directive, blocked_source, page_path ORDER BY occurrence_count DESC LIMIT 100`
|
|
46817
|
-
).bind(nowMs - 7 *
|
|
46888
|
+
).bind(nowMs - 7 * DAY_MS7).all();
|
|
46818
46889
|
return {
|
|
46819
46890
|
blocked_scripts: results.map((row2) => ({
|
|
46820
46891
|
violated_directive: row2.violated_directive,
|
|
@@ -46828,9 +46899,9 @@ async function blockedScriptsAnswer(env2, nowMs = Date.now()) {
|
|
|
46828
46899
|
}
|
|
46829
46900
|
|
|
46830
46901
|
// ../../apps/site/src/device-marks.ts
|
|
46831
|
-
var
|
|
46902
|
+
var DAY_MS8 = 864e5;
|
|
46832
46903
|
var MARK_DURATION_DAYS = 365;
|
|
46833
|
-
var MARK_DURATION_MS = MARK_DURATION_DAYS *
|
|
46904
|
+
var MARK_DURATION_MS = MARK_DURATION_DAYS * DAY_MS8;
|
|
46834
46905
|
|
|
46835
46906
|
// ../../apps/site/src/leaked-keys.ts
|
|
46836
46907
|
var MAXIMUM_GITHUB_BODY_SIZE_BYTES = 1024 * 1024;
|
|
@@ -47616,6 +47687,9 @@ var API_ROUTES = [
|
|
|
47616
47687
|
call({ method: "POST", path: "/pro/resume", reaches: "account", access: needs("credit_access", "write"), isAccountWide: true }),
|
|
47617
47688
|
call({ method: "POST", path: "/pro/refund", reaches: "account", access: needs("credit_access", "write"), isAccountWide: true }),
|
|
47618
47689
|
call({ method: "POST", path: "/credit/refunds", reaches: "account", access: needs("credit_access", "write"), isAccountWide: true, body: { inputs: plainInputs("top_up_id") } }),
|
|
47690
|
+
call({ method: "POST", path: "/credit/plan/checkout", reaches: "account", access: needs("credit_access", "write"), isAccountWide: true, body: { inputs: plainInputs("refill_level_cents") } }),
|
|
47691
|
+
call({ method: "PATCH", path: "/credit/plan", reaches: "account", access: needs("credit_access", "write"), isAccountWide: true, body: { inputs: plainInputs("refill_level_cents") } }),
|
|
47692
|
+
call({ method: "POST", path: "/credit/plan/stop", reaches: "account", access: needs("credit_access", "write"), isAccountWide: true }),
|
|
47619
47693
|
call({ method: "GET", path: "/stripe-account", reaches: "account", access: needs("stripe_account_access", "read"), isAccountWide: true }),
|
|
47620
47694
|
call({ method: "POST", path: "/stripe-account/connection-links", reaches: "account", access: needs("stripe_account_access", "write"), isAccountWide: true }),
|
|
47621
47695
|
call({ method: "PATCH", path: "/stripe-account", reaches: "account", access: needs("stripe_account_access", "write"), isAccountWide: true, body: { inputs: plainInputs("is_stripe_tax_used") } }),
|
|
@@ -47734,6 +47808,7 @@ function findingsFor(report) {
|
|
|
47734
47808
|
}
|
|
47735
47809
|
var splitLabels = (labels) => labels ? labels.split(",") : [];
|
|
47736
47810
|
async function gameJson(env2, game) {
|
|
47811
|
+
const { outsideLinksEnabled } = await standingOf(env2.DB, game.owner_id, Date.now());
|
|
47737
47812
|
const waitingWords = game.details_waiting_for_review ? JSON.parse(game.details_waiting_for_review) : null;
|
|
47738
47813
|
return {
|
|
47739
47814
|
slug: game.slug,
|
|
@@ -47763,7 +47838,7 @@ async function gameJson(env2, game) {
|
|
|
47763
47838
|
minimum_player_age_years: game.minimum_player_age_years,
|
|
47764
47839
|
game_server_allowed_hosts: allowedHostsOf(game),
|
|
47765
47840
|
own_server_urls: ownServerUrlsOf(game),
|
|
47766
|
-
links: linksOf(game).map(linkJson),
|
|
47841
|
+
links: linksOf(game).map((link) => linkJson(link, outsideLinksEnabled)),
|
|
47767
47842
|
details_waiting_for_review: waitingWords ? { title: waitingWords.title ?? null, description: waitingWords.description ?? null } : null,
|
|
47768
47843
|
// Why the title keeps the game off Zaisma's lists ("No slop"); null when nothing does.
|
|
47769
47844
|
title_listing_reason: reasonAnswer(game.title_listing_reason),
|
|
@@ -47780,12 +47855,12 @@ async function gameJson(env2, game) {
|
|
|
47780
47855
|
updated_at: iso(game.updated_at_ms)
|
|
47781
47856
|
};
|
|
47782
47857
|
}
|
|
47783
|
-
function linkJson(link) {
|
|
47858
|
+
function linkJson(link, outsideLinksEnabled) {
|
|
47784
47859
|
return {
|
|
47785
47860
|
link_name: link.link_name,
|
|
47786
47861
|
destination_url: link.destination_url,
|
|
47787
47862
|
is_on_game_page: link.is_on_game_page,
|
|
47788
|
-
who_can_open: linkAudience(link),
|
|
47863
|
+
who_can_open: linkAudience(link, outsideLinksEnabled),
|
|
47789
47864
|
review_reason: link.review_reason ? reviewReasonAnswer(link.review_reason) : null
|
|
47790
47865
|
};
|
|
47791
47866
|
}
|
|
@@ -47822,7 +47897,7 @@ async function tellWebhooks(env2, kind, gameId, parts2, waitUntil) {
|
|
|
47822
47897
|
console.error("Couldn't tell webhooks", kind, gameId, error);
|
|
47823
47898
|
}
|
|
47824
47899
|
}
|
|
47825
|
-
var
|
|
47900
|
+
var DAY_MS9 = 24 * 3600 * 1e3;
|
|
47826
47901
|
async function sendAll(env2, webhooks, deliveries) {
|
|
47827
47902
|
const sendToGameServer = gameServerSender(env2);
|
|
47828
47903
|
const tries = await Promise.allSettled(deliveries.map((delivery, i) => attemptDelivery(env2.DB, webhooks[i], delivery, { sendToGameServer })));
|
|
@@ -50036,7 +50111,7 @@ function gamePageResponse(env2, view) {
|
|
|
50036
50111
|
...purchaseLabel.categories.length && refundWords ? [{ label: "Refunds", text: refundWords }] : []
|
|
50037
50112
|
];
|
|
50038
50113
|
const isSignedInAdult = viewer.account?.age_band === "18+";
|
|
50039
|
-
const pageLinks = linksOf(game).filter((link) => link.is_on_game_page && canOpenLink(link, isSignedInAdult));
|
|
50114
|
+
const pageLinks = linksOf(game).filter((link) => link.is_on_game_page && canOpenLink(link, isSignedInAdult, view.outsideLinksEnabled));
|
|
50040
50115
|
const gameOrigin = new URL(frameUrl).origin;
|
|
50041
50116
|
const testerWords = tester ? `You're testing this game with a link from its developer.${isTestBuild ? ` It opens a version players don't have yet${game.multiplayer_mode !== "none" || ownServers.length ? ", so online play is off: it works only on the version players have" : ""}.` : ""}${hidden ? " It isn't public yet: anyone with this link can play, so only share it with other testers." : ""} Your progress stays in this browser.${tester.ends_at_ms ? ` The link works until ${day(tester.ends_at_ms)}.` : ""}` : "";
|
|
50042
50117
|
const reportHref = `/g/${game.slug}/report`;
|
|
@@ -50256,7 +50331,7 @@ ${isClosed ? scriptTags(GAME_PAGE_SCRIPT, GAME_EMBEDDED_SCRIPT) : scriptTags(
|
|
|
50256
50331
|
)}`
|
|
50257
50332
|
});
|
|
50258
50333
|
response.headers.append("Vary", "Sec-Fetch-Dest, Sec-Fetch-Site");
|
|
50259
|
-
if (!view.isShownInLists || tester) response.headers.set("X-Robots-Tag", "noindex");
|
|
50334
|
+
if (!view.isShownInLists || !view.searchEnginesEnabled || tester) response.headers.set("X-Robots-Tag", "noindex");
|
|
50260
50335
|
return response;
|
|
50261
50336
|
}
|
|
50262
50337
|
var SECURITY_PAGE = `
|
|
@@ -51681,7 +51756,7 @@ var LINK = object({
|
|
|
51681
51756
|
destination_url: url("The https address it opens; the game can add a path and query under it."),
|
|
51682
51757
|
is_on_game_page: yes("The game page shows it too."),
|
|
51683
51758
|
who_can_open: choice(
|
|
51684
|
-
{ everyone: "it's on a trusted store, payment or video site, so it opens for guests and under-18 accounts too", signed_in_adults: `it opens for signed-in adults; anyone else sees "This link can't be opened here."`, nobody: "its site is listed for malware or phishing (review_reason says so)" },
|
|
51759
|
+
{ everyone: "it's on a trusted store, payment or video site, so it opens for guests and under-18 accounts too", signed_in_adults: `it opens for signed-in adults; anyone else sees "This link can't be opened here."`, waiting_for_standing: "a site off the trusted list that opens for signed-in adults once your account is established, or has a card that paid Zaisma or a Stripe account (get_access's standing); until then it opens for nobody", nobody: "its site is listed for malware or phishing (review_reason says so)" },
|
|
51685
51760
|
"Who it opens for."
|
|
51686
51761
|
),
|
|
51687
51762
|
review_reason: nullable(reviewReason2("Why it opens for nobody; null when it opens."))
|
|
@@ -51982,7 +52057,7 @@ var ACCESS = object({
|
|
|
51982
52057
|
sign_in_app_access: choice(AREA_ACCESS_CHOICES.sign_in_app_access, "Sign-in apps; none for keys limited to some games."),
|
|
51983
52058
|
webhook_access: choice(AREA_ACCESS_CHOICES.webhook_access, "Webhooks; none for keys limited to some games."),
|
|
51984
52059
|
stripe_account_access: choice(AREA_ACCESS_CHOICES.stripe_account_access, "The developer's connected Stripe account for the buy sheet; none for keys limited to some games. read: the connection. write: also ask for a link to connect (the developer connects with their passkey on zaisma.com), turn Stripe Tax on or off, and disconnect."),
|
|
51985
|
-
credit_access: choice(AREA_ACCESS_CHOICES.credit_access, "Hosting credit; none for keys limited to some games. read: balance, top-ups and spending. write: also start a top-up, refund one, and turn spending off or down (on or up needs the developer's passkey on zaisma.com). Also Zaisma Pro: read the plan; write: start, cancel or resume it.")
|
|
52060
|
+
credit_access: choice(AREA_ACCESS_CHOICES.credit_access, "Hosting credit; none for keys limited to some games. read: balance, top-ups and spending. write: also start a top-up, refund one, and turn spending off or down (on or up needs the developer's passkey on zaisma.com). Also the monthly credit plan: write starts it, changes its level (higher needs the passkey) and stops it. And Zaisma Pro: read the plan; write: start, cancel or resume it.")
|
|
51986
52061
|
},
|
|
51987
52062
|
"What the key may do in each area."
|
|
51988
52063
|
),
|
|
@@ -51997,6 +52072,22 @@ var NEXT = list(
|
|
|
51997
52072
|
}),
|
|
51998
52073
|
"The calls that usually follow; empty when nothing does."
|
|
51999
52074
|
);
|
|
52075
|
+
var STANDING = object(
|
|
52076
|
+
{
|
|
52077
|
+
is_established_developer: yes("Whether the account is established: 14 days old, 20 different returning players on its games, and nothing ruled against it in 30 days. Zaisma's own accounts are established from the start."),
|
|
52078
|
+
outside_links_enabled: yes("Whether the games' links to sites outside the trusted list open (for signed-in adults): once established, or at once with a card that paid Zaisma or a Stripe account that can take payments. Until then those links answer who_can_open waiting_for_standing."),
|
|
52079
|
+
search_engines_enabled: yes("Whether search engines find the games (the sitemap, and no noindex on their pages): only once established. Zaisma's own lists show listed games from the first day either way."),
|
|
52080
|
+
returning_player_count: countOf("Different players who came back to your games on 2 days or more, each with an account at least 7 days old that played on 3 days, as the daily count last counted them. Your own account never counts."),
|
|
52081
|
+
minimum_returning_player_count: countOf("Returning players needed to be established: 20."),
|
|
52082
|
+
account_age_days: countOf("Whole days since the account was made."),
|
|
52083
|
+
minimum_account_age_days: countOf("Days old the account must be to be established: 14."),
|
|
52084
|
+
has_card_or_stripe_account: yes("Whether a card paid Zaisma (a top-up or Zaisma Pro) on this account, or its Stripe account can take payments: either opens links to other sites at once."),
|
|
52085
|
+
last_ruling_at: nullable(isoTime("When Zaisma last rejected one of your versions or took a game down; null when never.")),
|
|
52086
|
+
ruling_wait_ends_at: nullable(isoTime("While the 30 days after the last ruling run, when they end: until then links to other sites and search engines are closed. null when there's no wait.")),
|
|
52087
|
+
message_for_people: text2("What's open and what opens next, in a sentence or two, to show the developer.")
|
|
52088
|
+
},
|
|
52089
|
+
"What the account has earned, and what each power needs. Nobody else sees it."
|
|
52090
|
+
);
|
|
52000
52091
|
var ERROR_FIELDS = object({
|
|
52001
52092
|
code: text2("Stable, one problem per code; docs/DEVELOPER-API.md lists every code."),
|
|
52002
52093
|
message: text2("What happened."),
|
|
@@ -52140,11 +52231,22 @@ var HOSTING_USE = object(
|
|
|
52140
52231
|
},
|
|
52141
52232
|
"This month (UTC) on the game's one hosting meter: the free allowance as a percentage, what used it, and what credit paid past it."
|
|
52142
52233
|
);
|
|
52234
|
+
var CREDIT_PLAN = object(
|
|
52235
|
+
{
|
|
52236
|
+
refill_level_cents: countOf("What credit is filled back to on the 1st of every month (UTC), in cents (4000 is $40)."),
|
|
52237
|
+
plan_status: choice({ running: "Stripe is paid; the next 1st refills", payment_failing: "the card was declined, and Stripe is trying again over the next days" }),
|
|
52238
|
+
next_payment_at: isoTime("The next 1st, 00:00 UTC, when the refill is paid."),
|
|
52239
|
+
next_payment_cents: countOf("What the next 1st pays at today's balance: the level minus the balance, 0 when that's under $1 or the balance is at or over the level. Sales tax or VAT comes on top on Stripe's side."),
|
|
52240
|
+
started_at: isoTime()
|
|
52241
|
+
},
|
|
52242
|
+
"The monthly credit plan (start_credit_plan); null with none."
|
|
52243
|
+
);
|
|
52143
52244
|
var CREDIT_ENTRY_KIND = choice({ top_up: "credit bought", hosting_charge: "a game's hosting past its free allowance, one entry per game per day", refund: "credit refunded to its card", written_off: "what a game used past its budget, your credit, or its free hosting while it had no budget, which Zaisma took on: never charged" });
|
|
52144
52245
|
var CREDIT = object({
|
|
52145
52246
|
credit: object({
|
|
52146
52247
|
balance_cents: countOf("What's left to spend, in cents. Never below 0."),
|
|
52147
52248
|
is_credit_low: yes("Whether the balance is under 20% of what there was right after the last top-up: add credit. Each game with a budget shows a credit_low alert meanwhile."),
|
|
52249
|
+
credit_plan: nullable(CREDIT_PLAN),
|
|
52148
52250
|
top_ups: list(
|
|
52149
52251
|
object({
|
|
52150
52252
|
id: idOf("top_up"),
|
|
@@ -52162,17 +52264,16 @@ var CREDIT = object({
|
|
|
52162
52264
|
)
|
|
52163
52265
|
})
|
|
52164
52266
|
});
|
|
52165
|
-
var
|
|
52166
|
-
|
|
52167
|
-
};
|
|
52267
|
+
var BUDGET_CHANGE = object({ monthly_budget_cents: nullable(countOf("A game's monthly budget; null: no paid hosting.")) }, "game_budget's");
|
|
52268
|
+
var PLAN_LEVEL_CHANGE = object({ refill_level_cents: nullable(countOf("The monthly credit plan's level; null: no plan.")) }, "credit_plan_refill_level's");
|
|
52168
52269
|
var SPENDING_REQUEST = object({
|
|
52169
52270
|
spending_request: object({
|
|
52170
52271
|
id: idOf("spending_request"),
|
|
52171
|
-
request_kind: choice({ game_budget: "a game's monthly budget" }),
|
|
52172
|
-
game_slug: nullable(text2("The game; null once it's deleted.")),
|
|
52272
|
+
request_kind: choice({ game_budget: "a game's monthly budget", credit_plan_refill_level: "the monthly credit plan's level" }),
|
|
52273
|
+
game_slug: nullable(text2("The game; null once it's deleted, and for the credit plan's level.")),
|
|
52173
52274
|
game_title: nullable(text2()),
|
|
52174
|
-
requested_change:
|
|
52175
|
-
current_setting:
|
|
52275
|
+
requested_change: { anyOf: [BUDGET_CHANGE, PLAN_LEVEL_CHANGE], description: "The change asked for: monthly_budget_cents for a game's budget, refill_level_cents for the credit plan's level." },
|
|
52276
|
+
current_setting: { anyOf: [BUDGET_CHANGE, PLAN_LEVEL_CHANGE], description: "The same field as it is now." },
|
|
52176
52277
|
request_status: choice({ waiting: "the developer hasn't answered", approved: "approved: the change is made", declined: "the developer declined", expired: "a day passed without an answer" }),
|
|
52177
52278
|
created_at: isoTime(),
|
|
52178
52279
|
expires_at: isoTime("A day after it was asked."),
|
|
@@ -52212,6 +52313,12 @@ var PRO_CHECKOUT = object({
|
|
|
52212
52313
|
expires_at: isoTime("When the page stops taking payment."),
|
|
52213
52314
|
next_steps: NEXT
|
|
52214
52315
|
});
|
|
52316
|
+
var CREDIT_PLAN_CHECKOUT = object({
|
|
52317
|
+
checkout_url: url("Stripe's page where the developer pays. Only a person can pay there."),
|
|
52318
|
+
expires_at: isoTime("When the page stops taking payment."),
|
|
52319
|
+
next_steps: NEXT
|
|
52320
|
+
});
|
|
52321
|
+
var CREDIT_CHANGED = object({ ...CREDIT.properties, next_steps: NEXT });
|
|
52215
52322
|
var TOP_UP_CHECKOUT = object({
|
|
52216
52323
|
checkout_url: url("Stripe's page where the developer pays. Only a person can pay there."),
|
|
52217
52324
|
expires_at: isoTime("When the page stops taking payment."),
|
|
@@ -53374,6 +53481,11 @@ var BLOCKED_SCRIPT_LIST = object({
|
|
|
53374
53481
|
"The last 7 days, most reported first, up to 100. Browser extensions' reports aren't kept."
|
|
53375
53482
|
)
|
|
53376
53483
|
});
|
|
53484
|
+
var ZAISMA_ACCOUNT_CHANGED = object({
|
|
53485
|
+
username: text2(),
|
|
53486
|
+
is_zaisma_account: yes("Whether it's one of Zaisma's own accounts now, established from the start (links to other sites and search engines open)."),
|
|
53487
|
+
next_steps: NEXT
|
|
53488
|
+
});
|
|
53377
53489
|
var CAN_PUBLISH_CHANGED = object({
|
|
53378
53490
|
username: text2(),
|
|
53379
53491
|
can_publish: yes("Whether the account can make, upload and publish games now."),
|
|
@@ -53565,7 +53677,7 @@ var details = {
|
|
|
53565
53677
|
required: ["link_name", "destination_url", "is_on_game_page"],
|
|
53566
53678
|
additionalProperties: false
|
|
53567
53679
|
},
|
|
53568
|
-
description: `The game's registered links: the sites it opens with Zaisma.links.open(link_name), like its store, Patreon, Steam page or Discord, behind Zaisma's "You're leaving Zaisma" card over the game; the game can open no other sites. The whole list each time, up to 20; [] for none. The game can add a path and query under a link's address: Zaisma.links.open("store", { path: "/buy", queryParameters: { item: "sword" } }). Signed-in adults open any link; guests and under-18 accounts only links on trusted store, payment and video sites (itch.io, Steam, Patreon, Ko-fi, Gumroad, PayPal, Stripe payment links, hosted Bitcoin pages, YouTube and others), never chat apps or social networks like Discord or Instagram: get_game's who_can_open says which, and anyone a link doesn't open for sees "This link can't be opened here." (the game hears "stayed"). No link shorteners, IP addresses or user names in addresses. Every site is checked against Cloudflare's malware and phishing list when added and every day; a listed one opens for nobody, with a review_reason. Live at once, with no approval.`,
|
|
53680
|
+
description: `The game's registered links: the sites it opens with Zaisma.links.open(link_name), like its store, Patreon, Steam page or Discord, behind Zaisma's "You're leaving Zaisma" card over the game; the game can open no other sites. The whole list each time, up to 20; [] for none. The game can add a path and query under a link's address: Zaisma.links.open("store", { path: "/buy", queryParameters: { item: "sword" } }). Signed-in adults open any link once the account's standing opens links to other sites (established, or a card that paid Zaisma or a Stripe account: get_access's standing; until then who_can_open says waiting_for_standing and the link is kept); guests and under-18 accounts only links on trusted store, payment and video sites (itch.io, Steam, Patreon, Ko-fi, Gumroad, PayPal, Stripe payment links, hosted Bitcoin pages, YouTube and others), never chat apps or social networks like Discord or Instagram: get_game's who_can_open says which, and anyone a link doesn't open for sees "This link can't be opened here." (the game hears "stayed"). No link shorteners, IP addresses or user names in addresses. Every site is checked against Cloudflare's malware and phishing list when added and every day; a listed one opens for nobody, with a review_reason. Live at once, with no approval.`,
|
|
53569
53681
|
examples: [[{ link_name: "store", destination_url: "https://kit.itch.io/sky-hopper", is_on_game_page: true }], [{ link_name: "patreon", destination_url: "https://www.patreon.com/kit", is_on_game_page: true }, { link_name: "discord", destination_url: "https://discord.gg/skyhopper", is_on_game_page: false }]]
|
|
53570
53682
|
},
|
|
53571
53683
|
glass_tint_color: {
|
|
@@ -54055,7 +54167,7 @@ Not: { "name": "Zaisma Login" } (name the app the way players know your site)`,
|
|
|
54055
54167
|
{
|
|
54056
54168
|
name: "get_credit",
|
|
54057
54169
|
title: "My hosting credit",
|
|
54058
|
-
description: "The developer's hosting credit: the balance (in cents, never in dollars), whether it's low (is_credit_low: under 20% of what there was after the last top-up), the last 30 days' top-ups with what each still refunds, each game's budget and what credit paid for it this month, and the last 90 days' entries. Every game gets free hosting each month on one meter (get_stats' hosting_use_this_month, a percentage); past it, only a game with a budget (update_game, monthly_budget_cents) draws from credit, never past its budget. Credit never expires and is never paid out.",
|
|
54170
|
+
description: "The developer's hosting credit: the balance (in cents, never in dollars), whether it's low (is_credit_low: under 20% of what there was after the last top-up), the monthly credit plan (credit_plan: its level, whether the card pays, and what the next 1st pays at today's balance; null with none), the last 30 days' top-ups with what each still refunds, each game's budget and what credit paid for it this month, and the last 90 days' entries. Every game gets free hosting each month on one meter (get_stats' hosting_use_this_month, a percentage); past it, only a game with a budget (update_game, monthly_budget_cents) draws from credit, never past its budget. Credit never expires and is never paid out.",
|
|
54059
54171
|
inputSchema: object2({}),
|
|
54060
54172
|
outputSchema: CREDIT,
|
|
54061
54173
|
annotations: read,
|
|
@@ -54064,7 +54176,7 @@ Not: { "name": "Zaisma Login" } (name the app the way players know your site)`,
|
|
|
54064
54176
|
{
|
|
54065
54177
|
name: "get_spending_request",
|
|
54066
54178
|
title: "A spending request's answer",
|
|
54067
|
-
description: "Whether the developer has answered a raise in spending you asked for. When update_game (a game's first or higher budget) answers confirmation_needed, it also answers spending_request_id: give the developer confirm_url, where a sheet names the exact change and they approve with a passkey tap or decline. This shows request_status (waiting, approved, declined or expired after a day), the change asked for and what it is now. Approved means the change is already made.",
|
|
54179
|
+
description: "Whether the developer has answered a raise in spending you asked for. When update_game (a game's first or higher budget) or change_credit_plan (a higher level) answers confirmation_needed, it also answers spending_request_id: give the developer confirm_url, where a sheet names the exact change and they approve with a passkey tap or decline. This shows request_status (waiting, approved, declined or expired after a day), the change asked for and what it is now. Approved means the change is already made.",
|
|
54068
54180
|
inputSchema: object2({ spending_request_id: { type: "string", pattern: "^spending_request_[a-z0-9]+$", description: "From confirmation_needed's spending_request_id, like spending_request_k2m9x4\u2026" } }, ["spending_request_id"]),
|
|
54069
54181
|
outputSchema: SPENDING_REQUEST,
|
|
54070
54182
|
annotations: read,
|
|
@@ -54082,6 +54194,38 @@ Not: { "amount_cents": 500 } (the least is 1000, $10)`,
|
|
|
54082
54194
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
54083
54195
|
run: (toolArguments, call3) => call3("POST", "/credit/top-ups", pick(toolArguments, ["amount_cents"]))
|
|
54084
54196
|
},
|
|
54197
|
+
{
|
|
54198
|
+
name: "start_credit_plan",
|
|
54199
|
+
title: "Start a monthly credit plan",
|
|
54200
|
+
description: `Start the developer's monthly credit plan: credit filled back to refill_level_cents on the 1st of every month (UTC), so games with a budget never run dry. Answers Stripe's page (checkout_url), where only the developer can pay; give them the link. The first payment fills credit to the level (at most $1,000; a higher level fills to it from the next 1st); each 1st pays only what brings the balance back to the level, nothing under $1 or when the balance is at or over it. Stripe sells every payment, with sales tax or VAT on top. Each payment is a top-up, refundable for 30 days with refund_top_up. One plan at a time: already_credit_plan when there is one.
|
|
54201
|
+
|
|
54202
|
+
Not: { "refill_level_cents": 40 } (that's 40 cents; cents, not dollars, so $40 is 4000)
|
|
54203
|
+
Not: { "refill_level_cents": 500 } (the least is 1000, $10)`,
|
|
54204
|
+
inputSchema: { ...object2({ refill_level_cents: { type: "integer", minimum: 1e3, maximum: 1e6, description: "The level in cents: 4000 is $40. From 1000 ($10) to 1000000 ($10,000)." } }, ["refill_level_cents"]), examples: [{ refill_level_cents: 4e3 }] },
|
|
54205
|
+
outputSchema: CREDIT_PLAN_CHECKOUT,
|
|
54206
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
54207
|
+
run: (toolArguments, call3) => call3("POST", "/credit/plan/checkout", pick(toolArguments, ["refill_level_cents"]))
|
|
54208
|
+
},
|
|
54209
|
+
{
|
|
54210
|
+
name: "change_credit_plan",
|
|
54211
|
+
title: "Change the monthly credit plan",
|
|
54212
|
+
description: `Set the monthly credit plan's level: what credit is filled back to on the 1st. A lower or equal level applies at once with any key that has credit_access write; a higher one spends more, so it answers confirmation_needed with confirm_url, where the developer approves it with their passkey, and spending_request_id for get_spending_request. Answers no_credit_plan with none (start_credit_plan starts one).
|
|
54213
|
+
|
|
54214
|
+
Not: { "refill_level_cents": 60 } (cents, not dollars, so $60 is 6000)`,
|
|
54215
|
+
inputSchema: { ...object2({ refill_level_cents: { type: "integer", minimum: 1e3, maximum: 1e6, description: "The new level in cents: 6000 is $60." } }, ["refill_level_cents"]), examples: [{ refill_level_cents: 6e3 }] },
|
|
54216
|
+
outputSchema: CREDIT_CHANGED,
|
|
54217
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
54218
|
+
run: (toolArguments, call3) => call3("PATCH", "/credit/plan", pick(toolArguments, ["refill_level_cents"]))
|
|
54219
|
+
},
|
|
54220
|
+
{
|
|
54221
|
+
name: "stop_credit_plan",
|
|
54222
|
+
title: "Stop the monthly credit plan",
|
|
54223
|
+
description: "Stop the monthly credit plan at once: nothing is charged again, and credit already bought stays (each payment still refunds for 30 days with refund_top_up). It holds no prepaid month, so nothing is lost by stopping. Answers no_credit_plan with none. Ask the developer first.",
|
|
54224
|
+
inputSchema: object2({}),
|
|
54225
|
+
outputSchema: CREDIT_CHANGED,
|
|
54226
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
54227
|
+
run: (_, call3) => call3("POST", "/credit/plan/stop")
|
|
54228
|
+
},
|
|
54085
54229
|
{
|
|
54086
54230
|
name: "get_stripe_account",
|
|
54087
54231
|
title: "My Stripe account",
|
|
@@ -54507,7 +54651,7 @@ Not: { "rating": "helpful" } (like)`,
|
|
|
54507
54651
|
{
|
|
54508
54652
|
name: "get_access",
|
|
54509
54653
|
title: "What this key can do",
|
|
54510
|
-
description: "What the API key you're signed in with may do: its access_level (read, upload, publish, full or custom), what that gives in each area, the games it works on (game_slugs, null for all) and when it ends (api_key_ends_at, null for never). A call the key doesn't reach answers key_not_allowed. Keys are made only by the developer on zaisma.com/dashboard/settings, never through the MCP. can_publish says whether the account can make, upload and publish games, which until the beta only accounts Zaisma adds can. banned_at and ban_scope say whether Zaisma banned the account: a banned developer can call only get_access, list_games and export_game. plan says whether the developer has Zaisma Pro (free or pro).",
|
|
54654
|
+
description: "What the API key you're signed in with may do: its access_level (read, upload, publish, full or custom), what that gives in each area, the games it works on (game_slugs, null for all) and when it ends (api_key_ends_at, null for never). A call the key doesn't reach answers key_not_allowed. Keys are made only by the developer on zaisma.com/dashboard/settings, never through the MCP. can_publish says whether the account can make, upload and publish games, which until the beta only accounts Zaisma adds can. banned_at and ban_scope say whether Zaisma banned the account: a banned developer can call only get_access, list_games and export_game. plan says whether the developer has Zaisma Pro (free or pro). standing says what the account has earned and what each power needs: links to sites outside the trusted list open once it's established (14 days old, 20 different returning players, nothing ruled against it in 30 days) or at once with a card that paid Zaisma or a Stripe account, and search engines only once it's established; tell the developer standing.message_for_people when they ask why a link doesn't open or their game isn't on search engines.",
|
|
54511
54655
|
inputSchema: object2({}),
|
|
54512
54656
|
outputSchema: object({
|
|
54513
54657
|
access: ACCESS,
|
|
@@ -54515,7 +54659,9 @@ Not: { "rating": "helpful" } (like)`,
|
|
|
54515
54659
|
banned_at: nullable(isoTime("When Zaisma banned this account; null when it isn't banned.")),
|
|
54516
54660
|
ban_scope: nullable(choice({ developer: "its games are down, and only get_access, list_games and export_game work; playing still works", whole_account: "the account is closed" }, "How far the ban goes; null when it isn't banned.")),
|
|
54517
54661
|
credit_balance_cents: nullable(countOf("The developer's hosting credit, in cents; null when the key can't read it (credit_access none, or a key limited to some games).")),
|
|
54518
|
-
plan: choice({ free: "the free daily limits", pro: "Zaisma Pro: the higher daily limits" }, "The developer's plan; get_pro has its limits.")
|
|
54662
|
+
plan: choice({ free: "the free daily limits", pro: "Zaisma Pro: the higher daily limits" }, "The developer's plan; get_pro has its limits."),
|
|
54663
|
+
standing: STANDING,
|
|
54664
|
+
next_steps: NEXT
|
|
54519
54665
|
}),
|
|
54520
54666
|
annotations: read,
|
|
54521
54667
|
run: (_, call3) => call3("GET", "/access")
|
|
@@ -55359,7 +55505,7 @@ async function precedentAction(env2, path, form, actor) {
|
|
|
55359
55505
|
var redirect = () => new Response(null, { status: 303, headers: { Location: "/admin/precedents" } });
|
|
55360
55506
|
|
|
55361
55507
|
// ../../apps/site/src/admin-mcp.ts
|
|
55362
|
-
var
|
|
55508
|
+
var DAY_MS10 = 864e5;
|
|
55363
55509
|
var read2 = { readOnlyHint: true, openWorldHint: false };
|
|
55364
55510
|
var change = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
55365
55511
|
var object3 = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
@@ -55579,7 +55725,7 @@ var ADMIN_TOOLS = [
|
|
|
55579
55725
|
run: async (_, { env: env2 }) => {
|
|
55580
55726
|
const { results } = await env2.DB.prepare(
|
|
55581
55727
|
`${QUEUE_SELECT} WHERE reports.report_status != 'open' AND reports.closed_at_ms = reports.triaged_at_ms AND reports.closed_at_ms > ? AND reports.severity_index > 0 ORDER BY reports.closed_at_ms DESC LIMIT 100`
|
|
55582
|
-
).bind(Date.now() - 7 *
|
|
55728
|
+
).bind(Date.now() - 7 * DAY_MS10).all();
|
|
55583
55729
|
return {
|
|
55584
55730
|
period_duration_days: 7,
|
|
55585
55731
|
reports: results.map((report) => {
|
|
@@ -56187,6 +56333,29 @@ Not: { "decision": "block", "why": "\u2026" } for every country (a game breaking
|
|
|
56187
56333
|
return { username: account.username, can_publish: canPublish, publishing_accounts: publishingAccountRows(await publishingAccounts(env2.DB)), next_steps: [] };
|
|
56188
56334
|
}
|
|
56189
56335
|
},
|
|
56336
|
+
{
|
|
56337
|
+
name: "set_zaisma_account",
|
|
56338
|
+
title: "Mark one of Zaisma's own accounts",
|
|
56339
|
+
description: "Mark an account as one of Zaisma's own (is_zaisma_account true), like the seed games' developers, by its username, or unmark it (false). Zaisma's own accounts are established from the start, so their games' links to other sites and search engines open before any players come back; a ruling or a ban still closes them. Unmarking it leaves it to the daily count. Ask the admin first.",
|
|
56340
|
+
inputSchema: object3(
|
|
56341
|
+
{
|
|
56342
|
+
username: usernameInput,
|
|
56343
|
+
is_zaisma_account: { type: "boolean", description: "true marks it as Zaisma's own; false unmarks it." }
|
|
56344
|
+
},
|
|
56345
|
+
["username", "is_zaisma_account"]
|
|
56346
|
+
),
|
|
56347
|
+
outputSchema: ZAISMA_ACCOUNT_CHANGED,
|
|
56348
|
+
annotations: change,
|
|
56349
|
+
run: async (toolArguments, { env: env2, actor }) => {
|
|
56350
|
+
const account = await accountNamed(env2, toolArguments.username);
|
|
56351
|
+
const isZaismaAccount = toolArguments.is_zaisma_account === true;
|
|
56352
|
+
const owner = await personOwner(env2.DB, account.id);
|
|
56353
|
+
const rulingWaitStartedAfterMs = Date.now() - STANDING_LIMITS.rulingWaitDurationDays * 864e5;
|
|
56354
|
+
await env2.DB.prepare("UPDATE owners SET is_zaisma_account = ?1, is_established = CASE WHEN ?1 = 1 AND (last_ruling_at_ms IS NULL OR last_ruling_at_ms <= ?2) THEN 1 ELSE 0 END, standing_checked_at_ms = NULL WHERE id = ?3").bind(isZaismaAccount ? 1 : 0, rulingWaitStartedAfterMs, owner.id).run();
|
|
56355
|
+
await audit(env2.DB, null, isZaismaAccount ? "zaisma-account-marked" : "zaisma-account-unmarked", `@${account.username}`, actor);
|
|
56356
|
+
return { username: account.username, is_zaisma_account: isZaismaAccount, next_steps: [] };
|
|
56357
|
+
}
|
|
56358
|
+
},
|
|
56190
56359
|
{
|
|
56191
56360
|
name: "ban_account",
|
|
56192
56361
|
title: "Ban an account",
|
|
@@ -56456,6 +56625,7 @@ function countryBlockSection(game) {
|
|
|
56456
56625
|
async function takeDownGame(env2, gameId, contentRule, reason, actor) {
|
|
56457
56626
|
const removedReason = decidedReason("taken_down", contentRule, reason);
|
|
56458
56627
|
await setReviewStatus(env2.DB, gameId, "removed", removedReason);
|
|
56628
|
+
await noteRulingOnGame(env2, gameId);
|
|
56459
56629
|
await recordRestoreEvent(env2, { event_kind: "game_removed", at_ms: Date.now(), game_id: gameId, review_reason: removedReason });
|
|
56460
56630
|
await closeReports(env2.DB, gameId, "actioned");
|
|
56461
56631
|
await audit(env2.DB, gameId, "takedown", reason, actor, removedReason);
|
|
@@ -56512,6 +56682,7 @@ async function adminForm(env2, path, form, actor) {
|
|
|
56512
56682
|
const rejectedReason = decidedReason("rejected_by_zaisma", pickedRule(form), reason);
|
|
56513
56683
|
await saveScan(env2.DB, version.id, "rejected", null);
|
|
56514
56684
|
await env2.DB.prepare("UPDATE versions SET review_reason = ? WHERE id = ?").bind(JSON.stringify(rejectedReason), version.id).run();
|
|
56685
|
+
await noteRulingOnGame(env2, version.game_id);
|
|
56515
56686
|
if (version.report_held_at_ms) await closeReports(env2.DB, version.game_id, "actioned");
|
|
56516
56687
|
await audit(env2.DB, version.game_id, "rejected", reason ? `${version.id}: ${reason}` : version.id, actor, rejectedReason);
|
|
56517
56688
|
}
|
|
@@ -57326,6 +57497,9 @@ async function playerPage(input) {
|
|
|
57326
57497
|
updates: [],
|
|
57327
57498
|
otherGames: [],
|
|
57328
57499
|
liveRooms: null,
|
|
57500
|
+
// The developer's own computer: their links open as an established developer's would, and no search engine is near it.
|
|
57501
|
+
searchEnginesEnabled: false,
|
|
57502
|
+
outsideLinksEnabled: true,
|
|
57329
57503
|
checkedPlayerCount: 0,
|
|
57330
57504
|
hasThreads: false,
|
|
57331
57505
|
purchaseLabel: { categories: input.soldCategories, has_own_payments: false },
|