@magicweave/core 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1495 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/client.ts
8
+ import createOpenapiClient from "openapi-fetch";
9
+ var STAGING_API_BASE = "https://console.staging.magicweave.xyz/api";
10
+ var PROD_API_BASE = "https://console.magicweave.xyz/api";
11
+ function resolveBaseUrl(explicit) {
12
+ return explicit ?? process.env.MW_API_BASE ?? STAGING_API_BASE;
13
+ }
14
+ var MagicweaveError = class extends Error {
15
+ constructor(message, status, code, detail) {
16
+ super(message);
17
+ this.status = status;
18
+ this.code = code;
19
+ this.detail = detail;
20
+ this.name = "MagicweaveError";
21
+ }
22
+ status;
23
+ code;
24
+ detail;
25
+ };
26
+ var UnauthorizedError = class extends MagicweaveError {
27
+ constructor(detail) {
28
+ super(
29
+ "Not authenticated or your session expired. Run `mw login`.",
30
+ 401,
31
+ "unauthorized",
32
+ detail
33
+ );
34
+ this.name = "UnauthorizedError";
35
+ }
36
+ };
37
+ var NotFoundError = class extends MagicweaveError {
38
+ constructor(detail) {
39
+ super("Not found.", 404, "not_found", detail);
40
+ this.name = "NotFoundError";
41
+ }
42
+ };
43
+ var ValidationError = class extends MagicweaveError {
44
+ constructor(detail) {
45
+ super(formatValidationDetail(detail), 422, "validation_error", detail);
46
+ this.name = "ValidationError";
47
+ }
48
+ };
49
+ function formatValidationDetail(detail) {
50
+ const list = detail?.detail;
51
+ if (Array.isArray(list) && list.length) {
52
+ return list.map((e) => {
53
+ const field = Array.isArray(e.loc) ? e.loc.filter((p) => p !== "body").join(".") : "";
54
+ return field ? `${field}: ${e.msg}` : e.msg ?? "invalid";
55
+ }).join("; ");
56
+ }
57
+ if (typeof detail?.detail === "string") {
58
+ return detail.detail;
59
+ }
60
+ return "Request validation failed.";
61
+ }
62
+ function errorFromResponse(response, body) {
63
+ switch (response.status) {
64
+ case 401:
65
+ return new UnauthorizedError(body);
66
+ case 404:
67
+ return new NotFoundError(body);
68
+ case 422:
69
+ return new ValidationError(body);
70
+ default: {
71
+ const msg = body?.detail ?? `Request failed: ${response.status} ${response.statusText}`;
72
+ return new MagicweaveError(
73
+ typeof msg === "string" ? msg : `Request failed: ${response.status}`,
74
+ response.status,
75
+ "http_error",
76
+ body
77
+ );
78
+ }
79
+ }
80
+ }
81
+ async function unwrap(promise) {
82
+ const { data, error, response } = await promise;
83
+ if (response.ok) return data;
84
+ throw errorFromResponse(response, error ?? data);
85
+ }
86
+ var MagicweaveClient = class {
87
+ raw;
88
+ baseUrl;
89
+ constructor(opts = {}) {
90
+ this.baseUrl = resolveBaseUrl(opts.baseUrl);
91
+ const tokenProvider = opts.tokenProvider;
92
+ const auth = {
93
+ async onRequest({ request }) {
94
+ const token = tokenProvider ? await tokenProvider.getToken() : null;
95
+ if (token) request.headers.set("Authorization", `Bearer ${token}`);
96
+ return request;
97
+ }
98
+ };
99
+ this.raw = createOpenapiClient({
100
+ baseUrl: this.baseUrl,
101
+ fetch: opts.fetch
102
+ });
103
+ this.raw.use(auth);
104
+ }
105
+ };
106
+ function createClient(opts = {}) {
107
+ return new MagicweaveClient(opts);
108
+ }
109
+
110
+ // src/auth.ts
111
+ async function requestLoginOtp(baseUrl, email, opts = {}) {
112
+ const f = opts.fetch ?? globalThis.fetch;
113
+ const res = await f(`${baseUrl}/auth/login-email/`, {
114
+ method: "POST",
115
+ headers: { "Content-Type": "application/json" },
116
+ body: JSON.stringify({ email })
117
+ });
118
+ if (!res.ok) {
119
+ throw new MagicweaveError(
120
+ `Could not send a login code to ${email} (HTTP ${res.status}).`,
121
+ res.status,
122
+ "otp_request_failed"
123
+ );
124
+ }
125
+ }
126
+ async function verifyLoginOtp(baseUrl, email, otp, opts = {}) {
127
+ const f = opts.fetch ?? globalThis.fetch;
128
+ const res = await f(`${baseUrl}/auth/verify-email-otp/`, {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/json" },
131
+ body: JSON.stringify({ email, otp })
132
+ });
133
+ const body = await res.json().catch(() => ({}));
134
+ if (!res.ok || !body.access_token || !body.refresh_token) {
135
+ throw new MagicweaveError(
136
+ body.detail ?? "That code was not accepted. Check it and try again.",
137
+ res.status,
138
+ "otp_verify_failed",
139
+ body
140
+ );
141
+ }
142
+ return { access_token: body.access_token, refresh_token: body.refresh_token };
143
+ }
144
+ function decodeJwt(token) {
145
+ const part = token.split(".")[1];
146
+ if (!part) return null;
147
+ try {
148
+ return JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ function isExpired(token, skewSeconds = 30) {
154
+ const exp = decodeJwt(token)?.exp;
155
+ if (!exp) return true;
156
+ return exp * 1e3 <= Date.now() + skewSeconds * 1e3;
157
+ }
158
+ var StaticTokenProvider = class {
159
+ constructor(token) {
160
+ this.token = token;
161
+ }
162
+ token;
163
+ getToken() {
164
+ return this.token;
165
+ }
166
+ };
167
+ var EnvTokenProvider = class {
168
+ getToken() {
169
+ return process.env.MW_ACCESS_TOKEN ?? null;
170
+ }
171
+ };
172
+
173
+ // src/resources/orgs.ts
174
+ function orgs(client) {
175
+ return {
176
+ list: () => unwrap(client.raw.GET("/org/")),
177
+ create: (name) => unwrap(client.raw.POST("/org/", { body: { name } })),
178
+ remove: (orgId) => unwrap(client.raw.DELETE("/org/{org_id}/", { params: { path: { org_id: orgId } } }))
179
+ };
180
+ }
181
+
182
+ // src/resources/projects.ts
183
+ function projects(client) {
184
+ return {
185
+ list: (orgId) => unwrap(client.raw.GET("/project/{org_id}", { params: { path: { org_id: orgId } } })),
186
+ create: (orgId, name) => unwrap(
187
+ client.raw.POST("/project/{org_id}", {
188
+ params: { path: { org_id: orgId } },
189
+ body: { name }
190
+ })
191
+ ),
192
+ remove: (orgId, projectId) => unwrap(
193
+ client.raw.DELETE("/project/{org_id}/{project_id}/", {
194
+ params: { path: { org_id: orgId, project_id: projectId } }
195
+ })
196
+ ),
197
+ enableNetwork: (orgId, projectId) => unwrap(
198
+ client.raw.POST("/project/{org_id}/{project_id}/enable-network", {
199
+ params: { path: { org_id: orgId, project_id: projectId } }
200
+ })
201
+ )
202
+ };
203
+ }
204
+
205
+ // src/resources/environments.ts
206
+ function environments(client) {
207
+ const base = (orgId, projectId) => ({
208
+ path: { org_id: orgId, project_id: projectId }
209
+ });
210
+ const withEnv = (orgId, projectId, environmentId) => ({
211
+ path: { org_id: orgId, project_id: projectId, environment_id: environmentId }
212
+ });
213
+ return {
214
+ list: (orgId, projectId) => unwrap(
215
+ client.raw.GET("/project/{org_id}/{project_id}/environments/", {
216
+ params: base(orgId, projectId)
217
+ })
218
+ ),
219
+ create: (orgId, projectId, name) => unwrap(
220
+ client.raw.POST("/project/{org_id}/{project_id}/environments/", {
221
+ params: base(orgId, projectId),
222
+ body: { name }
223
+ })
224
+ ),
225
+ remove: (orgId, projectId, environmentId) => unwrap(
226
+ client.raw.DELETE("/project/{org_id}/{project_id}/environments/{environment_id}", {
227
+ params: withEnv(orgId, projectId, environmentId)
228
+ })
229
+ ),
230
+ rotateSecret: (orgId, projectId, environmentId) => unwrap(
231
+ client.raw.POST(
232
+ "/project/{org_id}/{project_id}/environments/{environment_id}/rotate-secret",
233
+ {
234
+ params: withEnv(orgId, projectId, environmentId)
235
+ }
236
+ )
237
+ ),
238
+ rotatePreviewSecret: (orgId, projectId, environmentId) => unwrap(
239
+ client.raw.POST(
240
+ "/project/{org_id}/{project_id}/environments/{environment_id}/rotate-preview-secret",
241
+ { params: withEnv(orgId, projectId, environmentId) }
242
+ )
243
+ )
244
+ };
245
+ }
246
+
247
+ // src/resources/releases.ts
248
+ function releases(client) {
249
+ const rel = (orgId, projectId, releaseId) => ({
250
+ path: { org_id: orgId, project_id: projectId, release_id: releaseId }
251
+ });
252
+ return {
253
+ list: (orgId, projectId, environmentId) => unwrap(
254
+ client.raw.GET("/project/{org_id}/{project_id}/environments/{environment_id}/releases/", {
255
+ params: { path: { org_id: orgId, project_id: projectId, environment_id: environmentId } }
256
+ })
257
+ ),
258
+ create: (orgId, projectId, environmentId, body = { release_notes: "" }) => unwrap(
259
+ client.raw.POST("/project/{org_id}/{project_id}/environments/{environment_id}/releases/", {
260
+ params: { path: { org_id: orgId, project_id: projectId, environment_id: environmentId } },
261
+ body
262
+ })
263
+ ),
264
+ update: (orgId, projectId, releaseId, body) => unwrap(
265
+ client.raw.PATCH("/project/{org_id}/{project_id}/releases/{release_id}", {
266
+ params: rel(orgId, projectId, releaseId),
267
+ body
268
+ })
269
+ ),
270
+ remove: (orgId, projectId, releaseId) => unwrap(
271
+ client.raw.DELETE("/project/{org_id}/{project_id}/releases/{release_id}", {
272
+ params: rel(orgId, projectId, releaseId)
273
+ })
274
+ ),
275
+ pushItem: (orgId, projectId, releaseId, body) => unwrap(
276
+ client.raw.POST("/project/{org_id}/{project_id}/releases/{release_id}/items/", {
277
+ params: rel(orgId, projectId, releaseId),
278
+ body
279
+ })
280
+ ),
281
+ removeItem: (orgId, projectId, releaseId, itemId) => unwrap(
282
+ client.raw.DELETE("/project/{org_id}/{project_id}/releases/{release_id}/items/{item_id}", {
283
+ params: {
284
+ path: { org_id: orgId, project_id: projectId, release_id: releaseId, item_id: itemId }
285
+ }
286
+ })
287
+ ),
288
+ lock: (orgId, projectId, releaseId) => unwrap(
289
+ client.raw.POST("/project/{org_id}/{project_id}/releases/{release_id}/lock", {
290
+ params: rel(orgId, projectId, releaseId)
291
+ })
292
+ ),
293
+ activate: (orgId, projectId, releaseId) => unwrap(
294
+ client.raw.POST("/project/{org_id}/{project_id}/releases/{release_id}/activate", {
295
+ params: rel(orgId, projectId, releaseId)
296
+ })
297
+ ),
298
+ deactivate: (orgId, projectId, releaseId) => unwrap(
299
+ client.raw.POST("/project/{org_id}/{project_id}/releases/{release_id}/deactivate", {
300
+ params: rel(orgId, projectId, releaseId)
301
+ })
302
+ ),
303
+ move: (orgId, projectId, releaseId, targetEnvironmentId, note = "") => unwrap(
304
+ client.raw.POST("/project/{org_id}/{project_id}/releases/{release_id}/move", {
305
+ params: rel(orgId, projectId, releaseId),
306
+ body: { target_environment_id: targetEnvironmentId, note }
307
+ })
308
+ ),
309
+ copy: (orgId, projectId, releaseId, targetEnvironmentId, note = "") => unwrap(
310
+ client.raw.POST("/project/{org_id}/{project_id}/releases/{release_id}/copy", {
311
+ params: rel(orgId, projectId, releaseId),
312
+ body: { target_environment_id: targetEnvironmentId, note }
313
+ })
314
+ )
315
+ };
316
+ }
317
+
318
+ // src/resources/currencies.ts
319
+ function currencies(client) {
320
+ const proj = (orgId, projectId) => ({
321
+ path: { org_id: orgId, project_id: projectId }
322
+ });
323
+ const one = (orgId, projectId, definitionId) => ({
324
+ path: { org_id: orgId, project_id: projectId, definition_id: definitionId }
325
+ });
326
+ return {
327
+ list: (orgId, projectId) => unwrap(
328
+ client.raw.GET("/currency-definitions/{org_id}/{project_id}/", {
329
+ params: proj(orgId, projectId)
330
+ })
331
+ ),
332
+ overview: (orgId, projectId) => unwrap(
333
+ client.raw.GET("/currency-definitions/{org_id}/{project_id}/overview/", {
334
+ params: proj(orgId, projectId)
335
+ })
336
+ ),
337
+ get: (orgId, projectId, definitionId) => unwrap(
338
+ client.raw.GET("/currency-definitions/{org_id}/{project_id}/{definition_id}/", {
339
+ params: one(orgId, projectId, definitionId)
340
+ })
341
+ ),
342
+ create: (orgId, projectId, body) => unwrap(
343
+ client.raw.POST("/currency-definitions/{org_id}/{project_id}/", {
344
+ params: proj(orgId, projectId),
345
+ body
346
+ })
347
+ ),
348
+ update: (orgId, projectId, definitionId, body) => unwrap(
349
+ client.raw.PATCH("/currency-definitions/{org_id}/{project_id}/{definition_id}/", {
350
+ params: one(orgId, projectId, definitionId),
351
+ body
352
+ })
353
+ ),
354
+ remove: (orgId, projectId, definitionId) => unwrap(
355
+ client.raw.DELETE("/currency-definitions/{org_id}/{project_id}/{definition_id}/", {
356
+ params: one(orgId, projectId, definitionId)
357
+ })
358
+ )
359
+ };
360
+ }
361
+
362
+ // src/resources/stats.ts
363
+ function stats(client) {
364
+ const proj = (orgId, projectId) => ({
365
+ path: { org_id: orgId, project_id: projectId }
366
+ });
367
+ const one = (orgId, projectId, definitionId) => ({
368
+ path: { org_id: orgId, project_id: projectId, definition_id: definitionId }
369
+ });
370
+ return {
371
+ list: (orgId, projectId) => unwrap(
372
+ client.raw.GET("/stat-definitions/{org_id}/{project_id}/", {
373
+ params: proj(orgId, projectId)
374
+ })
375
+ ),
376
+ overview: (orgId, projectId) => unwrap(
377
+ client.raw.GET("/stat-definitions/{org_id}/{project_id}/overview/", {
378
+ params: proj(orgId, projectId)
379
+ })
380
+ ),
381
+ get: (orgId, projectId, definitionId) => unwrap(
382
+ client.raw.GET("/stat-definitions/{org_id}/{project_id}/{definition_id}/", {
383
+ params: one(orgId, projectId, definitionId)
384
+ })
385
+ ),
386
+ create: (orgId, projectId, body) => unwrap(
387
+ client.raw.POST("/stat-definitions/{org_id}/{project_id}/", {
388
+ params: proj(orgId, projectId),
389
+ body
390
+ })
391
+ ),
392
+ update: (orgId, projectId, definitionId, body) => unwrap(
393
+ client.raw.PATCH("/stat-definitions/{org_id}/{project_id}/{definition_id}/", {
394
+ params: one(orgId, projectId, definitionId),
395
+ body
396
+ })
397
+ ),
398
+ remove: (orgId, projectId, definitionId) => unwrap(
399
+ client.raw.DELETE("/stat-definitions/{org_id}/{project_id}/{definition_id}/", {
400
+ params: one(orgId, projectId, definitionId)
401
+ })
402
+ )
403
+ };
404
+ }
405
+
406
+ // src/resources/manifests.ts
407
+ function manifests(client) {
408
+ const proj = (orgId, projectId) => ({
409
+ path: { org_id: orgId, project_id: projectId }
410
+ });
411
+ const one = (orgId, projectId, manifestId) => ({
412
+ path: { org_id: orgId, project_id: projectId, manifest_id: manifestId }
413
+ });
414
+ return {
415
+ list: (orgId, projectId) => unwrap(
416
+ client.raw.GET("/game-manifests/{org_id}/{project_id}/", {
417
+ params: proj(orgId, projectId)
418
+ })
419
+ ),
420
+ overview: (orgId, projectId) => unwrap(
421
+ client.raw.GET("/game-manifests/{org_id}/{project_id}/overview/", {
422
+ params: proj(orgId, projectId)
423
+ })
424
+ ),
425
+ get: (orgId, projectId, manifestId) => unwrap(
426
+ client.raw.GET("/game-manifests/{org_id}/{project_id}/{manifest_id}/", {
427
+ params: one(orgId, projectId, manifestId)
428
+ })
429
+ ),
430
+ create: (orgId, projectId, body) => unwrap(
431
+ client.raw.POST("/game-manifests/{org_id}/{project_id}/", {
432
+ params: proj(orgId, projectId),
433
+ body
434
+ })
435
+ ),
436
+ update: (orgId, projectId, manifestId, body) => unwrap(
437
+ client.raw.PATCH("/game-manifests/{org_id}/{project_id}/{manifest_id}/", {
438
+ params: one(orgId, projectId, manifestId),
439
+ body
440
+ })
441
+ ),
442
+ remove: (orgId, projectId, manifestId) => unwrap(
443
+ client.raw.DELETE("/game-manifests/{org_id}/{project_id}/{manifest_id}/", {
444
+ params: one(orgId, projectId, manifestId)
445
+ })
446
+ )
447
+ };
448
+ }
449
+
450
+ // src/resources/leaderboards.ts
451
+ function leaderboards(client) {
452
+ const proj = (orgId, projectId) => ({
453
+ path: { org_id: orgId, project_id: projectId }
454
+ });
455
+ const one = (orgId, projectId, leaderboardId) => ({
456
+ path: { org_id: orgId, project_id: projectId, leaderboard_id: leaderboardId }
457
+ });
458
+ return {
459
+ list: (orgId, projectId) => unwrap(
460
+ client.raw.GET("/leaderboards/{org_id}/{project_id}/", { params: proj(orgId, projectId) })
461
+ ),
462
+ overview: (orgId, projectId) => unwrap(
463
+ client.raw.GET("/leaderboards/{org_id}/{project_id}/overview/", {
464
+ params: proj(orgId, projectId)
465
+ })
466
+ ),
467
+ get: (orgId, projectId, leaderboardId) => unwrap(
468
+ client.raw.GET("/leaderboards/{org_id}/{project_id}/{leaderboard_id}/", {
469
+ params: one(orgId, projectId, leaderboardId)
470
+ })
471
+ ),
472
+ create: (orgId, projectId, body) => unwrap(
473
+ client.raw.POST("/leaderboards/{org_id}/{project_id}/", {
474
+ params: proj(orgId, projectId),
475
+ body
476
+ })
477
+ ),
478
+ update: (orgId, projectId, leaderboardId, body) => unwrap(
479
+ client.raw.PATCH("/leaderboards/{org_id}/{project_id}/{leaderboard_id}/", {
480
+ params: one(orgId, projectId, leaderboardId),
481
+ body
482
+ })
483
+ ),
484
+ remove: (orgId, projectId, leaderboardId) => unwrap(
485
+ client.raw.DELETE("/leaderboards/{org_id}/{project_id}/{leaderboard_id}/", {
486
+ params: one(orgId, projectId, leaderboardId)
487
+ })
488
+ ),
489
+ /** Irreversible: archive scores and reset the board. Confirm-gate in the CLI. */
490
+ processReset: (orgId, projectId, leaderboardId) => unwrap(
491
+ client.raw.POST("/leaderboards/{org_id}/{project_id}/{leaderboard_id}/process-reset/", {
492
+ params: one(orgId, projectId, leaderboardId)
493
+ })
494
+ )
495
+ };
496
+ }
497
+
498
+ // src/resources/segments.ts
499
+ function segments(client) {
500
+ const proj = (orgId, projectId) => ({
501
+ path: { org_id: orgId, project_id: projectId }
502
+ });
503
+ const one = (orgId, projectId, segmentId) => ({
504
+ path: { org_id: orgId, project_id: projectId, segment_id: segmentId }
505
+ });
506
+ return {
507
+ list: (orgId, projectId) => unwrap(
508
+ client.raw.GET("/spin-wheel/{org_id}/{project_id}/segments/", {
509
+ params: proj(orgId, projectId)
510
+ })
511
+ ),
512
+ catalog: (orgId, projectId) => unwrap(
513
+ client.raw.GET("/spin-wheel/{org_id}/{project_id}/segments/catalog/", {
514
+ params: proj(orgId, projectId)
515
+ })
516
+ ),
517
+ get: (orgId, projectId, segmentId) => unwrap(
518
+ client.raw.GET("/spin-wheel/{org_id}/{project_id}/segments/{segment_id}/", {
519
+ params: one(orgId, projectId, segmentId)
520
+ })
521
+ ),
522
+ create: (orgId, projectId, body) => unwrap(
523
+ client.raw.POST("/spin-wheel/{org_id}/{project_id}/segments/", {
524
+ params: proj(orgId, projectId),
525
+ body
526
+ })
527
+ ),
528
+ update: (orgId, projectId, segmentId, body) => unwrap(
529
+ client.raw.PATCH("/spin-wheel/{org_id}/{project_id}/segments/{segment_id}/", {
530
+ params: one(orgId, projectId, segmentId),
531
+ body
532
+ })
533
+ ),
534
+ remove: (orgId, projectId, segmentId) => unwrap(
535
+ client.raw.DELETE("/spin-wheel/{org_id}/{project_id}/segments/{segment_id}/", {
536
+ params: one(orgId, projectId, segmentId)
537
+ })
538
+ )
539
+ };
540
+ }
541
+
542
+ // src/resources/wheels.ts
543
+ function wheels(client) {
544
+ const proj = (orgId, projectId) => ({
545
+ path: { org_id: orgId, project_id: projectId }
546
+ });
547
+ const one = (orgId, projectId, wheelConfigId) => ({
548
+ path: { org_id: orgId, project_id: projectId, wheel_config_id: wheelConfigId }
549
+ });
550
+ return {
551
+ list: (orgId, projectId) => unwrap(
552
+ client.raw.GET("/spin-wheel/{org_id}/{project_id}/wheels/", {
553
+ params: proj(orgId, projectId)
554
+ })
555
+ ),
556
+ overview: (orgId, projectId) => unwrap(
557
+ client.raw.GET("/spin-wheel/{org_id}/{project_id}/wheels/overview/", {
558
+ params: proj(orgId, projectId)
559
+ })
560
+ ),
561
+ create: (orgId, projectId, body) => unwrap(
562
+ client.raw.POST("/spin-wheel/{org_id}/{project_id}/wheels/", {
563
+ params: proj(orgId, projectId),
564
+ body
565
+ })
566
+ ),
567
+ update: (orgId, projectId, wheelConfigId, body) => unwrap(
568
+ client.raw.PATCH("/spin-wheel/{org_id}/{project_id}/wheels/{wheel_config_id}/", {
569
+ params: one(orgId, projectId, wheelConfigId),
570
+ body
571
+ })
572
+ ),
573
+ remove: (orgId, projectId, wheelConfigId) => unwrap(
574
+ client.raw.DELETE("/spin-wheel/{org_id}/{project_id}/wheels/{wheel_config_id}/", {
575
+ params: one(orgId, projectId, wheelConfigId)
576
+ })
577
+ )
578
+ };
579
+ }
580
+
581
+ // src/schemas/spec.ts
582
+ var spec_exports = {};
583
+ __export(spec_exports, {
584
+ Condition: () => Condition,
585
+ ConditionSet: () => ConditionSet,
586
+ CurrencySpec: () => CurrencySpec,
587
+ EconomySpec: () => EconomySpec,
588
+ EntryRequirement: () => EntryRequirement,
589
+ Formula: () => Formula,
590
+ LeaderboardSpec: () => LeaderboardSpec,
591
+ ManifestSpec: () => ManifestSpec,
592
+ Outcome: () => Outcome,
593
+ Reward: () => Reward,
594
+ SegmentSpec: () => SegmentSpec,
595
+ StatSpec: () => StatSpec,
596
+ Step: () => Step,
597
+ WheelSegmentLink: () => WheelSegmentLink,
598
+ WheelSpec: () => WheelSpec
599
+ });
600
+ import { z } from "zod";
601
+ var CurrencySpec = z.object({
602
+ key: z.string(),
603
+ name: z.string(),
604
+ symbol: z.string().default(""),
605
+ initial_balance: z.number().int().default(0),
606
+ max_balance: z.number().int().nullable().default(null),
607
+ usable_as_reward: z.boolean().default(true),
608
+ usable_in_conditions: z.boolean().default(true)
609
+ });
610
+ var StatSpec = z.object({
611
+ key: z.string(),
612
+ name: z.string(),
613
+ stat_type: z.string(),
614
+ // integer | float | boolean
615
+ default_value: z.union([z.number(), z.boolean()]),
616
+ min_value: z.number().nullable().default(null),
617
+ max_value: z.number().nullable().default(null),
618
+ usable_in_conditions: z.boolean().default(true),
619
+ usable_as_reward: z.boolean().default(true)
620
+ });
621
+ var Formula = z.object({
622
+ formula: z.string().nullish(),
623
+ max: z.number().nullish()
624
+ });
625
+ var Outcome = z.object({
626
+ reward_type: z.string().nullish(),
627
+ // fixed | variable
628
+ reward_target: z.string().nullish(),
629
+ // currency | stat | gem
630
+ currency_key: z.string().nullish(),
631
+ stat_key: z.string().nullish(),
632
+ operation: z.string().nullish(),
633
+ // add | subtract | set | multiply
634
+ value: z.number().nullish(),
635
+ formula: Formula.nullish()
636
+ });
637
+ var Condition = z.object({
638
+ payload_key: z.string().nullish(),
639
+ operator: z.string().nullish(),
640
+ // eq | neq | gt | gte | lt | lte
641
+ expected_value: z.union([z.number(), z.boolean(), z.string()]).nullish()
642
+ });
643
+ var ConditionSet = z.object({
644
+ conditions: z.array(Condition).default([]),
645
+ outcomes: z.array(Outcome).default([])
646
+ });
647
+ var Step = z.object({
648
+ key: z.string().nullish(),
649
+ // optional so the validator, not Zod, reports "missing key"
650
+ name: z.string().default(""),
651
+ is_entry: z.boolean().default(false),
652
+ is_terminal: z.boolean().default(false),
653
+ next_step_keys: z.array(z.string()).default([]),
654
+ condition_sets: z.array(ConditionSet).default([])
655
+ });
656
+ var EntryRequirement = z.object({
657
+ requirement_type: z.string().nullish(),
658
+ // entry_cost | eligibility
659
+ value_source: z.string().nullish(),
660
+ // currency | stat
661
+ currency_key: z.string().nullish(),
662
+ stat_key: z.string().nullish(),
663
+ operator: z.string().nullish(),
664
+ // gte | lte | eq (eligibility)
665
+ threshold_value: z.union([z.number(), z.boolean()]).nullish(),
666
+ cost_amount: z.number().int().nullish()
667
+ // REQUIRED by the admin API for entry_cost
668
+ });
669
+ var ManifestSpec = z.object({
670
+ slug: z.string(),
671
+ name: z.string(),
672
+ entry_requirements: z.array(EntryRequirement).default([]),
673
+ steps: z.array(Step)
674
+ });
675
+ var LeaderboardSpec = z.object({
676
+ slug: z.string(),
677
+ name: z.string(),
678
+ stat_key: z.string(),
679
+ // authored by key; resolved to stat_id at create time
680
+ sort_order: z.string().default("desc"),
681
+ leaderboard_type: z.string().default("relative"),
682
+ reset_type: z.string().nullable().default(null),
683
+ soft_reset_percentage: z.number().nullish()
684
+ });
685
+ var Reward = z.object({
686
+ reward_type: z.string().nullish(),
687
+ // currency | stat | gem
688
+ currency_key: z.string().nullish(),
689
+ stat_key: z.string().nullish(),
690
+ amount: z.number().int().nullish()
691
+ });
692
+ var SegmentSpec = z.object({
693
+ label: z.string(),
694
+ reward: Reward,
695
+ probability: z.number().int(),
696
+ can_override: z.boolean().default(false)
697
+ });
698
+ var WheelSegmentLink = z.object({
699
+ segment_ref: z.string(),
700
+ // label of a segment defined in this spec
701
+ probability_override: z.number().int().nullish()
702
+ });
703
+ var WheelSpec = z.object({
704
+ key_slug: z.string(),
705
+ name: z.string(),
706
+ daily_free_spins: z.number().int().default(1),
707
+ allow_paid_spins: z.boolean().default(false),
708
+ paid_spin_currency_key: z.string().default(""),
709
+ paid_spin_currency_amount: z.number().int().nullish(),
710
+ segments: z.array(WheelSegmentLink).default([])
711
+ });
712
+ var EconomySpec = z.object({
713
+ currencies: z.array(CurrencySpec).default([]),
714
+ stats: z.array(StatSpec).default([]),
715
+ manifests: z.array(ManifestSpec).default([]),
716
+ leaderboards: z.array(LeaderboardSpec).default([]),
717
+ segments: z.array(SegmentSpec).default([]),
718
+ wheels: z.array(WheelSpec).default([])
719
+ });
720
+
721
+ // src/smart/export.ts
722
+ async function exportDefinitions(client, orgId, projectId) {
723
+ const [currencyList, statList, manifestList, leaderboardList, segmentList, wheelList] = await Promise.all([
724
+ currencies(client).list(orgId, projectId),
725
+ stats(client).list(orgId, projectId),
726
+ manifests(client).list(orgId, projectId),
727
+ leaderboards(client).list(orgId, projectId),
728
+ segments(client).list(orgId, projectId),
729
+ wheels(client).list(orgId, projectId)
730
+ ]);
731
+ return {
732
+ org_id: orgId,
733
+ project_id: projectId,
734
+ currencies: currencyList,
735
+ stats: statList,
736
+ manifests: manifestList,
737
+ leaderboards: leaderboardList,
738
+ segments: segmentList,
739
+ wheels: wheelList
740
+ };
741
+ }
742
+
743
+ // src/smart/refgraph.ts
744
+ function didYouMean(needle, candidates) {
745
+ let best = null;
746
+ let bestRatio = 0;
747
+ for (const c of candidates) {
748
+ const r = similarity(needle, c);
749
+ if (r > bestRatio) {
750
+ bestRatio = r;
751
+ best = c;
752
+ }
753
+ }
754
+ return bestRatio >= 0.6 ? best : null;
755
+ }
756
+ function similarity(a, b) {
757
+ if (a === b) return 1;
758
+ if (!a.length || !b.length) return 0;
759
+ return 2 * lcsLength(a, b) / (a.length + b.length);
760
+ }
761
+ function lcsLength(a, b) {
762
+ const m = a.length;
763
+ const n = b.length;
764
+ const row = new Array(n + 1).fill(0);
765
+ for (let i = 1; i <= m; i++) {
766
+ let prevDiag = 0;
767
+ for (let j = 1; j <= n; j++) {
768
+ const tmp = row[j];
769
+ row[j] = a[i - 1] === b[j - 1] ? prevDiag + 1 : Math.max(row[j], row[j - 1]);
770
+ prevDiag = tmp;
771
+ }
772
+ }
773
+ return row[n];
774
+ }
775
+ function str(v) {
776
+ return typeof v === "string" && v ? v : void 0;
777
+ }
778
+ function* iterManifestRefs(manifest) {
779
+ for (const req of manifest.entry_requirements ?? []) {
780
+ if (typeof req !== "object" || req === null) continue;
781
+ const ck = str(req.currency_key);
782
+ if (ck) yield ["currency", ck, "entry requirement"];
783
+ const sk = str(req.stat_key);
784
+ if (sk) yield ["stat", sk, "entry requirement"];
785
+ }
786
+ for (const step of manifest.steps ?? []) {
787
+ if (typeof step !== "object" || step === null) continue;
788
+ const skey = str(step.key) ?? "?";
789
+ for (const cs of step.condition_sets ?? []) {
790
+ if (typeof cs !== "object" || cs === null) continue;
791
+ for (const out of cs.outcomes ?? []) {
792
+ if (typeof out !== "object" || out === null) continue;
793
+ const target = out.reward_target;
794
+ if (target === "currency") {
795
+ const ck = str(out.currency_key);
796
+ if (ck) yield ["currency", ck, `outcome in step '${skey}'`];
797
+ } else if (target === "stat") {
798
+ const sk = str(out.stat_key);
799
+ if (sk) yield ["stat", sk, `outcome in step '${skey}'`];
800
+ }
801
+ }
802
+ }
803
+ }
804
+ }
805
+ function* iterSegmentRefs(segment) {
806
+ const reward = segment.reward ?? {};
807
+ if (reward.reward_type === "currency") {
808
+ const ck = str(reward.currency_key);
809
+ if (ck) yield ["currency", ck, "segment reward"];
810
+ } else if (reward.reward_type === "stat") {
811
+ const sk = str(reward.stat_key);
812
+ if (sk) yield ["stat", sk, "segment reward"];
813
+ }
814
+ }
815
+ function wheelViews(wheel) {
816
+ const eff = wheel.effective ?? {};
817
+ const cfg = wheel.wheel_config ?? {};
818
+ if (Object.keys(eff).length === 0 && Object.keys(cfg).length === 0) return [wheel, wheel];
819
+ return [eff, cfg];
820
+ }
821
+ var ProjectGraph = class {
822
+ constructor(currencies2, stats2, manifests2, leaderboards2, segments2, wheels2) {
823
+ this.currencies = currencies2;
824
+ this.stats = stats2;
825
+ this.manifests = manifests2;
826
+ this.leaderboards = leaderboards2;
827
+ this.segments = segments2;
828
+ this.wheels = wheels2;
829
+ }
830
+ currencies;
831
+ stats;
832
+ manifests;
833
+ leaderboards;
834
+ segments;
835
+ wheels;
836
+ currencyKeys() {
837
+ return this.currencies.map((c) => str(c.key)).filter((k) => !!k);
838
+ }
839
+ statKeys() {
840
+ return this.stats.map((s) => str(s.key)).filter((k) => !!k);
841
+ }
842
+ segmentIds() {
843
+ return this.segments.map((s) => s.id).filter((v) => typeof v === "number");
844
+ }
845
+ statIdForKey(key) {
846
+ for (const s of this.stats) if (s.key === key) return s.id ?? null;
847
+ return null;
848
+ }
849
+ /** Inbound references (blast radius) to a currency/stat (by key) or segment (by id). */
850
+ referencesTo(kind, ref) {
851
+ if (kind === "segment") return this.refsToSegment(Number(ref));
852
+ return this.refsToDefinition(kind, String(ref));
853
+ }
854
+ refsToDefinition(targetKind, key) {
855
+ const found = [];
856
+ for (const m of this.manifests) {
857
+ for (const [tk, refKey, locus] of iterManifestRefs(m)) {
858
+ if (tk === targetKind && refKey === key) {
859
+ found.push({
860
+ referencingKind: "manifest",
861
+ id: m.id ?? null,
862
+ name: str(m.slug) ?? "?",
863
+ locus
864
+ });
865
+ }
866
+ }
867
+ }
868
+ for (const seg of this.segments) {
869
+ for (const [tk, refKey] of iterSegmentRefs(seg)) {
870
+ if (tk === targetKind && refKey === key) {
871
+ found.push({
872
+ referencingKind: "segment",
873
+ id: seg.id ?? null,
874
+ name: str(seg.label) ?? "?",
875
+ locus: "segment reward"
876
+ });
877
+ }
878
+ }
879
+ }
880
+ if (targetKind === "currency") {
881
+ for (const w of this.wheels) {
882
+ const [eff, cfg] = wheelViews(w);
883
+ const paid = str(cfg.paid_spin_currency_key) ?? str(eff.paid_spin_currency_key);
884
+ if (paid === key) {
885
+ found.push({
886
+ referencingKind: "wheel",
887
+ id: cfg.id ?? null,
888
+ name: str(cfg.key_slug) ?? str(eff.key_slug) ?? "?",
889
+ locus: "paid-spin currency"
890
+ });
891
+ }
892
+ }
893
+ }
894
+ if (targetKind === "stat") {
895
+ const statId = this.statIdForKey(key);
896
+ if (statId !== null) {
897
+ for (const lb of this.leaderboards) {
898
+ if (lb.stat_id === statId) {
899
+ found.push({
900
+ referencingKind: "leaderboard",
901
+ id: lb.id ?? null,
902
+ name: str(lb.slug) ?? "?",
903
+ locus: "ranked stat"
904
+ });
905
+ }
906
+ }
907
+ }
908
+ }
909
+ return found;
910
+ }
911
+ refsToSegment(segmentId) {
912
+ const found = [];
913
+ for (const w of this.wheels) {
914
+ const [eff, cfg] = wheelViews(w);
915
+ const links = eff.segments ?? cfg.segments ?? [];
916
+ if (links.some((l) => l.segment_id === segmentId)) {
917
+ found.push({
918
+ referencingKind: "wheel",
919
+ id: cfg.id ?? null,
920
+ name: str(cfg.key_slug) ?? str(eff.key_slug) ?? "?",
921
+ locus: "wheel segment link"
922
+ });
923
+ }
924
+ }
925
+ return found;
926
+ }
927
+ };
928
+ async function buildReferenceGraph(client, orgId, projectId) {
929
+ const path = { org_id: orgId, project_id: projectId };
930
+ const pick = (obj, key) => {
931
+ const arr = obj?.[key];
932
+ return Array.isArray(arr) ? arr : [];
933
+ };
934
+ const [currencies2, stats2, manifests2, leaderboards2, segments2, wheels2] = await Promise.all([
935
+ unwrap(client.raw.GET("/currency-definitions/{org_id}/{project_id}/", { params: { path } })),
936
+ unwrap(client.raw.GET("/stat-definitions/{org_id}/{project_id}/", { params: { path } })),
937
+ unwrap(client.raw.GET("/game-manifests/{org_id}/{project_id}/", { params: { path } })),
938
+ unwrap(client.raw.GET("/leaderboards/{org_id}/{project_id}/", { params: { path } })),
939
+ unwrap(client.raw.GET("/spin-wheel/{org_id}/{project_id}/segments/", { params: { path } })),
940
+ unwrap(client.raw.GET("/spin-wheel/{org_id}/{project_id}/wheels/", { params: { path } }))
941
+ ]);
942
+ return new ProjectGraph(
943
+ pick(currencies2, "currency_definitions"),
944
+ pick(stats2, "stat_definitions"),
945
+ pick(manifests2, "game_manifests"),
946
+ pick(leaderboards2, "leaderboards"),
947
+ pick(segments2, "segments"),
948
+ pick(wheels2, "wheels")
949
+ );
950
+ }
951
+
952
+ // src/smart/validate.ts
953
+ var STAT_TYPES = ["integer", "float", "boolean"];
954
+ var SORT_ORDERS = ["desc", "asc"];
955
+ var LEADERBOARD_TYPES = ["relative", "absolute"];
956
+ var RESET_TYPES = ["soft", "hard"];
957
+ var REWARD_TYPES = ["currency", "stat", "gem"];
958
+ var REQUIREMENT_TYPES = ["entry_cost", "eligibility"];
959
+ var VALUE_SOURCES = ["currency", "stat"];
960
+ var OUTCOME_REWARD_TYPES = ["fixed", "variable"];
961
+ var REWARD_TARGETS = ["currency", "stat", "gem"];
962
+ var OPERATIONS = ["add", "subtract", "set", "multiply"];
963
+ var OPERATORS = ["eq", "neq", "gt", "gte", "lt", "lte"];
964
+ var EXPR_IN_VALUE = /\s[-+*/]\s/;
965
+ var FORMULA_TOKEN = /[A-Za-z_][A-Za-z0-9_]*/g;
966
+ var FORMULA_NOISE = /* @__PURE__ */ new Set([
967
+ "max",
968
+ "min",
969
+ "abs",
970
+ "round",
971
+ "floor",
972
+ "ceil",
973
+ "sqrt",
974
+ "pow",
975
+ "if",
976
+ "else",
977
+ "and",
978
+ "or",
979
+ "not",
980
+ "true",
981
+ "false"
982
+ ]);
983
+ function validateSpec(input) {
984
+ const parsed = EconomySpec.safeParse(input);
985
+ if (!parsed.success) {
986
+ const violations = parsed.error.issues.map((issue) => ({
987
+ path: issue.path.join("."),
988
+ message: issue.message,
989
+ severity: "error",
990
+ rule: "schema"
991
+ }));
992
+ return report(violations);
993
+ }
994
+ return report(validateEconomy(parsed.data));
995
+ }
996
+ function report(violations) {
997
+ const errors = violations.filter((v) => v.severity === "error").length;
998
+ const warnings = violations.length - errors;
999
+ return { ok: errors === 0, violations, summary: { errors, warnings } };
1000
+ }
1001
+ function validateEconomy(spec) {
1002
+ const out = [];
1003
+ const currencyKeys = new Set(spec.currencies.map((c) => c.key));
1004
+ const statKeys = new Set(spec.stats.map((s) => s.key));
1005
+ const segmentLabels = new Set(spec.segments.map((s) => s.label));
1006
+ const bad = (path, message, severity = "error", rule) => out.push({ path, message, severity, rule });
1007
+ const refOk = (kind, key, path) => {
1008
+ const pool = kind === "currency" ? currencyKeys : statKeys;
1009
+ if (!pool.has(key)) {
1010
+ const hint = didYouMean(key, [...pool].sort());
1011
+ bad(
1012
+ path,
1013
+ `references unknown ${kind} '${key}'.${hint ? ` Did you mean '${hint}'?` : ""}`,
1014
+ "error",
1015
+ "unknown-ref"
1016
+ );
1017
+ }
1018
+ };
1019
+ for (const [poolName, items] of [
1020
+ ["currencies", spec.currencies],
1021
+ ["stats", spec.stats]
1022
+ ]) {
1023
+ const seen = /* @__PURE__ */ new Set();
1024
+ items.forEach((item, i) => {
1025
+ if (seen.has(item.key))
1026
+ bad(`${poolName}[${i}].key`, `duplicate key '${item.key}'.`, "error", "duplicate-key");
1027
+ seen.add(item.key);
1028
+ });
1029
+ }
1030
+ spec.stats.forEach((s, i) => {
1031
+ if (!STAT_TYPES.includes(s.stat_type)) {
1032
+ bad(
1033
+ `stats[${i}].stat_type`,
1034
+ `must be one of ${STAT_TYPES.join(", ")}.`,
1035
+ "error",
1036
+ "stat-type"
1037
+ );
1038
+ }
1039
+ });
1040
+ spec.manifests.forEach((m, i) => {
1041
+ const stepsErr = validateSteps(m);
1042
+ if (stepsErr) bad(`manifests[${i}].steps`, stepsErr, "error", "steps-shape");
1043
+ for (const [tk, key, locus] of iterManifestRefs(m)) {
1044
+ refOk(tk, key, `manifests[${i}] (${locus})`);
1045
+ }
1046
+ validateManifestInternals(bad, i, m);
1047
+ validateEntryRequirements(bad, i, m);
1048
+ lintReachability(bad, i, m);
1049
+ lintFormulaKeys(bad, i, m);
1050
+ });
1051
+ spec.leaderboards.forEach((lb, i) => {
1052
+ const lbErr = validateLeaderboardFields(lb);
1053
+ if (lbErr) bad(`leaderboards[${i}]`, lbErr, "error", "leaderboard-fields");
1054
+ if (!statKeys.has(lb.stat_key)) {
1055
+ const hint = didYouMean(lb.stat_key, [...statKeys].sort());
1056
+ bad(
1057
+ `leaderboards[${i}].stat_key`,
1058
+ `unknown stat '${lb.stat_key}'.${hint ? ` Did you mean '${hint}'?` : ""}`,
1059
+ "error",
1060
+ "unknown-ref"
1061
+ );
1062
+ }
1063
+ });
1064
+ spec.segments.forEach((seg, i) => {
1065
+ const rErr = validateReward(seg.reward);
1066
+ if (rErr) bad(`segments[${i}].reward`, rErr, "error", "reward-shape");
1067
+ for (const [tk, key] of iterSegmentRefs({ reward: seg.reward })) {
1068
+ refOk(tk, key, `segments[${i}].reward`);
1069
+ }
1070
+ if (seg.reward.reward_type === "gem") {
1071
+ bad(
1072
+ `segments[${i}].reward.reward_type`,
1073
+ "gem rewards are only allowed on network catalog segments \u2014 project segments may reward currency only. Link network gem segments into the wheel at authoring time (get_segment_catalog).",
1074
+ "error",
1075
+ "gem-project-segment"
1076
+ );
1077
+ }
1078
+ });
1079
+ spec.wheels.forEach((w, i) => validateWheelSpec(out, i, w, spec, currencyKeys, segmentLabels));
1080
+ lintPhantomStats(bad, spec);
1081
+ lintOrphanCurrencies(bad, spec);
1082
+ return out;
1083
+ }
1084
+ function validateSteps(m) {
1085
+ const steps = m.steps ?? [];
1086
+ if (steps.length === 0) return "manifest has no steps.";
1087
+ const keys = steps.map((s) => s.key).filter((k) => !!k);
1088
+ if (keys.length !== steps.length) return "every step must have a key.";
1089
+ if (new Set(keys).size !== keys.length) return "step keys must be unique.";
1090
+ const entries = steps.filter((s) => s.is_entry);
1091
+ if (entries.length !== 1) return `exactly one step must be is_entry (found ${entries.length}).`;
1092
+ if (!steps.some((s) => s.is_terminal)) return "at least one step must be is_terminal.";
1093
+ const keySet = new Set(keys);
1094
+ for (const s of steps) {
1095
+ for (const nk of s.next_step_keys ?? []) {
1096
+ if (!keySet.has(nk)) return `step '${s.key}' points to unknown next step '${nk}'.`;
1097
+ }
1098
+ }
1099
+ return null;
1100
+ }
1101
+ function validateLeaderboardFields(lb) {
1102
+ if (!SORT_ORDERS.includes(lb.sort_order))
1103
+ return `sort_order must be one of ${SORT_ORDERS.join(", ")}.`;
1104
+ if (!LEADERBOARD_TYPES.includes(lb.leaderboard_type))
1105
+ return `leaderboard_type must be one of ${LEADERBOARD_TYPES.join(", ")}.`;
1106
+ if (lb.reset_type != null && !RESET_TYPES.includes(lb.reset_type))
1107
+ return `reset_type must be one of ${RESET_TYPES.join(", ")} or null.`;
1108
+ if (lb.reset_type === "soft") {
1109
+ const p = lb.soft_reset_percentage;
1110
+ if (p == null || p <= 0 || p > 100)
1111
+ return "soft reset requires soft_reset_percentage in (0, 100].";
1112
+ }
1113
+ return null;
1114
+ }
1115
+ function validateReward(reward) {
1116
+ if (!reward.reward_type) return "reward is missing reward_type (currency | stat | gem).";
1117
+ if (!REWARD_TYPES.includes(reward.reward_type))
1118
+ return `reward_type must be one of ${REWARD_TYPES.join(", ")}.`;
1119
+ if (reward.reward_type === "currency" && !reward.currency_key)
1120
+ return "currency reward must set currency_key.";
1121
+ if (reward.reward_type === "stat" && !reward.stat_key) return "stat reward must set stat_key.";
1122
+ if (reward.amount == null || reward.amount < 1) return "reward amount must be an integer >= 1.";
1123
+ return null;
1124
+ }
1125
+ function validateEntryRequirements(bad, i, m) {
1126
+ m.entry_requirements.forEach((req, j) => {
1127
+ const base = `manifests[${i}].entry_requirements[${j}]`;
1128
+ if (!req.requirement_type || !REQUIREMENT_TYPES.includes(req.requirement_type)) {
1129
+ bad(
1130
+ `${base}.requirement_type`,
1131
+ `must be one of ${REQUIREMENT_TYPES.join(", ")} (got ${JSON.stringify(req.requirement_type)}).`,
1132
+ "error",
1133
+ "entry-requirement"
1134
+ );
1135
+ }
1136
+ if (!req.value_source || !VALUE_SOURCES.includes(req.value_source)) {
1137
+ bad(
1138
+ `${base}.value_source`,
1139
+ `is required and must be one of ${VALUE_SOURCES.join(", ")}.`,
1140
+ "error",
1141
+ "entry-requirement"
1142
+ );
1143
+ }
1144
+ if (req.requirement_type === "entry_cost" && (req.cost_amount == null || req.cost_amount < 1)) {
1145
+ bad(
1146
+ `${base}.cost_amount`,
1147
+ "cost_amount (integer >= 1) is required for entry_cost.",
1148
+ "error",
1149
+ "entry-requirement"
1150
+ );
1151
+ }
1152
+ });
1153
+ }
1154
+ function validateManifestInternals(bad, i, m) {
1155
+ m.steps.forEach((step, j) => {
1156
+ step.condition_sets.forEach((cs, k) => {
1157
+ const baseCs = `manifests[${i}].steps[${j}].condition_sets[${k}]`;
1158
+ cs.conditions.forEach((cond, c) => {
1159
+ if (cond.operator != null && !OPERATORS.includes(cond.operator)) {
1160
+ bad(
1161
+ `${baseCs}.conditions[${c}].operator`,
1162
+ `must be one of ${OPERATORS.join(", ")}.`,
1163
+ "error",
1164
+ "condition-operator"
1165
+ );
1166
+ }
1167
+ if (typeof cond.expected_value === "string" && EXPR_IN_VALUE.test(cond.expected_value)) {
1168
+ bad(
1169
+ `${baseCs}.conditions[${c}].expected_value`,
1170
+ `'${cond.expected_value}' looks like an expression \u2014 expected_value must be a literal; compute the value client-side and send the result in the record payload.`,
1171
+ "warning",
1172
+ "expected-value-literal"
1173
+ );
1174
+ }
1175
+ });
1176
+ cs.outcomes.forEach((oc, o) => {
1177
+ const base = `${baseCs}.outcomes[${o}]`;
1178
+ if (!oc.reward_type)
1179
+ bad(base, "outcome is missing reward_type (fixed | variable).", "error", "outcome-enum");
1180
+ else if (!OUTCOME_REWARD_TYPES.includes(oc.reward_type))
1181
+ bad(
1182
+ `${base}.reward_type`,
1183
+ `must be one of ${OUTCOME_REWARD_TYPES.join(", ")}.`,
1184
+ "error",
1185
+ "outcome-enum"
1186
+ );
1187
+ if (!oc.reward_target)
1188
+ bad(
1189
+ base,
1190
+ "outcome is missing reward_target (currency | stat | gem).",
1191
+ "error",
1192
+ "outcome-enum"
1193
+ );
1194
+ else if (!REWARD_TARGETS.includes(oc.reward_target))
1195
+ bad(
1196
+ `${base}.reward_target`,
1197
+ `must be one of ${REWARD_TARGETS.join(", ")}.`,
1198
+ "error",
1199
+ "outcome-enum"
1200
+ );
1201
+ if (oc.operation != null && !OPERATIONS.includes(oc.operation))
1202
+ bad(
1203
+ `${base}.operation`,
1204
+ `must be one of ${OPERATIONS.join(", ")}.`,
1205
+ "error",
1206
+ "outcome-enum"
1207
+ );
1208
+ const hasFormula = oc.formula != null && !!oc.formula.formula;
1209
+ if (oc.reward_type === "fixed") {
1210
+ if (oc.value == null) bad(base, "fixed outcome must set value.", "error", "pairing");
1211
+ if (hasFormula)
1212
+ bad(
1213
+ base,
1214
+ "fixed outcome must not carry a formula \u2014 use reward_type 'variable' to evaluate a formula.",
1215
+ "error",
1216
+ "pairing"
1217
+ );
1218
+ } else if (oc.reward_type === "variable") {
1219
+ if (!hasFormula)
1220
+ bad(base, "variable outcome must set formula.formula.", "error", "pairing");
1221
+ if (oc.value != null)
1222
+ bad(
1223
+ base,
1224
+ "variable outcome must not set value \u2014 use reward_type 'fixed' to award a fixed value.",
1225
+ "error",
1226
+ "pairing"
1227
+ );
1228
+ }
1229
+ if (oc.reward_target === "gem" && oc.reward_type === "variable" && oc.formula != null && oc.formula.max == null) {
1230
+ bad(
1231
+ `${base}.formula.max`,
1232
+ "uncapped variable gem award \u2014 set formula.max (gems are the network-wide premium wallet).",
1233
+ "warning",
1234
+ "uncapped-gem"
1235
+ );
1236
+ }
1237
+ });
1238
+ });
1239
+ });
1240
+ }
1241
+ function validateWheelSpec(out, i, wheel, spec, currencyKeys, segmentLabels) {
1242
+ const probByLabel = new Map(spec.segments.map((s) => [s.label, s.probability]));
1243
+ let total = 0;
1244
+ wheel.segments.forEach((link, j) => {
1245
+ if (!segmentLabels.has(link.segment_ref)) {
1246
+ const hint = didYouMean(link.segment_ref, [...segmentLabels].sort());
1247
+ out.push({
1248
+ path: `wheels[${i}].segments[${j}].segment_ref`,
1249
+ message: `unknown segment '${link.segment_ref}'.${hint ? ` Did you mean '${hint}'?` : ""}`,
1250
+ severity: "error",
1251
+ rule: "segment-ref"
1252
+ });
1253
+ return;
1254
+ }
1255
+ total += link.probability_override ?? probByLabel.get(link.segment_ref) ?? 0;
1256
+ });
1257
+ if (wheel.segments.length && total !== 100) {
1258
+ out.push({
1259
+ path: `wheels[${i}].segments`,
1260
+ message: `effective probabilities total ${total}, must be exactly 100.`,
1261
+ severity: "error",
1262
+ rule: "probability-total"
1263
+ });
1264
+ }
1265
+ if (wheel.allow_paid_spins) {
1266
+ if (!currencyKeys.has(wheel.paid_spin_currency_key)) {
1267
+ const hint = didYouMean(wheel.paid_spin_currency_key, [...currencyKeys].sort());
1268
+ out.push({
1269
+ path: `wheels[${i}].paid_spin_currency_key`,
1270
+ message: `unknown currency '${wheel.paid_spin_currency_key}'.${hint ? ` Did you mean '${hint}'?` : ""}`,
1271
+ severity: "error",
1272
+ rule: "unknown-ref"
1273
+ });
1274
+ }
1275
+ if (wheel.paid_spin_currency_amount == null || wheel.paid_spin_currency_amount < 1) {
1276
+ out.push({
1277
+ path: `wheels[${i}].paid_spin_currency_amount`,
1278
+ message: "must be an integer >= 1 when allow_paid_spins is true.",
1279
+ severity: "error",
1280
+ rule: "paid-spin"
1281
+ });
1282
+ }
1283
+ const rewardByLabel = new Map(spec.segments.map((s) => [s.label, s.reward]));
1284
+ const amt = wheel.paid_spin_currency_amount;
1285
+ if (currencyKeys.has(wheel.paid_spin_currency_key) && amt && amt >= 1 && wheel.segments.length) {
1286
+ let ev = 0;
1287
+ for (const link of wheel.segments) {
1288
+ const reward = rewardByLabel.get(link.segment_ref);
1289
+ if (!reward || reward.reward_type !== "currency" || !reward.amount) continue;
1290
+ if (reward.currency_key !== wheel.paid_spin_currency_key) continue;
1291
+ const prob = link.probability_override ?? probByLabel.get(link.segment_ref) ?? 0;
1292
+ ev += prob / 100 * reward.amount;
1293
+ }
1294
+ if (ev >= amt) {
1295
+ out.push({
1296
+ path: `wheels[${i}].paid_spin_currency_amount`,
1297
+ message: `money printer: expected payout per paid spin is ${ev.toFixed(1)} '${wheel.paid_spin_currency_key}' but a spin costs ${amt} \u2014 raise the cost or lower the payouts.`,
1298
+ severity: "error",
1299
+ rule: "money-printer"
1300
+ });
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ function payloadKeyCounts(m) {
1306
+ const counts = /* @__PURE__ */ new Map();
1307
+ const inc = (k) => counts.set(k, (counts.get(k) ?? 0) + 1);
1308
+ for (const step of m.steps) {
1309
+ for (const cs of step.condition_sets) {
1310
+ for (const cond of cs.conditions) if (cond.payload_key) inc(cond.payload_key);
1311
+ for (const oc of cs.outcomes) {
1312
+ if (oc.formula?.formula) {
1313
+ for (const tok of oc.formula.formula.match(FORMULA_TOKEN) ?? []) {
1314
+ if (!FORMULA_NOISE.has(tok.toLowerCase())) inc(tok);
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+ return counts;
1321
+ }
1322
+ function lintReachability(bad, i, m) {
1323
+ const stepsByKey = new Map(m.steps.filter((s) => s.key).map((s) => [s.key, s]));
1324
+ const entries = m.steps.filter((s) => s.is_entry && s.key).map((s) => s.key);
1325
+ if (entries.length !== 1) return;
1326
+ const seen = /* @__PURE__ */ new Set([entries[0]]);
1327
+ const frontier = [entries[0]];
1328
+ while (frontier.length) {
1329
+ const step = stepsByKey.get(frontier.pop());
1330
+ for (const nk of step?.next_step_keys ?? []) {
1331
+ if (stepsByKey.has(nk) && !seen.has(nk)) {
1332
+ seen.add(nk);
1333
+ frontier.push(nk);
1334
+ }
1335
+ }
1336
+ }
1337
+ m.steps.forEach((s, j) => {
1338
+ if (s.key && !seen.has(s.key)) {
1339
+ bad(
1340
+ `manifests[${i}].steps[${j}]`,
1341
+ `step '${s.key}' is unreachable from the entry step.`,
1342
+ "warning",
1343
+ "unreachable-step"
1344
+ );
1345
+ }
1346
+ });
1347
+ }
1348
+ function lintFormulaKeys(bad, i, m) {
1349
+ const counts = payloadKeyCounts(m);
1350
+ for (const [key, n] of counts) {
1351
+ if (n !== 1) continue;
1352
+ const others = [...counts.keys()].filter((k) => k !== key);
1353
+ const match = didYouMean(key, others);
1354
+ if (match && similarityCloseEnough(key, match)) {
1355
+ bad(
1356
+ `manifests[${i}]`,
1357
+ `payload key '${key}' is used once \u2014 did you mean '${match}'?`,
1358
+ "warning",
1359
+ "payload-typo"
1360
+ );
1361
+ }
1362
+ }
1363
+ }
1364
+ function similarityCloseEnough(a, b) {
1365
+ const maxLen = Math.max(a.length, b.length) || 1;
1366
+ let same = 0;
1367
+ const bl = [...b];
1368
+ for (const ch of a) {
1369
+ const idx = bl.indexOf(ch);
1370
+ if (idx !== -1) {
1371
+ same++;
1372
+ bl.splice(idx, 1);
1373
+ }
1374
+ }
1375
+ return 2 * same / (a.length + b.length) >= 0.85 && a !== b && Math.abs(a.length - b.length) <= maxLen;
1376
+ }
1377
+ function lintPhantomStats(bad, spec) {
1378
+ const written = /* @__PURE__ */ new Set();
1379
+ for (const m of spec.manifests)
1380
+ for (const s of m.steps)
1381
+ for (const cs of s.condition_sets)
1382
+ for (const oc of cs.outcomes) if (oc.stat_key) written.add(oc.stat_key);
1383
+ const ranked = new Set(spec.leaderboards.map((lb) => lb.stat_key));
1384
+ const rewarded = new Set(
1385
+ spec.segments.map((s) => s.reward.stat_key).filter((k) => !!k)
1386
+ );
1387
+ const payloadKeys = /* @__PURE__ */ new Set();
1388
+ for (const m of spec.manifests) for (const k of payloadKeyCounts(m).keys()) payloadKeys.add(k);
1389
+ const referenced = /* @__PURE__ */ new Set([...written, ...ranked, ...rewarded]);
1390
+ spec.stats.forEach((st, i) => {
1391
+ if (!referenced.has(st.key) && payloadKeys.has(st.key)) {
1392
+ bad(
1393
+ `stats[${i}]`,
1394
+ `'${st.key}' looks like a runtime payload key, not a stat \u2014 no outcome writes it and no leaderboard ranks it. Payload keys are read from the record payload and need no definition.`,
1395
+ "warning",
1396
+ "phantom-stat"
1397
+ );
1398
+ }
1399
+ });
1400
+ }
1401
+ function lintOrphanCurrencies(bad, spec) {
1402
+ const sources = /* @__PURE__ */ new Set();
1403
+ const sinks = /* @__PURE__ */ new Set();
1404
+ for (const m of spec.manifests) {
1405
+ for (const req of m.entry_requirements) if (req.currency_key) sinks.add(req.currency_key);
1406
+ for (const s of m.steps)
1407
+ for (const cs of s.condition_sets)
1408
+ for (const oc of cs.outcomes)
1409
+ if (oc.reward_target === "currency" && oc.currency_key)
1410
+ (oc.operation === "subtract" ? sinks : sources).add(oc.currency_key);
1411
+ }
1412
+ for (const seg of spec.segments)
1413
+ if (seg.reward.reward_type === "currency" && seg.reward.currency_key)
1414
+ sources.add(seg.reward.currency_key);
1415
+ for (const w of spec.wheels)
1416
+ if (w.allow_paid_spins && w.paid_spin_currency_key) sinks.add(w.paid_spin_currency_key);
1417
+ spec.currencies.forEach((c, i) => {
1418
+ if (!sources.has(c.key))
1419
+ bad(
1420
+ `currencies[${i}]`,
1421
+ `'${c.key}' has no source \u2014 nothing awards it.`,
1422
+ "warning",
1423
+ "orphan-currency"
1424
+ );
1425
+ if (!sinks.has(c.key))
1426
+ bad(
1427
+ `currencies[${i}]`,
1428
+ `'${c.key}' has no sink \u2014 add an entry cost, a subtract outcome, or paid wheel spins.`,
1429
+ "warning",
1430
+ "orphan-currency"
1431
+ );
1432
+ });
1433
+ }
1434
+ async function impactScan(client, orgId, projectId, kind, ref) {
1435
+ const graph = await buildReferenceGraph(client, orgId, projectId);
1436
+ const exists = kind === "currency" ? graph.currencyKeys().includes(String(ref)) : kind === "stat" ? graph.statKeys().includes(String(ref)) : graph.segmentIds().includes(Number(ref));
1437
+ const refs = graph.referencesTo(kind, ref);
1438
+ const byKind = {};
1439
+ for (const r of refs) byKind[r.referencingKind] = (byKind[r.referencingKind] ?? 0) + 1;
1440
+ return {
1441
+ target: { kind, ref },
1442
+ exists,
1443
+ referenceCount: refs.length,
1444
+ byKind,
1445
+ references: refs.map((r) => ({
1446
+ kind: r.referencingKind,
1447
+ id: r.id,
1448
+ name: r.name,
1449
+ locus: r.locus
1450
+ })),
1451
+ breaking: refs.length > 0
1452
+ };
1453
+ }
1454
+
1455
+ // src/index.ts
1456
+ var CORE_VERSION = "0.0.0";
1457
+ export {
1458
+ CORE_VERSION,
1459
+ EnvTokenProvider,
1460
+ MagicweaveClient,
1461
+ MagicweaveError,
1462
+ NotFoundError,
1463
+ PROD_API_BASE,
1464
+ ProjectGraph,
1465
+ STAGING_API_BASE,
1466
+ StaticTokenProvider,
1467
+ UnauthorizedError,
1468
+ ValidationError,
1469
+ buildReferenceGraph,
1470
+ createClient,
1471
+ currencies,
1472
+ decodeJwt,
1473
+ didYouMean,
1474
+ environments,
1475
+ exportDefinitions,
1476
+ impactScan,
1477
+ isExpired,
1478
+ iterManifestRefs,
1479
+ iterSegmentRefs,
1480
+ leaderboards,
1481
+ manifests,
1482
+ orgs,
1483
+ projects,
1484
+ releases,
1485
+ requestLoginOtp,
1486
+ resolveBaseUrl,
1487
+ segments,
1488
+ spec_exports as spec,
1489
+ stats,
1490
+ unwrap,
1491
+ validateSpec,
1492
+ verifyLoginOtp,
1493
+ wheels
1494
+ };
1495
+ //# sourceMappingURL=index.js.map