@anweather/game-plugin-ssdk 1.16.0 → 1.18.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.
Files changed (47) hide show
  1. package/dist-cjs/runtime/auth.js +8 -0
  2. package/dist-cjs/runtime/blobClient.js +20 -2
  3. package/dist-cjs/runtime/conformance.js +6 -0
  4. package/dist-cjs/runtime/index.js +24 -1
  5. package/dist-cjs/runtime/live/checkpoint.js +31 -0
  6. package/dist-cjs/runtime/live/conformance.js +215 -0
  7. package/dist-cjs/runtime/live/harness.js +144 -0
  8. package/dist-cjs/runtime/live/outbox.js +72 -0
  9. package/dist-cjs/runtime/live/protocol.js +227 -0
  10. package/dist-cjs/runtime/live/rooms.js +579 -0
  11. package/dist-cjs/runtime/live/server.js +105 -0
  12. package/dist-cjs/runtime/servePlugin.js +8 -2
  13. package/dist-cjs/runtime/spaceClient.js +214 -0
  14. package/dist-cjs/src/models/models_0.js +43 -1
  15. package/dist-cjs/src/protocols/Aws_restJson1.js +9 -0
  16. package/dist-cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist-es/runtime/auth.js +5 -0
  18. package/dist-es/runtime/blobClient.js +19 -2
  19. package/dist-es/runtime/conformance.js +6 -0
  20. package/dist-es/runtime/index.js +7 -1
  21. package/dist-es/runtime/live/checkpoint.js +28 -0
  22. package/dist-es/runtime/live/conformance.js +210 -0
  23. package/dist-es/runtime/live/harness.js +141 -0
  24. package/dist-es/runtime/live/outbox.js +68 -0
  25. package/dist-es/runtime/live/protocol.js +217 -0
  26. package/dist-es/runtime/live/rooms.js +575 -0
  27. package/dist-es/runtime/live/server.js +102 -0
  28. package/dist-es/runtime/servePlugin.js +8 -2
  29. package/dist-es/runtime/spaceClient.js +205 -0
  30. package/dist-es/src/models/models_0.js +42 -0
  31. package/dist-es/src/protocols/Aws_restJson1.js +9 -0
  32. package/dist-es/tsconfig.es.tsbuildinfo +1 -1
  33. package/dist-types/runtime/auth.d.ts +3 -0
  34. package/dist-types/runtime/blobClient.d.ts +27 -2
  35. package/dist-types/runtime/conformance.d.ts +6 -0
  36. package/dist-types/runtime/index.d.ts +15 -2
  37. package/dist-types/runtime/live/checkpoint.d.ts +15 -0
  38. package/dist-types/runtime/live/conformance.d.ts +24 -0
  39. package/dist-types/runtime/live/harness.d.ts +71 -0
  40. package/dist-types/runtime/live/outbox.d.ts +39 -0
  41. package/dist-types/runtime/live/protocol.d.ts +169 -0
  42. package/dist-types/runtime/live/rooms.d.ts +302 -0
  43. package/dist-types/runtime/live/server.d.ts +36 -0
  44. package/dist-types/runtime/servePlugin.d.ts +8 -0
  45. package/dist-types/runtime/spaceClient.d.ts +159 -0
  46. package/dist-types/src/models/models_0.d.ts +23 -0
  47. package/package.json +6 -3
@@ -9,6 +9,8 @@ const defaultService_1 = require("./defaultService");
9
9
  const express_2 = require("./express");
10
10
  const registration_1 = require("./registration");
11
11
  const version_1 = require("./version");
12
+ const server_1 = require("./live/server");
13
+ const auth_1 = require("./auth");
12
14
  const permissiveValidation = () => undefined;
13
15
  function createPluginApp(options) {
14
16
  const env = options.env ?? process.env;
@@ -34,7 +36,7 @@ function createPluginApp(options) {
34
36
  return next();
35
37
  if (!authToken)
36
38
  return next();
37
- if (req.header("X-Plugin-Auth") === `Bearer ${authToken}`)
39
+ if ((0, auth_1.bearerMatches)(req.header("X-Plugin-Auth"), authToken))
38
40
  return next();
39
41
  res.status(401).json({ __type: "Unauthorized", message: "missing or invalid X-Plugin-Auth" });
40
42
  });
@@ -63,10 +65,14 @@ async function servePlugin(options) {
63
65
  });
64
66
  const boundPort = server.address().port;
65
67
  log.log(`${options.gameType} plugin backend listening on :${boundPort}`);
