@mpgd/cli 0.50.1 → 0.51.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "81660b13605730116d99c58c4ab7d3deb62586d1",
2
+ "kitGitSha": "f3eb7e939f75b2736dcea741a112b50a3de096fa",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.50.1"
4
+ "packageVersion": "0.51.0"
5
5
  }
@@ -6895,6 +6895,76 @@ var maximumLeaseTtlMs = 60 * 60 * 1e3;
6895
6895
  // node_modules/.cache/mpgd-native-build/packages/game-services/src/verified-leaderboard.js
6896
6896
  var timestampPattern = new RegExp("^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,3}))?(Z|([+-])(\\d{2}):(\\d{2}))$");
6897
6897
 
6898
+ // node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/fcm.js
6899
+ var fcmAddressKinds = Object.freeze(["fcm-token"]);
6900
+ var fcmReservedDataKeys = Object.freeze(["topic", "route"]);
6901
+ var tokenRefreshMarginMs = 5 * 60 * 1e3;
6902
+
6903
+ // node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/registry.js
6904
+ var notificationHistoryRetentionDays = 30;
6905
+ var maxHistoryWindowSeconds = notificationHistoryRetentionDays * 86400;
6906
+
6907
+ // node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/store.js
6908
+ var notificationAmbiguousReplayHours = 24;
6909
+ var ambiguousReplayMs = notificationAmbiguousReplayHours * 36e5;
6910
+
6911
+ // node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/service.js
6912
+ var secondMs = 1e3;
6913
+ var dayMs2 = 86400 * secondMs;
6914
+ var keyRetentionMs = 30 * dayMs2;
6915
+ var endpointStaleMs = 60 * dayMs2;
6916
+ var endpointPurgeDelayMs = 30 * dayMs2;
6917
+ var historyRetentionMs = notificationHistoryRetentionDays * dayMs2;
6918
+ var ambiguousReplayMs2 = notificationAmbiguousReplayHours * 36e5;
6919
+ var maxTextLength = 2048;
6920
+ var maxRowKeyLength = maxTextLength + 1 + 9 * maxTextLength;
6921
+ var schedulerRetryMs = 60 * secondMs;
6922
+ var releaseBackoffMs = 60 * secondMs;
6923
+
6924
+ // node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/redis-notification-store.js
6925
+ var defaultPurgeLimit = Number.POSITIVE_INFINITY;
6926
+
6927
+ // node_modules/.cache/mpgd-native-build/adapters/devvit/src/notifications-contract.js
6928
+ var devvitHostCodeCharacters = "[A-Za-z0-9_.:-]{1,128}";
6929
+
6930
+ // node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/notifications-delivery.js
6931
+ var devvitNotificationProviderCodes = Object.freeze({
6932
+ deviceRecipient: "DEVVIT_DEVICE_RECIPIENT",
6933
+ recipientInvalid: "DEVVIT_RECIPIENT_INVALID",
6934
+ linkInvalid: "DEVVIT_LINK_INVALID",
6935
+ textMissing: "DEVVIT_TEXT_MISSING",
6936
+ titleTooLong: "DEVVIT_TITLE_TOO_LONG",
6937
+ bodyTooLong: "DEVVIT_BODY_TOO_LONG",
6938
+ templateSyntax: "DEVVIT_TEMPLATE_SYNTAX",
6939
+ enqueueFailed: "DEVVIT_ENQUEUE_FAILED",
6940
+ enqueueRejected: "DEVVIT_ENQUEUE_REJECTED",
6941
+ enqueueUnanswered: "DEVVIT_ENQUEUE_UNANSWERED",
6942
+ enqueueUnclassified: "DEVVIT_ENQUEUE_UNCLASSIFIED"
6943
+ });
6944
+ var dailyPeriodSource = String.raw`daily|per[- ]?(?:day|user|app)|today'?s?`;
6945
+ var dailyLimitWords = new RegExp(String.raw`\b(?:${dailyPeriodSource})\b`, "iu");
6946
+ var adjacentCountedLimit = new RegExp(String.raw`\b(?:${dailyPeriodSource}|send(?:ing)?|notifications?) limits?\b`, "iu");
6947
+ var devvitNotificationSchedulerTasks = Object.freeze({
6948
+ dispatch: Object.freeze({
6949
+ name: "mpgd-notifications-dispatch",
6950
+ endpoint: "/internal/scheduler/mpgd-notifications-dispatch",
6951
+ cron: "* * * * *"
6952
+ }),
6953
+ purge: Object.freeze({
6954
+ name: "mpgd-notifications-purge",
6955
+ endpoint: "/internal/scheduler/mpgd-notifications-purge",
6956
+ cron: "*/5 * * * *"
6957
+ })
6958
+ });
6959
+
6960
+ // node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/notifications-bridge.js
6961
+ var devvitNotificationHostCodes = Object.freeze({
6962
+ optInRefused: "DEVVIT_OPT_IN_REFUSED",
6963
+ optOutRefused: "DEVVIT_OPT_OUT_REFUSED",
6964
+ pluginFailed: "DEVVIT_NOTIFICATIONS_PLUGIN_FAILED"
6965
+ });
6966
+ var hostCodeValue = new RegExp(`^${devvitHostCodeCharacters}$`, "u");
6967
+
6898
6968
  // node_modules/.cache/mpgd-native-build/packages/cli/src/target-name.js
