@mpgd/cli 0.55.0 → 0.57.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.
@@ -19,4 +19,9 @@ interface ImportMetaEnv {
19
19
  readonly VITE_MPGD_GAME_SERVICES_URL?: string;
20
20
  readonly VITE_MPGD_GAME_SERVICES_TARGET?: string;
21
21
  readonly VITE_MPGD_GAME_SERVICES_TRANSPORT?: string;
22
+ /**
23
+ * Local development only: `<playerId>` or `<playerId>.<installationId>`
24
+ * for an unauthenticated notifications session (see `serverSession.ts`).
25
+ */
26
+ readonly VITE_MPGD_DEV_NOTIFICATION_SESSION?: string;
22
27
  }
@@ -0,0 +1,104 @@
1
+ import { createDevelopmentServerSession, type DevelopmentServerSession } from '@mpgd/game-services';
2
+
3
+ /**
4
+ * Local development only: an unauthenticated session for a game-services
5
+ * Worker run with `MPGD_ALLOW_INSECURE_DEVELOPMENT_NOTIFICATION_SESSIONS = "true"`
6
+ * (see docs/NOTIFICATIONS.md, "Try it locally"). On only when the Vite dev
7
+ * server runs (`dev`, `import.meta.env.DEV`), the mode is `development`, and
8
+ * `VITE_MPGD_DEV_NOTIFICATION_SESSION` is `<playerId>` or
9
+ * `<playerId>.<installationId>` (each 1 to 64 letters, digits, `_`, or `-`;
10
+ * the installation defaults to `dev-installation`). The session id is
11
+ * `dev-<installationId>`. Anyone can write such a header, so the Worker
12
+ * refuses the flag with its D1 store. Otherwise `undefined`; an invalid
13
+ * value logs why.
14
+ */
15
+ export function resolveStarterDevelopmentServerSession(env: {
16
+ readonly dev: boolean;
17
+ readonly mode: string;
18
+ readonly value: string | undefined;
19
+ }): DevelopmentServerSession | undefined {
20
+ const value = env.value?.trim();
21
+ // Only the development mode: a staging or release build that runs with
22
+ // NODE_ENV=development has `dev` set too.
23
+ if (!env.dev || env.mode !== 'development' || value === undefined || value === '') {
24
+ return undefined;
25
+ }
26
+ const [playerId = '', installationId = 'dev-installation', ...rest] = value.split('.');
27
+ try {
28
+ if (rest.length > 0) {
29
+ throw new Error('Expected <playerId> or <playerId>.<installationId>.');
30
+ }
31
+ const session = createDevelopmentServerSession({ mode: env.mode, playerId, installationId });
32
+ console.warn(
33
+ `[game-services] VITE_MPGD_DEV_NOTIFICATION_SESSION: signed in as ${playerId} with an `
34
+ + 'unauthenticated development session. Local development only.',
35
+ );
36
+ return session;
37
+ } catch (error) {
38
+ console.error('[game-services] VITE_MPGD_DEV_NOTIFICATION_SESSION is ignored.', error);
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * What `serverSession.ts` hands out, kept apart from `import.meta.env` so it
45
+ * can be tested: the game's own session once it called `set` (even with
46
+ * `undefined`), else the development session, read once. `once` marks the
47
+ * reader that keeps what it reads, the commerce client, which only ever
48
+ * gets the game's own session: the development session is for the
49
+ * notifications Worker, and a commerce backend with ingress authentication
50
+ * would refuse its header. A `set` that changes the session after a reader
51
+ * kept the old one warns: after the commerce client read it, or after the
52
+ * development session was handed out (a notifications backend keeps it).
53
+ */
54
+ export function createStarterServerSessionSlot<Session extends object>(input: {
55
+ /** The development session; called at most once. */
56
+ readonly development: () => Session | undefined;
57
+ readonly warn: (message: string) => void;
58
+ }): {
59
+ resolve(options?: { readonly once?: boolean }): Session | undefined;
60
+ set(session: Session | undefined): void;
61
+ } {
62
+ let session: Session | undefined;
63
+ let setByGame = false;
64
+ let readOnce = false;
65
+ let development: Session | null | undefined;
66
+ let developmentHandedOut = false;
67
+ return {
68
+ resolve(options = {}) {
69
+ if (options.once === true) {
70
+ readOnce = true;
71
+ }
72
+ // The commerce client never gets the development session.
73
+ if (setByGame || options.once === true) {
74
+ return session;
75
+ }
76
+ if (development === undefined) {
77
+ development = input.development() ?? null;
78
+ }
79
+ if (development === null) {
80
+ return undefined;
81
+ }
82
+ developmentHandedOut = true;
83
+ return development;
84
+ },
85
+ set(next) {
86
+ if (developmentHandedOut && !setByGame) {
87
+ // Even `undefined`: a notifications backend that kept the development session keeps sending its header.
88
+ input.warn(
89
+ '[game-services] setStarterServerSession ran after the development session '
90
+ + '(VITE_MPGD_DEV_NOTIFICATION_SESSION) was in use; a notifications backend created '
91
+ + 'already keeps it. Unset the variable, or call setStarterServerSession before bootstrap().',
92
+ );
93
+ }
94
+ if (readOnce && next !== undefined && next !== session) {
95
+ input.warn(
96
+ '[game-services] setStarterServerSession ran after the commerce client was created; '
97
+ + "it does not send this session's header. Call it before bootstrap().",
98
+ );
99
+ }
100
+ session = next;
101
+ setByGame = true;
102
+ },
103
+ };
104
+ }
@@ -1,19 +1,37 @@
1
1
  import type { GameServicesHeaderResolver } from '@mpgd/game-services';
2
2
 
3
+ import {
4
+ createStarterServerSessionSlot,
5
+ resolveStarterDevelopmentServerSession,
6
+ } from './developmentServerSession';
7
+
3
8
  /**
4
9
  * The game's server-backed session: who is signed in to the game backend,
5
10
  * and the `Authorization` header for that session. The starter has none, so
6
11
  * `resolveStarterServerSession` returns `undefined`. Set yours with
7
- * `setStarterServerSession`, over your own session holder and identity
8
- * service. The commerce client then sends its header. In a game created with
9
- * `--providers notifications`, the notifications runtime uses the same
10
- * session and header for the game-services Worker's notification routes.
12
+ * `setStarterServerSession`. The commerce client then sends its header. In
13
+ * a game created with `--providers notifications`, the notifications
14
+ * runtime uses the same session and header for the game-services Worker's
15
+ * notification routes.
16
+ *
17
+ * With a guest-session backend, `createGuestServerSession` from
18
+ * `@mpgd/game-services` reads a `GuestSessionCoordinator` as this session:
19
+ *
20
+ * ```ts
21
+ * const guest = createGuestSessionCoordinator({ backend, credentials, installationId });
22
+ * setStarterServerSession(createGuestServerSession(guest));
23
+ * void guest.start();
24
+ * ```
25
+ *
26
+ * `sessionId` must be the stable session-family id: the same across token
27
+ * refreshes, and the session your identity service answers to the Worker's
28
+ * `authenticateNotificationSession` for this session's header. Do not pass
29
+ * a per-token or rotating id: `createGuestServerSession` answers the
30
+ * coordinator's `sessionFamilyId`, never a view's `sessionId`, which may
31
+ * rotate on refresh. With another session holder, pass its family id.
11
32
  *
12
- * `sessionId` must be the stable session-family id: the `sessionId` your
13
- * identity service answers to the Worker's `authenticateNotificationSession`
14
- * for this session's header, the same across token refreshes. Do not pass a
15
- * per-token or rotating id: a `GuestSessionCoordinator` view's `sessionId`
16
- * may rotate on refresh, so it is not that id.
33
+ * For local development without an identity service, see
34
+ * `developmentServerSession.ts`.
17
35
  */
18
36
  export interface StarterServerSession {
19
37
  /**
@@ -29,22 +47,31 @@ export interface StarterServerSession {
29
47
  readonly getHeaders: GameServicesHeaderResolver;
30
48
  }
31
49
 
32
- let serverSession: StarterServerSession | undefined;
33
- /** Whether the commerce client read the session already: it reads it once. */
34
- let readOnce = false;
50
+ /** The game's session, or the development one (see `developmentServerSession.ts`). */
51
+ const slot = createStarterServerSessionSlot<StarterServerSession>({
52
+ // `import.meta.env.DEV` is false in a normal `vite build`, which drops this branch.
53
+ development: () =>
54
+ import.meta.env.DEV
55
+ ? resolveStarterDevelopmentServerSession({
56
+ dev: import.meta.env.DEV,
57
+ mode: import.meta.env.MODE,
58
+ value: import.meta.env.VITE_MPGD_DEV_NOTIFICATION_SESSION,
59
+ })
60
+ : undefined,
61
+ warn: (message) => console.warn(message),
62
+ });
35
63
 
36
64
  /**
37
65
  * The game's server-backed session; `undefined` until the game sets one with
38
- * `setStarterServerSession`. `once` marks a reader that keeps what it reads
39
- * (the commerce client), so a later change warns.
66
+ * `setStarterServerSession`, unless a development session is on (see
67
+ * `developmentServerSession.ts`). `once` marks the reader that keeps what
68
+ * it reads, the commerce client, so a later change warns; it gets only the
69
+ * game's own session, never the development one.
40
70
  */
41
71
  export function resolveStarterServerSession(
42
72
  options: { readonly once?: boolean } = {},
43
73
  ): StarterServerSession | undefined {
44
- if (options.once === true) {
45
- readOnce = true;
46
- }
47
- return serverSession;
74
+ return slot.resolve(options);
48
75
  }
49
76
 
50
77
  /**
@@ -53,14 +80,10 @@ export function resolveStarterServerSession(
53
80
  * notification settings and opens read it until a backend exists. Its
54
81
  * `current()` may answer `undefined` until the player signs in. A session
55
82
  * set after the commerce client was created does not reach that client,
56
- * which this warns about.
83
+ * which this warns about. Any call, `undefined` included, turns the
84
+ * development session off; the first call after the development session
85
+ * was handed out warns, `undefined` included.
57
86
  */
58
87
  export function setStarterServerSession(session: StarterServerSession | undefined): void {
59
- if (readOnce && session !== undefined && session !== serverSession) {
60
- console.warn(
61
- '[game-services] setStarterServerSession ran after the commerce client was created; '
62
- + "it does not send this session's header. Call it before bootstrap().",
63
- );
64
- }
65
- serverSession = session;
88
+ slot.set(session);
66
89
  }