68
+ const live = options.live
69
+ ? (0, server_1.attachLive)(server, { gameType: options.gameType, authToken: built.authToken, live: options.live, log })
70
+ : undefined;
66
71
  built.registration.start();
67
72
  let closing;
68
73
  const close = () => (closing ??= (async () => {
69
74
  await built.registration.stop();
75
+ await live?.close();
70
76
  await new Promise((resolve) => server.close(() => resolve()));
71
77
  })());
72
78
  if (options.handleSignals ?? true) {
@@ -76,5 +82,5 @@ async function servePlugin(options) {
76
82
  process.once("SIGTERM", onSignal);
77
83
  process.once("SIGINT", onSignal);
78
84
  }
79
- return { ...built, server, port: boundPort, close };
85
+ return { ...built, server, live, port: boundPort, close };
80
86
  }
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FakeSpaceClient = exports.NotASpaceMemberError = exports.MAX_MEMBERSHIP_AGE_MS = exports.SpaceApiError = void 0;
4
+ exports.isAlreadyASpace = isAlreadyASpace;
5
+ exports.createSpaceClient = createSpaceClient;
6
+ exports.createMembershipGuard = createMembershipGuard;
7
+ class SpaceApiError extends Error {
8
+ type;
9
+ status;
10
+ details;
11
+ constructor(type, status, details) {
12
+ super(`${type}: ${String(details.message ?? status)}`);
13
+ this.type = type;
14
+ this.status = status;
15
+ this.details = details;
16
+ this.name = "SpaceApiError";
17
+ }
18
+ }
19
+ exports.SpaceApiError = SpaceApiError;
20
+ function isAlreadyASpace(e) {
21
+ return e instanceof SpaceApiError && e.type === "SpaceAlreadyExists";
22
+ }
23
+ const ERROR_TYPES = new Set([
24
+ "SpaceNotFound", "InvalidSpaceImport", "SpaceImportDisabled", "SpaceAlreadyExists", "InviteCodeTaken", "PluginUnauthorized",
25
+ ]);
26
+ function createSpaceClient(options) {
27
+ const doFetch = options.fetchImpl ?? fetch;
28
+ const timeoutMs = options.timeoutMs ?? 5000;
29
+ const base = `${options.platformUrl}/internal/plugins/${encodeURIComponent(options.gameType)}`;
30
+ async function call(method, path, body) {
31
+ const headers = {};
32
+ if (options.authToken)
33
+ headers["X-Plugin-Auth"] = `Bearer ${options.authToken}`;
34
+ if (body !== undefined)
35
+ headers["Content-Type"] = "application/json";
36
+ const res = await doFetch(`${base}/${path}`, {
37
+ method,
38
+ headers,
39
+ body: body === undefined ? undefined : JSON.stringify(body),
40
+ signal: AbortSignal.timeout(timeoutMs),
41
+ });
42
+ const text = await res.text();
43
+ let parsed = {};
44
+ try {
45
+ parsed = text ? JSON.parse(text) : {};
46
+ }
47
+ catch {
48
+ }
49
+ if (res.ok)
50
+ return parsed;
51
+ const type = (res.headers.get("X-Amzn-Errortype") ?? parsed.__type ?? "");
52
+ if (ERROR_TYPES.has(type))
53
+ throw new SpaceApiError(type, res.status, parsed);
54
+ throw new Error(`spaces ${method} ${path}: ${res.status} ${text.slice(0, 200)}`);
55
+ }
56
+ return {
57
+ get: async (spaceId) => {
58
+ try {
59
+ return await call("GET", `spaces/${encodeURIComponent(spaceId)}`);
60
+ }
61
+ catch (e) {
62
+ if (e instanceof SpaceApiError && e.type === "SpaceNotFound")
63
+ return null;
64
+ throw e;
65
+ }
66
+ },
67
+ import: (spaceId, body) => call("PUT", `spaces/${encodeURIComponent(spaceId)}/import`, body),
68
+ counts: () => call("GET", "space-counts"),
69
+ };
70
+ }
71
+ exports.MAX_MEMBERSHIP_AGE_MS = 15_000;
72
+ class NotASpaceMemberError extends Error {
73
+ spaceId;
74
+ playerId;
75
+ constructor(spaceId, playerId) {
76
+ super(`${playerId} is not an ACTIVE member of Space ${spaceId}`);
77
+ this.spaceId = spaceId;
78
+ this.playerId = playerId;
79
+ this.name = "NotASpaceMemberError";
80
+ }
81
+ }
82
+ exports.NotASpaceMemberError = NotASpaceMemberError;
83
+ function createMembershipGuard(client, options = {}) {
84
+ const maxAgeMs = Math.min(options.maxAgeMs ?? exports.MAX_MEMBERSHIP_AGE_MS, exports.MAX_MEMBERSHIP_AGE_MS);
85
+ const now = options.now ?? Date.now;
86
+ const cache = new Map();
87
+ function space(spaceId) {
88
+ const t = now();
89
+ const cached = cache.get(spaceId);
90
+ if (cached && t - cached.at < maxAgeMs)
91
+ return cached.read;
92
+ for (const [id, entry] of cache)
93
+ if (t - entry.at >= maxAgeMs)
94
+ cache.delete(id);
95
+ const read = client.get(spaceId);
96
+ cache.set(spaceId, { at: t, read });
97
+ const forget = () => {
98
+ if (cache.get(spaceId)?.read === read)
99
+ cache.delete(spaceId);
100
+ };
101
+ read.then((s) => { if (s === null)
102
+ forget(); }, forget);
103
+ return read;
104
+ }
105
+ const isMember = (s, playerId) => s !== null && s.members.some((m) => m.playerId === playerId && m.status === "ACTIVE");
106
+ return {
107
+ space,
108
+ isActiveMember: async (spaceId, playerId) => isMember(await space(spaceId), playerId),
109
+ assertActiveMember: async (spaceId, playerId) => {
110
+ const s = await space(spaceId);
111
+ if (!isMember(s, playerId))
112
+ throw new NotASpaceMemberError(spaceId, playerId);
113
+ return s;
114
+ },
115
+ invalidate: (spaceId) => (spaceId === undefined ? cache.clear() : void cache.delete(spaceId)),
116
+ };
117
+ }
118
+ class FakeSpaceClient {
119
+ options;
120
+ spaces = new Map();
121
+ calls = [];
122
+ names = new Map();
123
+ importEnabled = true;
124
+ constructor(options = {}) {
125
+ this.options = options;
126
+ }
127
+ add(space) {
128
+ const at = new Date(0).toISOString();
129
+ this.spaces.set(space.id, {
130
+ space: { id: space.id, name: space.name ?? "Space", ownerId: space.ownerId, settings: space.settings ?? {}, archived: false, revision: 1 },
131
+ inviteCode: space.inviteCode ? normalizeCode(space.inviteCode) : null,
132
+ members: new Map([[space.ownerId, { playerId: space.ownerId, role: "OWNER", status: "ACTIVE", joinedAt: at, statusAt: at }]]),
133
+ });
134
+ }
135
+ setStatus(spaceId, playerId, status) {
136
+ const s = this.spaces.get(spaceId);
137
+ if (!s)
138
+ throw new Error(`no Space ${spaceId}`);
139
+ const at = new Date().toISOString();
140
+ const current = s.members.get(playerId);
141
+ s.members.set(playerId, current ? { ...current, status, statusAt: at } : { playerId, role: "MEMBER", status, joinedAt: at, statusAt: at });
142
+ s.space.revision++;
143
+ s.imported = undefined;
144
+ }
145
+ archive(spaceId) {
146
+ const s = this.spaces.get(spaceId);
147
+ if (!s)
148
+ throw new Error(`no Space ${spaceId}`);
149
+ s.space.archived = true;
150
+ s.space.revision++;
151
+ s.imported = undefined;
152
+ }
153
+ async get(spaceId) {
154
+ this.calls.push(`get ${spaceId}`);
155
+ const s = this.spaces.get(spaceId);
156
+ return s ? this.view(s) : null;
157
+ }
158
+ async import(spaceId, body) {
159
+ this.calls.push(`import ${spaceId}`);
160
+ const refuse = (type, status, details = {}) => {
161
+ throw new SpaceApiError(type, status, { message: type, ...details });
162
+ };
163
+ if (!this.importEnabled)
164
+ refuse("SpaceImportDisabled", 403);
165
+ const owners = body.members.filter((m) => m.role === "OWNER");
166
+ if (owners.length !== 1 || owners[0].playerId !== body.ownerId || owners[0].status !== "ACTIVE") {
167
+ refuse("InvalidSpaceImport", 400, { message: "exactly one ACTIVE OWNER, who is ownerId" });
168
+ }
169
+ const code = body.inviteCode ? normalizeCode(body.inviteCode) : null;
170
+ const key = JSON.stringify({ ...body, inviteCode: code, settings: body.settings ?? {} });
171
+ const existing = this.spaces.get(spaceId);
172
+ if (existing) {
173
+ if (existing.imported === key)
174
+ return { created: false, space: this.view(existing) };
175
+ refuse("SpaceAlreadyExists", 409);
176
+ }
177
+ if (this.options.players) {
178
+ const known = new Set(this.options.players);
179
+ const unknown = [...new Set([body.ownerId, ...body.members.map((m) => m.playerId)])].filter((p) => !known.has(p));
180
+ if (unknown.length > 0)
181
+ refuse("InvalidSpaceImport", 400, { unknownPlayerIds: unknown });
182
+ }
183
+ if (code && [...this.spaces.values()].some((s) => s.inviteCode === code))
184
+ refuse("InviteCodeTaken", 409);
185
+ const stored = {
186
+ space: { id: spaceId, name: body.name.trim(), ownerId: body.ownerId, settings: body.settings ?? {}, archived: false, revision: 1 },
187
+ inviteCode: code,
188
+ members: new Map(body.members.map((m) => [m.playerId, { ...m }])),
189
+ imported: key,
190
+ };
191
+ this.spaces.set(spaceId, stored);
192
+ return { created: true, space: this.view(stored) };
193
+ }
194
+ async counts() {
195
+ this.calls.push("counts");
196
+ const all = [...this.spaces.values()];
197
+ return {
198
+ spaces: all.length,
199
+ inviteCodes: all.filter((s) => s.inviteCode !== null).length,
200
+ activeMembers: all.reduce((n, s) => n + [...s.members.values()].filter((m) => m.status === "ACTIVE").length, 0),
201
+ };
202
+ }
203
+ view(s) {
204
+ const members = [...s.members.values()]
205
+ .filter((m) => m.status === "ACTIVE")
206
+ .sort((a, b) => (a.role === b.role ? a.joinedAt.localeCompare(b.joinedAt) : a.role === "OWNER" ? -1 : 1))
207
+ .map((m) => ({ playerId: m.playerId, displayName: this.names.get(m.playerId), role: m.role, status: "ACTIVE" }));
208
+ return { ...s.space, settings: { ...s.space.settings }, members };
209
+ }
210
+ }
211
+ exports.FakeSpaceClient = FakeSpaceClient;
212
+ function normalizeCode(code) {
213
+ return code.toUpperCase().replace(/[\s\-_.]/g, "");
214
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ValidateMoveInput = exports.MoveValidation = exports.SelectBotMoveOutput = exports.SelectBotMoveInput = exports.IsOutOfTurnMoveOutput = exports.IsOutOfTurnMoveInput = exports.IsConfigMoveOutput = exports.IsConfigMoveInput = exports.InitializeGameInput = exports.HealthOutput = exports.GetSaveFileEditorStateOutput = exports.GetSaveFileEditorStateInput = exports.GetPlayerViewOutput = exports.GetPlayerViewInput = exports.GameTypeMetadata = exports.GameStatusOutput = exports.PluginStateOnlyInput = exports.GameResultOutput = exports.Outcome = exports.DescribeSaveFileOutput = exports.DescribeSaveFileInput = exports.BotSkillLevelEntry = exports.AssignParticipantsOutput = exports.AssignParticipantsInput = exports.SaveFileEditRejectedError = exports.ApplySaveFileEditOutput = exports.ApplySaveFileEditInput = exports.PluginStateUpdate = exports.InvalidMoveError = exports.ApplyMoveInput = exports.Move = void 0;
3
+ exports.ValidateMoveInput = exports.MoveValidation = exports.SelectBotMoveOutput = exports.SelectBotMoveInput = exports.IsOutOfTurnMoveOutput = exports.IsOutOfTurnMoveInput = exports.IsConfigMoveOutput = exports.IsConfigMoveInput = exports.InitializeGameInput = exports.HealthOutput = exports.GetSaveFileEditorStateOutput = exports.GetSaveFileEditorStateInput = exports.GetPlayerViewOutput = exports.GetPlayerViewInput = exports.GameTypeMetadata = exports.SpaceCapability = exports.GameStatusOutput = exports.PluginStateOnlyInput = exports.GameResultOutput = exports.Outcome = exports.DescribeSaveFileOutput = exports.DescribeSaveFileInput = exports.BotSkillLevelEntry = exports.AssignParticipantsOutput = exports.AssignParticipantsInput = exports.SaveFileEditRejectedError = exports.ApplySaveFileEditOutput = exports.ApplySaveFileEditInput = exports.PluginStateUpdate = exports.InvalidMoveError = exports.ApplyMoveInput = exports.Move = void 0;
4
4
  const server_common_1 = require("@aws-smithy/server-common");
5
5
  var Move;
6
6
  (function (Move) {
@@ -411,6 +411,43 @@ var GameStatusOutput;
411
411
  ];
412
412
  };
413
413
  })(GameStatusOutput || (exports.GameStatusOutput = GameStatusOutput = {}));
