@hadoku/prefs-client 1.1.5 → 1.1.6
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/README.md +12 -8
- package/dist/avatar.d.ts +43 -14
- package/dist/avatar.js +63 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,12 +121,15 @@ watchparty character.
|
|
|
121
121
|
|
|
122
122
|
## Avatars (`@wolffm/prefs-client/avatar`)
|
|
123
123
|
|
|
124
|
-
One resolved avatar per hadoku user, chosen by the user
|
|
124
|
+
One resolved avatar per hadoku user, chosen by the user in the fleet settings popout
|
|
125
|
+
(`ConnectedSettings`, @wolffm/task-ui-components): the picture's source and their character.
|
|
125
126
|
|
|
126
127
|
```ts
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
interface ResolvedAvatar {
|
|
129
|
+
source: 'sprite' | 'discord' | 'steam' | 'upload'; // what the picture is
|
|
130
|
+
url?: string; // present exactly when source !== 'sprite': square https image, >= 184px
|
|
131
|
+
sprite?: string; // the person's character, WHATEVER the source; absent = your default
|
|
132
|
+
}
|
|
130
133
|
```
|
|
131
134
|
|
|
132
135
|
Browser, the signed-in user's own avatar:
|
|
@@ -152,12 +155,13 @@ Rules for an app that renders avatars:
|
|
|
152
155
|
- **`url` → `<img src>`**, nothing else: never fetch it server-side, never proxy it. Set
|
|
153
156
|
`referrerpolicy="no-referrer"`. A URL can stop working (a changed Discord picture, an admin
|
|
154
157
|
removal): on `error`, fall back to the sprite.
|
|
155
|
-
- **`sprite
|
|
156
|
-
|
|
158
|
+
- **`sprite`** is the character even when the picture is a Discord avatar: a game uses it as the
|
|
159
|
+
piece and its colour. The app that owns the art builds its URL (watchparty owns art revisions;
|
|
160
|
+
its public `GET /watchparty/api/characters` lists them). Without one, use your own default.
|
|
157
161
|
- **Steam attribution is required** wherever a `steam` avatar is shown (Steam Web API terms): a
|
|
158
162
|
visible line such as "Steam avatars via the Steam Web API · not affiliated with Valve", linking
|
|
159
163
|
to `https://store.steampowered.com/`. Do not use Steam's or Discord's logos, and do not imply
|
|
160
164
|
either endorses the app.
|
|
161
165
|
- Resolve on join and on reconnect; avatars change rarely. `resolveAvatars` batches past 100 ids.
|
|
162
|
-
- Source choice,
|
|
163
|
-
|
|
166
|
+
- Source choice, character choice, linking (`linkInPopup`: no page navigation), uploads and
|
|
167
|
+
removal all happen in the settings popout. Don't build your own.
|
package/dist/avatar.d.ts
CHANGED
|
@@ -14,25 +14,41 @@ type LinkProvider = (typeof LINK_PROVIDERS)[number];
|
|
|
14
14
|
/**
|
|
15
15
|
* The one avatar every app renders for a user.
|
|
16
16
|
*
|
|
17
|
-
* - `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* URL
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
17
|
+
* - `source` is what the picture is. `'sprite'` means the character is the
|
|
18
|
+
* picture; any other source carries `url`.
|
|
19
|
+
* - `url` (present exactly when `source` is not `'sprite'`) is an absolute
|
|
20
|
+
* https image URL, square, at least 184px, on a host the hadoku.me CSP admits.
|
|
21
|
+
* Render it as an `<img>`; never fetch it server-side.
|
|
22
|
+
* - `sprite` is the person's character (a watchparty character id) whenever
|
|
23
|
+
* they have one, WHATEVER the source: games use it as the piece and the
|
|
24
|
+
* name colour even when the picture is a Discord avatar. Absent = never
|
|
25
|
+
* picked; use your own default. The app that owns the art (watchparty, its
|
|
26
|
+
* public GET /watchparty/api/characters) owns its URL and revision.
|
|
24
27
|
*
|
|
25
28
|
* A source that stops being available (an unlink, an admin removal, an account
|
|
26
29
|
* whose custom avatar was removed) resolves to `sprite` with no action from the app.
|
|
27
30
|
*/
|
|
28
|
-
|
|
29
|
-
source:
|
|
31
|
+
interface ResolvedAvatar {
|
|
32
|
+
source: AvatarSource;
|
|
33
|
+
url?: string;
|
|
30
34
|
sprite?: string;
|
|
31
|
-
|
|
35
|
+
}
|
|
36
|
+
/** PUT /api/v1/_avatar — either or both. */
|
|
37
|
+
interface AvatarUpdate {
|
|
38
|
+
source?: AvatarSource;
|
|
39
|
+
/** A watchparty character id (SPRITE_ID_PATTERN). */
|
|
40
|
+
sprite?: string;
|
|
41
|
+
}
|
|
42
|
+
/** What the callback page posts to the window that opened it (same origin only). */
|
|
43
|
+
declare const LINK_MESSAGE_TYPE = "hadoku-avatar-link";
|
|
44
|
+
type LinkMessage = {
|
|
45
|
+
type: typeof LINK_MESSAGE_TYPE;
|
|
46
|
+
ok: true;
|
|
47
|
+
provider: LinkProvider;
|
|
32
48
|
} | {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
type: typeof LINK_MESSAGE_TYPE;
|
|
50
|
+
ok: false;
|
|
51
|
+
error: string;
|
|
36
52
|
};
|
|
37
53
|
/** A linked account as its owner sees it. Never carries a token. */
|
|
38
54
|
interface LinkedAccountView {
|
|
@@ -92,6 +108,8 @@ declare const SPRITE_ID_PATTERN: RegExp;
|
|
|
92
108
|
*/
|
|
93
109
|
declare const LINK_CALLBACK_PATH = "/account/link/callback";
|
|
94
110
|
|
|
111
|
+
/** sessionStorage key the callback page reads to know it runs in the link popup. */
|
|
112
|
+
declare const LINK_POPUP_MARK = "hadoku-avatar-link-popup";
|
|
95
113
|
declare class AvatarError extends Error {
|
|
96
114
|
readonly status: number;
|
|
97
115
|
constructor(status: number, message: string);
|
|
@@ -106,6 +124,8 @@ declare function createAvatarClient(opts?: AvatarClientOptions): {
|
|
|
106
124
|
get: () => Promise<AvatarSettings>;
|
|
107
125
|
/** Pick a source. 409 when it is not available (not linked, nothing uploaded). */
|
|
108
126
|
choose: (source: AvatarSource) => Promise<AvatarSettings>;
|
|
127
|
+
/** Set the source, the character, or both in one write. */
|
|
128
|
+
update: (u: AvatarUpdate) => Promise<AvatarSettings>;
|
|
109
129
|
/**
|
|
110
130
|
* Begin linking. Navigate the browser to the returned URL; the provider
|
|
111
131
|
* returns it to hadoku.me/account/link/callback, which completes the link.
|
|
@@ -113,6 +133,15 @@ declare function createAvatarClient(opts?: AvatarClientOptions): {
|
|
|
113
133
|
* Discord or Steam before they ask.
|
|
114
134
|
*/
|
|
115
135
|
startLink: (provider: LinkProvider) => Promise<LinkStart>;
|
|
136
|
+
/**
|
|
137
|
+
* Link in a popup, so the page never navigates. Call it straight from the
|
|
138
|
+
* click handler: the window is opened before the first await, which is
|
|
139
|
+
* what popup blockers allow. Resolves with fresh settings once the popup
|
|
140
|
+
* reports back or is closed (closing is not an error: the settings say
|
|
141
|
+
* whether the link landed). Rejects only when the popup could not open
|
|
142
|
+
* or the link could not start.
|
|
143
|
+
*/
|
|
144
|
+
linkInPopup: (provider: LinkProvider) => Promise<AvatarSettings>;
|
|
116
145
|
/** Finish a link from the callback page's query string (`location.search`). */
|
|
117
146
|
completeLink: (query: string) => Promise<AvatarSettings>;
|
|
118
147
|
/** Delete the linked id and its cached avatar URL. */
|
|
@@ -132,4 +161,4 @@ interface ResolveAvatarsOptions extends AvatarClientOptions {
|
|
|
132
161
|
*/
|
|
133
162
|
declare function resolveAvatars(opts: ResolveAvatarsOptions, userIds: readonly string[]): Promise<Record<string, ResolvedAvatar>>;
|
|
134
163
|
|
|
135
|
-
export { AVATAR_SOURCES, type AvatarClient, type AvatarClientOptions, AvatarError, type AvatarSettings, type AvatarSource, type AvatarUploadLimits, LINK_CALLBACK_PATH, LINK_PROVIDERS, type LinkProvider, type LinkStart, type LinkedAccountView, RESOLVE_MAX_IDS, type ResolveAvatarsOptions, type ResolveRequest, type ResolveResponse, type ResolvedAvatar, SPRITE_ID_PATTERN, type UploadView, createAvatarClient, resolveAvatars };
|
|
164
|
+
export { AVATAR_SOURCES, type AvatarClient, type AvatarClientOptions, AvatarError, type AvatarSettings, type AvatarSource, type AvatarUpdate, type AvatarUploadLimits, LINK_CALLBACK_PATH, LINK_MESSAGE_TYPE, LINK_POPUP_MARK, LINK_PROVIDERS, type LinkMessage, type LinkProvider, type LinkStart, type LinkedAccountView, RESOLVE_MAX_IDS, type ResolveAvatarsOptions, type ResolveRequest, type ResolveResponse, type ResolvedAvatar, SPRITE_ID_PATTERN, type UploadView, createAvatarClient, resolveAvatars };
|
package/dist/avatar.js
CHANGED
|
@@ -4,11 +4,21 @@ var PREFS_API_BASE = "https://hadoku.me/prefs";
|
|
|
4
4
|
// src/avatar-contract.ts
|
|
5
5
|
var AVATAR_SOURCES = ["sprite", "discord", "steam", "upload"];
|
|
6
6
|
var LINK_PROVIDERS = ["discord", "steam"];
|
|
7
|
+
var LINK_MESSAGE_TYPE = "hadoku-avatar-link";
|
|
7
8
|
var RESOLVE_MAX_IDS = 100;
|
|
8
9
|
var SPRITE_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
9
10
|
var LINK_CALLBACK_PATH = "/account/link/callback";
|
|
10
11
|
|
|
11
12
|
// src/avatar.ts
|
|
13
|
+
var LINK_WINDOW_NAME = "hadoku-avatar-link";
|
|
14
|
+
var LINK_POPUP_MARK = "hadoku-avatar-link-popup";
|
|
15
|
+
function markPopup(on) {
|
|
16
|
+
try {
|
|
17
|
+
if (on) sessionStorage.setItem(LINK_POPUP_MARK, "1");
|
|
18
|
+
else sessionStorage.removeItem(LINK_POPUP_MARK);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
12
22
|
var AvatarError = class extends Error {
|
|
13
23
|
constructor(status, message) {
|
|
14
24
|
super(message);
|
|
@@ -32,11 +42,52 @@ function createAvatarClient(opts = {}) {
|
|
|
32
42
|
method,
|
|
33
43
|
...body === void 0 ? {} : { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
|
|
34
44
|
});
|
|
45
|
+
const getSettings = () => call("");
|
|
46
|
+
async function linkInPopup(provider) {
|
|
47
|
+
markPopup(true);
|
|
48
|
+
const popup = window.open("about:blank", LINK_WINDOW_NAME, "popup,width=520,height=760");
|
|
49
|
+
markPopup(false);
|
|
50
|
+
if (!popup)
|
|
51
|
+
throw new AvatarError(0, "The sign-in window was blocked: allow pop-ups for hadoku.me");
|
|
52
|
+
let authorizeUrl;
|
|
53
|
+
try {
|
|
54
|
+
({ authorizeUrl } = await send(`/link/${provider}`, "POST", {}));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
popup.close();
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
popup.location.href = authorizeUrl;
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const channel = new BroadcastChannel(LINK_MESSAGE_TYPE);
|
|
62
|
+
let settled = false;
|
|
63
|
+
const finish = (failure) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
channel.close();
|
|
67
|
+
clearInterval(closedPoll);
|
|
68
|
+
clearTimeout(giveUp);
|
|
69
|
+
if (failure) reject(new AvatarError(400, failure));
|
|
70
|
+
else getSettings().then(resolve, reject);
|
|
71
|
+
};
|
|
72
|
+
channel.onmessage = (e) => {
|
|
73
|
+
if (e.data?.type !== LINK_MESSAGE_TYPE) return;
|
|
74
|
+
finish(e.data.ok ? void 0 : e.data.error);
|
|
75
|
+
};
|
|
76
|
+
const closedPoll = setInterval(() => {
|
|
77
|
+
if (!popup.closed) return;
|
|
78
|
+
clearInterval(closedPoll);
|
|
79
|
+
setTimeout(() => finish(), 1500);
|
|
80
|
+
}, 500);
|
|
81
|
+
const giveUp = setTimeout(() => finish(), 10 * 60 * 1e3);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
35
84
|
return {
|
|
36
85
|
/** The caller's settings, including the resolved avatar. */
|
|
37
|
-
get:
|
|
86
|
+
get: getSettings,
|
|
38
87
|
/** Pick a source. 409 when it is not available (not linked, nothing uploaded). */
|
|
39
88
|
choose: (source) => send("", "PUT", { source }),
|
|
89
|
+
/** Set the source, the character, or both in one write. */
|
|
90
|
+
update: (u) => send("", "PUT", u),
|
|
40
91
|
/**
|
|
41
92
|
* Begin linking. Navigate the browser to the returned URL; the provider
|
|
42
93
|
* returns it to hadoku.me/account/link/callback, which completes the link.
|
|
@@ -44,6 +95,15 @@ function createAvatarClient(opts = {}) {
|
|
|
44
95
|
* Discord or Steam before they ask.
|
|
45
96
|
*/
|
|
46
97
|
startLink: (provider) => send(`/link/${provider}`, "POST", {}),
|
|
98
|
+
/**
|
|
99
|
+
* Link in a popup, so the page never navigates. Call it straight from the
|
|
100
|
+
* click handler: the window is opened before the first await, which is
|
|
101
|
+
* what popup blockers allow. Resolves with fresh settings once the popup
|
|
102
|
+
* reports back or is closed (closing is not an error: the settings say
|
|
103
|
+
* whether the link landed). Rejects only when the popup could not open
|
|
104
|
+
* or the link could not start.
|
|
105
|
+
*/
|
|
106
|
+
linkInPopup: (provider) => linkInPopup(provider),
|
|
47
107
|
/** Finish a link from the callback page's query string (`location.search`). */
|
|
48
108
|
completeLink: (query) => send("/link/complete", "POST", { query: query.replace(/^\?/, "") }),
|
|
49
109
|
/** Delete the linked id and its cached avatar URL. */
|
|
@@ -72,6 +132,8 @@ export {
|
|
|
72
132
|
AVATAR_SOURCES,
|
|
73
133
|
AvatarError,
|
|
74
134
|
LINK_CALLBACK_PATH,
|
|
135
|
+
LINK_MESSAGE_TYPE,
|
|
136
|
+
LINK_POPUP_MARK,
|
|
75
137
|
LINK_PROVIDERS,
|
|
76
138
|
RESOLVE_MAX_IDS,
|
|
77
139
|
SPRITE_ID_PATTERN,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadoku/prefs-client",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Client SDK for the hadoku unified preferences store. Wraps /prefs/api/v1/* with localStorage caching, debounced writes, Zod-validated reads, and a usePrefs() React hook, plus match history, ratings and leaderboards for every game (./games), and the one resolved avatar per user (./avatar).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|