6899
6969
  var deploymentTargetNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
6900
6970
  var reservedDeploymentTargetNames2 = /* @__PURE__ */ new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.50.1",
3
+ "version": "0.51.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,10 +70,10 @@
70
70
  "parse5": "7.3.0",
71
71
  "postcss": "8.5.28",
72
72
  "postcss-value-parser": "4.2.0",
73
- "@mpgd/catalog": "0.10.1",
74
- "@mpgd/platform": "0.23.1",
73
+ "@mpgd/catalog": "0.10.2",
75
74
  "@mpgd/phaser-assets": "0.9.1",
76
- "@mpgd/target-config": "0.23.1"
75
+ "@mpgd/platform": "0.24.0",
76
+ "@mpgd/target-config": "0.23.2"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/node": "^24.0.0",
@@ -81,18 +81,18 @@
81
81
  "@types/yauzl": "3.4.0",
82
82
  "ttsc": "0.32.1",
83
83
  "typescript": "7.0.2",
84
- "@mpgd/adapter-ait": "0.17.1",
85
- "@mpgd/adapter-browser": "0.11.1",
86
- "@mpgd/adapter-verse8": "0.7.1",
87
- "@mpgd/adapter-devvit": "0.12.1",
88
- "@mpgd/analytics": "0.4.10",
89
- "@mpgd/adapter-capacitor": "0.10.1",
90
- "@mpgd/bridge": "0.13.1",
91
- "@mpgd/capacitor-play-billing": "0.3.1",
92
- "@mpgd/game-runtime": "0.7.1",
93
- "@mpgd/capacitor-storekit": "0.3.1",
94
- "@mpgd/game-services": "0.24.1",
95
- "@mpgd/i18n": "0.6.15",
84
+ "@mpgd/adapter-ait": "0.18.0",
85
+ "@mpgd/adapter-capacitor": "0.11.0",
86
+ "@mpgd/adapter-devvit": "0.13.0",
87
+ "@mpgd/adapter-browser": "0.11.2",
88
+ "@mpgd/adapter-verse8": "0.7.2",
89
+ "@mpgd/capacitor-play-billing": "0.3.2",
90
+ "@mpgd/analytics": "0.4.11",
91
+ "@mpgd/capacitor-storekit": "0.3.2",
92
+ "@mpgd/bridge": "0.13.2",
93
+ "@mpgd/game-services": "0.25.0",
94
+ "@mpgd/game-runtime": "0.7.2",
95
+ "@mpgd/i18n": "0.6.16",
96
96
  "@mpgd/lite-shell": "0.1.0"
97
97
  },
98
98
  "main": "./dist/index.js",
@@ -43,5 +43,17 @@
43
43
  "scripts": {
44
44
  "dev": "vite build --mode staging --watch",
45
45
  "build": "pnpm build"
46
+ },
47
+ "scheduler": {
48
+ "tasks": {
49
+ "mpgd-notifications-dispatch": {
50
+ "endpoint": "/internal/scheduler/mpgd-notifications-dispatch",
51
+ "cron": "* * * * *"
52
+ },
53
+ "mpgd-notifications-purge": {
54
+ "endpoint": "/internal/scheduler/mpgd-notifications-purge",
55
+ "cron": "*/5 * * * *"
56
+ }
57
+ }
46
58
  }
47
59
  }
@@ -59,6 +59,15 @@ export interface DevvitBridgeCommerceHandler {
59
59
  request(input: BridgeRequest): Promise<BridgeResponse>;
60
60
  }
61
61
 
