SDK
@uloggd/arcade-sdk: the client, every command, event, error and close code, and the mock.
npm install @uloggd/arcade-sdk- MIT licensed, with no runtime dependencies. It targets ES2020 and is safe to import during server rendering.
- It ships as ES modules and as CommonJS, both with type declarations, and as a build for a plain script tag (below).
@uloggd/arcade-sdkis the client,@uloggd/arcade-sdk/mockthe mock, and@uloggd/arcade-sdk/servertoken verification for Node 20.19 or later, Deno, Bun and Workers (see Identity; for servers in other languages, see Verifying in other languages).- uloggd never serves the SDK to games: you bundle it, or host it with your game.
| Version | What it ships |
|---|---|
0.1.0 | ES modules only. |
0.2.0 | Adds CommonJS, loaded through require, and the script-tag build, dist/uloggd-arcade.min.js. The API and the protocol are unchanged. |
Without a bundler
The package includes dist/uloggd-arcade.min.js, a build for a plain
<script> tag that defines one global, UloggdArcade. Copy it from
node_modules/@uloggd/arcade-sdk/dist/ into your game, with
uloggd-arcade.min.js.map if you want readable stack traces, and serve it
from your game's own origin.
<script src="./uloggd-arcade.min.js"></script>
<script>
const arcade = UloggdArcade.createArcadeSDK({
appId: "YOUR_APP_ID",
mockWhenStandalone: true,
});
arcade.ready().then(({ context }) => startGame(context));
</script>UloggdArcadeholdscreateArcadeSDK,ArcadeSDKError,ArcadeSDKMockand the constants (SDK_VERSION,ERROR_CODES,HostCloseCode,GuestCloseCode,PROTOCOL_VERSION,SUBSCRIBABLE_EVENTSandDEFAULT_HOST_ORIGIN). Token verification is not in it: it belongs on your server.- Serve it yourself. uloggd does not serve it, and a copy on a public CDN is a third party your release must declare, one that can change what the game runs without a new review.
- Without npm, the same file is in
https://registry.npmjs.org/@uloggd/arcade-sdk/-/arcade-sdk-0.2.0.tgz, underpackage/dist/. - Load it as a classic script, before the scripts that use it. As a module
script (
type="module") it defines no global. - The ES modules in
distalso work from a module script, which importsdist/client.jswith itschunksfolder beside it.
Create the client
import { createArcadeSDK, ArcadeSDKError } from "@uloggd/arcade-sdk";
const arcade = createArcadeSDK({
appId: "YOUR_APP_ID",
mockWhenStandalone: true,
});
try {
const { context } = await arcade.ready();
// context: { appId, instanceId, mode, locale, platform, layout, game }
startGame(context);
} catch (error) {
if (error instanceof ArcadeSDKError && error.code === "not_embedded") {
showMessage("Play this game on uloggd.com");
}
}| Option | |
|---|---|
appId | Your app's id, as the portal shows it: a lowercase uuid. |
hostOrigin | Where uloggd runs. https://uloggd.com by default; an http:// origin is accepted only when the game itself runs on a loopback origin. |
mockWhenStandalone | Opened directly on a loopback origin (localhost), outside any frame, the client is the mock. Anywhere else outside uloggd, ready() rejects with not_embedded, so a misdeployed game fails loudly. |
createArcadeSDK throws invalid_args at once for a malformed appId or
hostOrigin.
| Member | |
|---|---|
ready() | Resolves once, after the handshake, with { protocol, commands, context }. Rejects with not_embedded, with timeout when uloggd does not answer the handshake within 10 seconds, or with closed. |
context | The same context, or null before ready() resolves. |
commands | The commands below. |
subscribe(event, listener, onError?) | Listens to an event and returns the function that stops listening. |
supports(command) | Whether uloggd announced the command in READY. Check it before using a command newer than the SDK you shipped. |
close({ code, message? }) | Ends the session, with a code from the close codes below. |
destroy() | Ends the session normally, with code 1000, as close would, and rejects everything pending. For an effect cleanup or a hot reload. |
Commands
Every command returns a promise and times out: after 10 seconds, or after 5
minutes for authorize, which waits on the player.
| Command | Arguments | Result | Needs |
|---|---|---|---|
authorize | { scopes: ["identify"] } | { scopes } | |
getPlayer | none | { id, nickname } | identify |
getIdentityToken | { nonce } | { token, expiresAt } | identify |
getGame | none | { appId, slug, name } | |
requestFullscreen | none | { fullscreen } | a gesture |
exitFullscreen | none | { fullscreen } |
authorizeopens uloggd's own consent dialog, outside the frame, where the player picks the nickname your game will see (a generated one by default). Without thepointer-lockcapability the dialog opens directly, or as soon as the tab is shown again if it is hidden. With it, the dialog does not open on its own, because uloggd cannot see a lock your game holds: the player's bar shows the request with an Answer button, and the dialog opens from that click. Ask only for scopes the release declared, or it fails withinvalid_args. If the player cancels, it fails withuser_cancelled, and asking again keeps failing until the player opens the game again. Ask after a click of your own, such as "Save my progress", rather than on load. A player who already agreed to your game is not asked again.getPlayerreturns the player's per-game id and nickname, unverified. Show them; never trust them on your server.getIdentityTokenreturns a token signed by uloggd, for your server to verify. The nonce comes from your server: 16 to 128 characters of letters, digits,_and-. One token every 3 seconds, and 30 per player per hour.getGamereturns the approved name and the slug, the address reserved when Arcade was enabled. The slug is nevernulltoday; the type allowsnullfor later versions.requestFullscreenandexitFullscreenact on uloggd's container, with its bar visible. CallrequestFullscreenfrom a click or a key press, or it fails withrequires_gesture.
document.querySelector("#save")!.addEventListener("click", async () => {
await arcade.commands.authorize({ scopes: ["identify"] });
const { nonce } = await fetch("/session/nonce", { method: "POST" }).then(
(r) => r.json(),
);
const { token } = await arcade.commands.getIdentityToken({ nonce });
await fetch("/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
});Events
const off = arcade.subscribe("LAYOUT_UPDATE", (layout) => {
resize(layout.width, layout.height);
});
// later
off();| Event | Data |
|---|---|
READY | { protocol: 1, commands, context }. Sent once, without subscribing; ready() resolves with it. |
VISIBILITY_UPDATE | { visible } |
LAYOUT_UPDATE | { width, height, fullscreen, safeArea: { top, right, bottom, left } } |
LOCALE_UPDATE | { locale } |
CLOSING | { reason, deadlineMs }, where reason is user, hidden or suspended. |
GRANT_UPDATE | { scopes } |
- The first listener of an event subscribes, and the last
off()unsubscribes. A subscription uloggd refuses removes its listeners and callsonError. - The SDK also writes the safe area on
:root, as--uloggd-safe-area-inset-top,-right,-bottomand-left. CLOSINGleaves you untildeadlineMsto save.GRANT_UPDATEarrives when the player revokes your game's access in Settings. Stop callinggetPlayerandgetIdentityToken, and treat the player as signed out of your server.
Errors
Every rejection is an ArcadeSDKError, with a code:
| Code | Means |
|---|---|
not_embedded | Not inside uloggd, and not on the mock. |
unknown_command | uloggd does not know the command; check supports() first. |
invalid_args | The arguments were refused: a scope the release did not declare, a malformed nonce, an extra key. |
missing_scope | The player has not granted the scope the command needs. |
user_cancelled | The player closed uloggd's dialog. |
requires_gesture | The command needs a click or a key press in the game first. |
rate_limited | Too many commands, or too many tokens. |
timeout | No answer in time. |
closed | The session closed before the answer. When a close code ended it, the error carries it as closeCode. |
unavailable | uloggd cannot do it right now, such as issuing identity tokens with no signing key available. |
internal | Something failed on uloggd's side. |
Close codes
uloggd ends the session with one of these, and removes the frame:
| Code | Means |
|---|---|
4000 | Invalid frame or app. |
4001 | Invalid origin. |
4002 | Too many messages. |
4003 | The player revoked the game's access. |
4004 | Unsupported protocol version. |
4010 | The game was suspended or switched off. |
The game may close with 1000 (normal) or a code of its own from 4500 to
4599. The message, up to 200 characters, reaches the browser console in
test mode only: players only ever see uloggd's own words.
Limits
The SDK keeps you inside them, but they matter if you send many commands at
once: each message is at most 64 KiB, at most 32 commands wait for an answer
at a time, and commands are limited to 20 a second with bursts of 50. Past
that, uloggd closes the session with 4002.
The mock
On localhost, with mockWhenStandalone: true, the client is an
ArcadeSDKMock, which has the same interface:
- every command answers from fixtures.
authorizegrants at once, as a player who agrees would, and until thengetPlayerandgetIdentityTokenfail withmissing_scope, as they do inside uloggd; ?mock_player=2in the page's address gives a second player, so two tabs can be two players;- identity tokens are signed with a throwaway key and
iss: "mock", which the server verifiers refuse unless you passallowMock: true.
From the script-tag build, the mock is UloggdArcade.ArcadeSDKMock.
In tests, build the mock yourself and steer it. Besides appId it takes
mode, locale, platform, player, and scopes for a player who already
agreed:
import { ArcadeSDKError } from "@uloggd/arcade-sdk";
import { ArcadeSDKMock } from "@uloggd/arcade-sdk/mock";
const arcade = new ArcadeSDKMock({
appId: "YOUR_APP_ID",
scopes: ["identify"],
});
arcade.setPlayer({
id: "6d0c0d6e-3f7a-4d4b-9d61-2f1f5b0a8c11",
nickname: "Ada",
});
arcade.overrideCommand("authorize", async () => {
throw new ArcadeSDKError("user_cancelled");
});
arcade.emit("VISIBILITY_UPDATE", { visible: false });