forbidden

@forbidden/game-telemetry (0.2.0)

Published 2026-09-04 17:57:40 +02:00 by forbidden-automation

Installation

@forbidden:registry=https://git.forbiddenprojects.dev/api/packages/forbidden/npm/
npm install @forbidden/game-telemetry@0.2.0
"@forbidden/game-telemetry": "0.2.0"

About this package

Framework-neutral, privacy-bounded client for the Forbidden Games API telemetry contract

@forbidden/game-telemetry

One framework-neutral client for the Forbidden Games API. It owns anonymous-device bootstrap, session lifecycle, page-view and mission telemetry, bounded retry, and the small browser/Godot Web adapters. Games provide only their catalog gameId, public API URL, build identifier, and gameplay signals.

The package contains no service credential. The generated anonymous device secret and backend session token are player-scoped API credentials: the identity may be stored by the caller, while the session token and retry queue stay in memory.

Browser

import { createBrowserTelemetry } from "@forbidden/game-telemetry/browser";

const telemetry = createBrowserTelemetry({
  gameId: "ashwater_parish",
  baseUrl: import.meta.env.VITE_EUGAMES_API_BASE_URL,
  build: import.meta.env.VITE_GIT_SHA,
});

await telemetry.start(); // false on failure; gameplay continues
telemetry.client.mission.start("silent_farm_01");
telemetry.client.mission.complete("silent_farm_01");

Versioned progression

Progression events describe a small, reviewed set of player-visible beats rather than arbitrary clicks or scene names. The Games API owns the canonical flow/node allowlist; game repositories keep only the mapping from authoritative gameplay transitions to those identifiers.

const run = telemetry.client.progress.startRun("estate_entry", 1);
run?.complete("entrance_sealed_acknowledged");
run?.complete("evidence_set_completed");

Each run receives a random run ID, a session-local attempt number and a monotonically increasing sequence. Only successfully queued events advance the sequence. A reason is accepted only for a fail observation and is restricted to the shared coarse allowlist. Do not use progression events for raw input, coordinates, free-form labels, save data, or browser lifecycle guesses.

run.cursor() returns a privacy-safe, run-only pseudonymous cursor that may be stored in game save data. After a later start(), pass it to progress.resumeRun(cursor) to preserve the run ID, attempt, and next sequence across backend sessions. The cursor contains no player or backend session identifier; treat an invalid or exhausted cursor as non-resumable and start a new run.

start() emits exactly one visit.start and one session.start for the backend session. An in-flight batch is immutable: its encoded body, event IDs, and request idempotency key remain stable even when a response is lost and new events arrive. A 409 is not accepted or dropped. The queue is bounded and in-memory, each encoded batch stays below 64 KiB, and an offline API never blocks the game. After bounded immediate attempts, only one delayed continuation is scheduled.

The browser adapter uses credential-capable fetch(..., { keepalive: true }) for pagehide teardown; sendBeacon cannot carry the required authorization headers. A persisted BFCache pageshow waits for teardown and starts a new resume session. Applications should use this adapter instead of adding their own unload handler.

Explicit opt-in (When the Room Blinks)

import {
  SLEEP_HORROR_EXTENSION,
  createGameTelemetryClient,
  createSleepHorrorConsent,
} from "@forbidden/game-telemetry";

const consent = createSleepHorrorConsent(localStorage); // unset means denied
const telemetry = createGameTelemetryClient({
  gameId: "sleephorror",
  baseUrl: import.meta.env.VITE_EUGAMES_API_BASE_URL,
  identityStorage: localStorage,
  consent,
  extensions: [SLEEP_HORROR_EXTENSION],
});

consent.grant();
await telemetry.start();
// consent.withdraw() aborts work, clears the queue/session and erases stored identity.

Refusing consent makes no network request. Gameplay must remain available whether consent is unset, denied, withdrawn, storage fails, or Games API is unavailable.

Godot Web

The @forbidden/game-telemetry/godot-web bridge exposes JSON-free start, resume, mission*, end, and dispose methods suitable for JavaScriptBridge. Construct the client in the web shell; do not pass a token or service credential through Godot.

Extension boundary

Core events and mission lifecycle are built in. A game-specific extension must be explicitly registered with an allowlisted name and validator. Payloads are additionally limited by byte size, depth, item count, and forbidden privacy/credential keys. The Games API contract must already accept that extension name; this is not an escape hatch for arbitrary analytics.

Publishing

The package is configured for the Forbidden organization npm registry and prepack always builds a fresh dist, including from a clean checkout. Publication belongs to the shared organization package publisher operated by forbidden-operator; repositories must not add another workflow, service user, token, or .npmrc credential. Consumers pin an immutable published version and commit the lockfile.

Version 0.1.0 was the first immutable release. Version 0.2.0 adds the additive progression API. A default-branch merge is the deliberate publication trigger and must happen only after the digest-pinned delivery-policy runner and the exact forbidden/eugames npm caller are live. The publisher proves that both the manifest version and the full-source-SHA dist-tag are absent before mutation, then verifies registry SHA-1, SHA-512 SRI, version and source-tag readback. Any existing version, conflicting tag or ambiguous registry result fails closed instead of overwriting package state.

A default-branch publication event is single-use. If infrastructure skips the npm preparation job before it starts, do not rerun that consumed event against changed central workflow source. Land a reviewed, meaningful source-only clarification after the central fix instead; the resulting fresh default-branch SHA is then the only release candidate and provenance tag.

Release evidence distinguishes the original scoped-caller event (push) from execution inside the reusable workflow (workflow_call). Both identities are validated independently before the credential-free preparation can mark a package eligible.

Npm preparation uses the qualified delivery runner's installed delivery-contract binary directly; it does not require a host Docker socket. Before eligibility, the runner proves the exact contract API version and all packaging dependencies with a credential-free fixture.

The credential-bearing publication boundary first performs a supported, read-only package-list GET scoped exactly to owner forbidden, type npm, query @forbidden/game-telemetry, and limit 1. Only an unredirected HTTP 200 with a strict empty or exact-target package array can proceed to the immutable absence checks. This proves the package capability target without broadening the package-only token; the forbidden-operator identity remains an owner-provisioned secret invariant, not a runtime identity claim. The token crosses into the publisher only through a mode-restricted, single-link file that is removed before registry calls; it is never a command argument or part of the publication artifact.

Dependencies

Development Dependencies

ID Version
@eugames/contracts 0.0.0
typescript ^5.8.2
vitest ^3.2.4
Details
npm
2026-09-04 17:57:40 +02:00
1
UNLICENSED
sha-68f2daefffa5ab998f112b804074a323fc9790f6
27 KiB
Assets (1)
Versions (2) View all
0.2.0 2026-09-04
0.1.0 2026-09-03