414
+ var SpaceCapability;
415
+ (function (SpaceCapability) {
416
+ const memberValidators = {};
417
+ SpaceCapability.validate = (obj, path = "") => {
418
+ function getMemberValidator(member) {
419
+ if (memberValidators[member] === undefined) {
420
+ switch (member) {
421
+ case "maxMembers": {
422
+ memberValidators["maxMembers"] = new server_common_1.CompositeValidator([
423
+ new server_common_1.RequiredValidator(),
424
+ ]);
425
+ break;
426
+ }
427
+ case "label": {
428
+ memberValidators["label"] = new server_common_1.NoOpValidator();
429
+ break;
430
+ }
431
+ case "settingsSchema": {
432
+ memberValidators["settingsSchema"] = new server_common_1.NoOpValidator();
433
+ break;
434
+ }
435
+ case "gameConfig": {
436
+ memberValidators["gameConfig"] = new server_common_1.NoOpValidator();
437
+ break;
438
+ }
439
+ }
440
+ }
441
+ return memberValidators[member];
442
+ }
443
+ return [
444
+ ...getMemberValidator("maxMembers").validate(obj.maxMembers, `${path}/maxMembers`),
445
+ ...getMemberValidator("label").validate(obj.label, `${path}/label`),
446
+ ...getMemberValidator("settingsSchema").validate(obj.settingsSchema, `${path}/settingsSchema`),
447
+ ...getMemberValidator("gameConfig").validate(obj.gameConfig, `${path}/gameConfig`),
448
+ ];
449
+ };
450
+ })(SpaceCapability || (exports.SpaceCapability = SpaceCapability = {}));
414
451
  var GameTypeMetadata;