62
+ /**
63
+ * Serves the nine `notifications.v2.*` bridge methods:
64
+ * `createDevvitNotificationsV2BridgeHandler` from `@mpgd/adapter-devvit/server`
65
+ * over the Devvit `notifications` plugin and the request's logged-in user.
66
+ */
67
+ export interface DevvitBridgeNotificationsHandler {
68
+ request(input: BridgeRequest): Promise<BridgeResponse>;
69
+ }
70
+
62
71
  export interface DevvitBridgeHandlerDependencies {
63
72
  readonly redis: DevvitBridgeRedisLike;
64
73
  /** Authenticated Reddit user ID from the Devvit request context, never from the payload. */
@@ -75,6 +84,13 @@ export interface DevvitBridgeHandlerDependencies {
75
84
  * plugin wired never lists orders or products.
76
85
  */
77
86
  readonly commerce?: DevvitBridgeCommerceHandler | undefined;
87
+ /**
88
+ * Notifications v2: the game-rendered opt-in and opt-out. Absent, every
89
+ * `notifications.v2.*` method answers `NOTIFICATIONS_V2_UNAVAILABLE`, and
90
+ * the v1 `notifications.*` answers stay `approval-required` and
91
+ * `unavailable`.
92
+ */
93
+ readonly notifications?: DevvitBridgeNotificationsHandler | undefined;
78
94
  readonly warn?: ((message: string) => void) | undefined;
79
95
  }
80
96
 
@@ -93,11 +109,25 @@ export function createDevvitBridgeHandler(
93
109
  }
94
110
 
95
111
  return async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse> {
96
- // Notifications v2 arrives with the Devvit provider (P3), which serves the
97
- // game-rendered opt-in from this bridge; until then every v2 method
98
- // answers the dedicated code, and the v1 `notifications.*` answers stay.
112
+ // Notifications v2: the configured handler serves the game-rendered
113
+ // opt-in; without one every v2 method answers the dedicated code. The v1
114
+ // `notifications.*` answers below stay; a gateway that exposes v2 serves
115
+ // v1 from it instead.
99
116
  if (isBridgeNotificationsV2Method(input.method)) {
100
- return createNotificationsV2UnavailableBridgeError(input, 'Devvit server bridge');
117
+ if (dependencies.notifications === undefined) {
118
+ return createNotificationsV2UnavailableBridgeError(input, 'Devvit server bridge');
119
+ }
120
+ try {
121
+ return await dependencies.notifications.request(input);
122
+ } catch (error) {
123
+ warn(`devvit notifications ${input.method} failed: ${errorMessage(error)}`);
124
+ return createBridgeError(
125
+ input.id,
126
+ 'NOTIFICATIONS_TEMPORARILY_UNAVAILABLE',
127
+ `${input.method} failed on the Devvit server.`,
128
+ true,
129
+ );
130
+ }
101
131
  }
102
132
  switch (input.method) {
103
133
  case 'runtime.getCapabilities':
@@ -13,6 +13,7 @@ import productCatalogJson from '@mpgd/catalog/catalog.json';
13
13
 
14
14
  import devvitManifest from '../../devvit.json';
15
15
  import { createDevvitBridgeHandler } from './bridge.js';
16
+ import { handleNotificationTask, notificationTaskForPath } from './notificationTasks.js';
16
17
 
17
18
  const maxRequestBodySize = 1_048_576;
18
19
  const gameName = '__GAME_NAME__';
@@ -45,6 +46,12 @@ const handleBridgeRequest = createDevvitBridgeHandler({
45
46
  storageKeyNamespace: gameName,
46
47
  ...(commerce === undefined ? {} : { commerce }),
47
48
  });
49
+ /**
50
+ * The notifications v2 service (`createDevvitNotificationService` from
51
+ * `@mpgd/adapter-devvit/server`). None until the game declares a topic
52
+ * catalog, so the scheduler tasks below do no work.
53
+ */
54
+ const notificationService: Parameters<typeof handleNotificationTask>[0]['notifications'] = undefined;
48
55
  const bridgeRpcHandler = createBridgeRpcNodeHandler(createBridgeRpcRouter(handleBridgeRequest), {
49
56
  maxBodySize: maxRequestBodySize,
50
57
  prefix: defaultBridgeRpcEndpoint,
@@ -57,17 +64,7 @@ async function handleHttpRequest(
57
64
  setResponseSecurityHeaders(response);
58
65
 
59
66
  if (request.method === 'POST' && requestPathname(request) === '/internal/menu/create-post') {
60
- try {
61
- await drainRequestBody(request, maxRequestBodySize);
62
- } catch (error) {
63
- if (!(error instanceof RequestBodyTooLargeError)) {
64
- throw error;
65
- }
66
-
67
- discardOversizedRequestBody(request, response);
68
- sendJson(response, 413, {
69
- error: 'REQUEST_BODY_TOO_LARGE',
70
- });
67
+ if (!(await drainOrRejectBody(request, response))) {
71
68
  return;
72
69
  }
73
70
 
@@ -75,6 +72,21 @@ async function handleHttpRequest(
75
72
  return;
76
73
  }
77
74
 
75
+ const notificationTask = request.method === 'POST'
76
+ ? notificationTaskForPath(requestPathname(request))
77
+ : undefined;
78
+ if (notificationTask !== undefined) {
79
+ if (!(await drainOrRejectBody(request, response))) {
80
+ return;
81
+ }
82
+ const answer = await handleNotificationTask({
83
+ task: notificationTask,
84
+ notifications: notificationService,
85
+ });
86
+ sendJson(response, answer.status, answer.body);
87
+ return;
88
+ }
89
+
78
90
  if (await bridgeRpcHandler(request, response)) {
79
91
  return;
80
92
  }
@@ -220,6 +232,21 @@ async function* readRequestBodyChunks(
220
232
  }
221
233
  }
222
234
 
235
+ /** Drains a bounded internal request body; answers 413 and false when it is too large. */
236
+ async function drainOrRejectBody(request: IncomingMessage, response: ServerResponse): Promise<boolean> {
237
+ try {
238
+ await drainRequestBody(request, maxRequestBodySize);
239
+ return true;
240
+ } catch (error) {
241
+ if (!(error instanceof RequestBodyTooLargeError)) {
242
+ throw error;
243
+ }
244
+ discardOversizedRequestBody(request, response);
245
+ sendJson(response, 413, { error: 'REQUEST_BODY_TOO_LARGE' });
246
+ return false;
247
+ }
248
+ }
249
+
223
250
  async function drainRequestBody(
224
251
  request: IncomingMessage,
225
252
  maxBodySize: number,
@@ -0,0 +1,64 @@
1
+ import {
2
+ devvitNotificationTaskForEndpoint,
3
+ runDevvitNotificationTask,
4
+ type DevvitNotificationTask,
5
+ type DevvitNotificationTaskResult,
6
+ } from '@mpgd/adapter-devvit/server';
7
+
8
+ /**
9
+ * The notifications scheduler tasks of `devvit.json` (`scheduler.tasks`):
10
+ * `mpgd-notifications-dispatch` runs the notifications scheduler every
11
+ * minute (due server reminders and due sends, starting no round after 20
12
+ * seconds) and `mpgd-notifications-purge` purges old rows in bounded
13
+ * batches every five minutes (`runDevvitNotificationTask` from
14
+ * `@mpgd/adapter-devvit/server`).
15
+ * A server without a notifications service (no topic catalog yet) answers
16
+ * the tasks without work.
17
+ */
18
+ export interface NotificationTaskAnswer {
19
+ readonly status: number;
20
+ /** A `TaskResponse` (`{}`) from `@devvit/web/server`. */
21
+ readonly body: Record<string, never>;
22
+ readonly task: DevvitNotificationTask;
23
+ readonly result?: DevvitNotificationTaskResult;
24
+ }
25
+
26
+ /** The task an internal scheduler path names, or `undefined` for any other path. */
27
+ export function notificationTaskForPath(pathname: string): DevvitNotificationTask | undefined {
28
+ return devvitNotificationTaskForEndpoint(pathname);
29
+ }
30
+
31
+ /**
32
+ * Runs `task`. A failure answers 500 (so the Devvit
33
+ * logs show it) and is logged; the next run tries again.
34
+ */
35
+ export async function handleNotificationTask(input: {
36
+ readonly task: DevvitNotificationTask;
37
+ /** The service's `runScheduler` and `purge` (`createDevvitNotificationService(...).service`). */
38
+ readonly notifications: Parameters<typeof runDevvitNotificationTask>[0] | undefined;
39
+ readonly log?: (message: string, error?: unknown) => void;
40
+ }): Promise<NotificationTaskAnswer> {
41
+ const log = input.log ?? ((message: string, error?: unknown) => {
42
+ if (error === undefined) {
43
+ console.log(message);
44
+ } else {
45
+ console.error(message, error);
46
+ }
47
+ });
48
+ if (input.notifications === undefined) {
49
+ return { status: 200, body: {}, task: input.task };
50
+ }
51
+ try {
52
+ const result = await runDevvitNotificationTask(input.notifications, input.task);
53
+ if (
54
+ (result.task === 'dispatch' && (result.touched > 0 || result.failed > 0))
55
+ || (result.task === 'purge' && result.sends + result.history + result.endpoints > 0)
56
+ ) {
57
+ log(`devvit notifications ${result.task}: ${JSON.stringify(result)}`);
58
+ }
59
+ return { status: 200, body: {}, task: input.task, result };
60
+ } catch (error) {
61
+ log(`devvit notifications ${input.task} task failed`, error);
62
+ return { status: 500, body: {}, task: input.task };
63
+ }
64
+ }