415
452
  (function (GameTypeMetadata) {
416
453
  const memberValidators = {};
@@ -490,6 +527,10 @@ var GameTypeMetadata;
490
527
  memberValidators["hasSaveFileEditor"] = new server_common_1.NoOpValidator();
491
528
  break;
492
529
  }
530
+ case "spaces": {
531
+ memberValidators["spaces"] = new server_common_1.CompositeStructureValidator(new server_common_1.NoOpValidator(), SpaceCapability.validate);
532
+ break;
533
+ }
493
534
  }
494
535
  }
495
536
  return memberValidators[member];
@@ -510,6 +551,7 @@ var GameTypeMetadata;
510
551
  ...getMemberValidator("hasSaveFiles").validate(obj.hasSaveFiles, `${path}/hasSaveFiles`),
511
552
  ...getMemberValidator("hasSaveSummary").validate(obj.hasSaveSummary, `${path}/hasSaveSummary`),
512
553
  ...getMemberValidator("hasSaveFileEditor").validate(obj.hasSaveFileEditor, `${path}/hasSaveFileEditor`),
554
+ ...getMemberValidator("spaces").validate(obj.spaces, `${path}/spaces`),
513
555
  ];
514
556
  };
515
557
  })(GameTypeMetadata || (exports.GameTypeMetadata = GameTypeMetadata = {}));
@@ -642,6 +642,7 @@ const serializeGetMetadataResponse = async (input, ctx) => {
642
642
  'id': [],
643
643
  'maxPlayers': [],
644
644
  'minPlayers': [],
645
+ 'spaces': _ => se_SpaceCapability(_, context),
645
646
  'spectatorSafe': [],
646
647
  'teamPlay': [],
647
648
  }));
@@ -1054,6 +1055,14 @@ const se_BotSkillLevelList = (input, context) => {
1054
1055
  const se_ParticipantIdList = (input, context) => {
1055
1056
  return input.filter((e) => e != null);
1056
1057
  };
1058
+ const se_SpaceCapability = (input, context) => {
1059
+ return (0, smithy_client_1.take)(input, {
1060
+ 'gameConfig': _ => se_Document(_, context),
1061
+ 'label': [],
1062
+ 'maxMembers': [],
1063
+ 'settingsSchema': _ => se_Document(_, context),
1064
+ });
1065
+ };
1057
1066
  const se_Document = (input, context) => {
1058
1067
  return input;
1059
1068
  };