glitch-javascript-sdk 3.10.8 → 3.15.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.
@@ -0,0 +1,385 @@
1
+ # Game microtransactions
2
+
3
+ **Minimum SDK for this guide: `3.15.0`.** Use a confirmed published `3.15.0+`
4
+ release or an approved local package. Registry verification on September 15, 2026
5
+ showed public latest `3.10.8`, which lacks commerce; a plain public npm install at
6
+ that point cannot run this callback/history example. Until `3.15.0` is actually
7
+ published, use only the reviewed local `glitch-javascript-sdk-3.15.0.tgz` for testing.
8
+ Do not change a production dependency to an unpublished version or assume that
9
+ local build success means public publication has happened.
10
+
11
+ The SDK entry point is `Glitch.api.Microtransactions`. All HTTP methods return an
12
+ Axios response whose `response.data.data` contains the typed commerce result.
13
+ Configure the ordinary API base once using the existing Glitch configuration.
14
+ Never put a developer/MCP/install token into a shipped browser game.
15
+
16
+ ## Developer setup
17
+
18
+ 1. Read `settings`, `readiness`, `providers` and `listProducts` with a signed-in
19
+ title administrator. Start disabled/sandbox. Approvals are server-controlled.
20
+ The **Enable in-game purchases** and **Show ads** switches live only on the
21
+ game's Pricing/monetization page. Microtransactions is for products, media,
22
+ prices, orders and integration—not a second location for those switches.
23
+ 2. `uploadMedia(titleId, file, onProgress?)` reuses existing Glitch Media with
24
+ trusted title/actor ownership. Attach returned `id` to `media_ids` or
25
+ `branding.logo_media_id`. No scheduler or social-library post is created.
26
+ 3. Create a product draft using `createProduct`. Prices use integer minor units,
27
+ not floating-point money: USD 499 means $4.99; JPY 499 means ¥499. Supported
28
+ currencies are USD/EUR/GBP/CAD/AUD/JPY/BRL/INR/KRW; provider coverage can be
29
+ narrower. Each price is 1–100000 minor units, with country `US` or `*` fallback.
30
+ Provider-specific purchase minima are checked separately (sandbox USD: 50).
31
+ 4. Product types are durable, consumable, currency, bundle or pass. Grants have
32
+ a stable key, quantity and kind. Durable quantity is one. Pass requires
33
+ `duration_seconds` between 60 and 31536000, with no active stacking. Existing
34
+ purchased grants cannot be changed in place: create a new SKU. No recurring
35
+ subscriptions, gifts, paid random loot, cash-out or cross-game wallet support.
36
+ 5. Configure the exact game origin, regions/currencies, support contact and
37
+ game name/accent/logo with `updateSettings`. The fixed commission is 1200bp
38
+ (12%) of discounted pre-tax subtotal. Actual provider costs are separate.
39
+ Taxes are separate; pending earnings are not a verified available payout.
40
+ 6. Run a real approved provider sandbox purchase, verify game delivery/claim,
41
+ then call `verifyIntegration(titleId, {order_id, confirm:true})`. This records
42
+ real evidence, not a self-certified integration checkbox. Launch still needs
43
+ independent seller/tax/provider approvals. MCP confirm alone cannot supply them.
44
+
45
+ Product limits: SKU/grant key 1–100 alphanumeric/underscore/dot/hyphen characters;
46
+ name 255 characters; description 4000; 10 distinct Media UUIDs; 50 prices; 30
47
+ distinct grants; 30 localizations. Settings allow 20 exact origins (255 chars
48
+ each), 100 countries and 20 supported currencies. Branding name maximum 100.
49
+ Server capability schemas remain authoritative.
50
+
51
+ Required product fields are **SKU\***, **Name\***, **Type\***, **Prices\*** and
52
+ **Grants\***; each price needs currency, country and integer minor-unit amount,
53
+ and each grant needs key, quantity and kind. A pass also needs duration in seconds.
54
+ For a pack of **100 Timber** spent building things, choose product type `currency`
55
+ (or `consumable`) and grant `{key:'timber',quantity:100,kind:'consumable'}`. A durable
56
+ grant means lasting ownership and cannot be spent; it is wrong for building Timber.
57
+
58
+ ## Hosted checkout and account creation
59
+
60
+ `createCheckoutSession(titleId, {product_id,quantity,country,currency,environment,
61
+ channel:'web',return_origin,nonce})` is anonymous-safe and only opens the purchase
62
+ flow. Use `createMicrotransactionNonce()` to generate the nonce and retain it in
63
+ the game. `hosted_url` points to Glitch's game-branded checkout with the session
64
+ secret in a `#token` fragment, never a query parameter.
65
+
66
+ Use `openMicrotransactionOverlay` to mount an accessible modal iframe inside the
67
+ running game, even if the game is itself embedded. The game document, URL,
68
+ session, canvas and state remain intact. Do not navigate the game away for checkout
69
+ or provide a top-level fallback. The iframe permits payments and controlled
70
+ bank/OAuth verification windows but has no top-navigation sandbox permission.
71
+ When embedding or required verification is unavailable, offer retry/close while
72
+ preserving the game. Retry reloads only the same session, not a new charge.
73
+
74
+ The overlay bounds each document-loading and application-ready wait to 20 seconds
75
+ by default (`frameLoadTimeoutMs`, clamped to 1–60 seconds). A missing iframe load
76
+ or error event cannot leave the player indefinitely at Loading. Timeout displays
77
+ explicit Retry/Close guidance without initiating payment or navigating the game.
78
+ A document `load` only starts the bounded application-ready wait; it is not proof
79
+ that checkout rendered. The hosted page sends
80
+ `{type:'glitch.microtransaction.ready',version:1,title_id,checkout_session_id,nonce}`
81
+ only after its valid-session account/checkout UI is usable. Exact origin/source/
82
+ session/nonce checks protect this signal, and it never grants inventory. Retry
83
+ resets timers; verified readiness and close clear them.
84
+
85
+ The read-only `getCheckoutFramePolicy(titleId,sessionId)` returns only approved
86
+ `frame_ancestors` and expiry; the hosted document uses that server-owned policy.
87
+ It does not expose a checkout token or player data.
88
+
89
+ Inside Glitch, the page uses existing sign-in/registration/OAuth and binds the
90
+ checkout session once to that user. Session methods require
91
+ `{checkoutToken: sessionSecret}` as their last options argument. The existing
92
+ signed-in account JWT stays inside Glitch, not the game. The SDK sends the limited
93
+ session secret only in `X-Checkout-Token` and never temporarily changes global auth.
94
+
95
+ `checkout(titleId, sessionId, {idempotency_key,accept_terms:true}, options)` returns
96
+ an order, quote, provider and UI mode. Stripe uses `ui_mode:'embedded'`, a limited
97
+ `client_secret` and public `publishable_key`; mount Stripe Embedded Checkout
98
+ inside the branded page, never build custom raw card fields. Approved Xsolla
99
+ routes use `ui_mode:'xsolla'` and its official Pay Station flow. Disclose payment
100
+ methods that need external authentication.
101
+
102
+ Stripe 3DS is part of checkout. Exercise challenge success, cancel/failure,
103
+ timeout and reload in the real sandbox. Keep the same order/session/idempotency
104
+ key through authentication. `action_required`, provider authorization, redirect,
105
+ and client completion are not paid/delivered proof. Call
106
+ `reconcileCheckoutSession(titleId, sessionId, options)` to query the original
107
+ attempt; never charge another provider after an uncertain submission.
108
+
109
+ ## Verified game/account handoff
110
+
111
+ After verified payment and fulfillment, `createHandoff` returns an event with
112
+ `type:'glitch.microtransaction.updated'`, `version:1`, `title_id`,
113
+ `checkout_session_id`, `order_id`, `nonce`, and a one-time `claim_code` valid for
114
+ two minutes. The hosted page targets the exact approved game origin. It never
115
+ posts the account JWT or a player token.
116
+
117
+ The SDK does the secure return work for you: it checks the actual checkout iframe,
118
+ origin, title, session and nonce, exchanges the one-time claim with Glitch, and
119
+ then calls **your `onVerified` function with the server-verified result**. This is
120
+ where your game connects the player account and displays the purchased inventory.
121
+ You do not need to write your own `message` listener or grant items from a browser
122
+ event. Closing checkout without a successful claim does not call this a purchase.
123
+
124
+ ### Minimal working Timber shop
125
+
126
+ Copy this JavaScript module into your game's browser bundle. Call
127
+ `installTimberShop` with the title/product IDs and API/checkout origins shown by
128
+ your game integration settings. For local testing use your approved local origins
129
+ and `allowLocalDevelopment:true`; for production use approved HTTPS endpoints.
130
+ The example creates its own small UI, so no undefined HTML elements or game helper
131
+ functions are required. It never takes the player away from the game.
132
+
133
+ ```js
134
+ import Glitch, {
135
+ createMicrotransactionNonce,
136
+ openMicrotransactionOverlay,
137
+ openMicrotransactionRestoreOverlay,
138
+ } from 'glitch-javascript-sdk';
139
+
140
+ export function installTimberShop({ titleId, productId, apiBaseUrl, checkoutOrigin,
141
+ environment = 'sandbox', allowLocalDevelopment = false }) {
142
+ Glitch.util.Requests.setBaseUrl(apiBaseUrl); // Set API location, NOT global auth.
143
+ const api = Glitch.api.Microtransactions;
144
+ const game = { playerId: null, inventory: [], paused: false };
145
+ let playerToken = '', tokenExpiresAt = 0, overlay = null;
146
+ let opening = false, spending = false, pendingUse = null;
147
+ const shop = document.createElement('section');
148
+ const status = document.createElement('p'); status.setAttribute('role', 'status');
149
+ const timber = document.createElement('output');
150
+ const history = document.createElement('pre');
151
+ shop.append(timber, status, history); document.body.append(shop);
152
+ const say = text => { status.textContent = text; };
153
+
154
+ function replaceInventoryInYourGame(entitlements) {
155
+ game.inventory = entitlements; // REPLACE the snapshot. Never add 100 here.
156
+ timber.textContent = 'Timber: ' + (entitlements.find(x => x.key === 'timber')?.balance ?? 0);
157
+ }
158
+ function setGamePaused(paused) { game.paused = paused; }
159
+ function onVerified(result) {
160
+ game.playerId = result.player_id; // Associate the game's profile with this player.
161
+ playerToken = result.player_token; // Memory only; never a URL or global auth token.
162
+ tokenExpiresAt = Date.parse(result.expires_at);
163
+ replaceInventoryInYourGame(result.entitlements); // Already verified by Glitch.
164
+ say('Account connected. Your current inventory is ready.');
165
+ }
166
+ function playerOptions() {
167
+ if (!playerToken || Date.now() >= tokenExpiresAt) {
168
+ throw new Error('Player authentication is required; restore needs an active purchase.');
169
+ }
170
+ return { playerToken }; // Per-request token; never choose a user_id.
171
+ }
172
+ function showError(error) {
173
+ const code = error?.response?.data?.code ?? error?.code ?? error?.response?.data?.message;
174
+ const noRestore = code === 'no_purchases_to_restore'
175
+ || error?.response?.data?.message === 'No active purchases to restore for this game/account/environment.';
176
+ say(noRestore
177
+ ? 'No active purchases to restore. Refunded receipt history still belongs to this account, but no new game token or items can be granted here.'
178
+ : 'Not completed. Retry the same action. If sign-in expired, authenticate again; restore requires an active purchase.');
179
+ }
180
+ async function openShop(restore) {
181
+ if (opening || overlay) return;
182
+ opening = true;
183
+ try {
184
+ const context = { return_origin: window.location.origin,
185
+ nonce: createMicrotransactionNonce(), environment };
186
+ const response = restore
187
+ ? await api.createRestoreSession(titleId, context)
188
+ : await api.createCheckoutSession(titleId, { ...context, product_id: productId,
189
+ quantity: 1, country: 'US', currency: 'USD', channel: 'web' });
190
+ const open = restore ? openMicrotransactionRestoreOverlay : openMicrotransactionOverlay;
191
+ overlay = open({ titleId, checkoutOrigin, session: response.data.data,
192
+ allowLocalDevelopment, onVerified,
193
+ onOpen: () => setGamePaused(true),
194
+ onClose: () => { overlay = null; setGamePaused(false); },
195
+ onOrderUpdate: order => say(order ? 'Receipt: ' + order.payment_status : 'No completed receipt yet.'),
196
+ onError: showError });
197
+ } finally { opening = false; }
198
+ }
199
+ async function showMyPurchases(page = 1) {
200
+ const response = await api.listMyPurchases(titleId,
201
+ { environment, page, per_page: 20 }, playerOptions());
202
+ const data = response.data.data;
203
+ history.textContent = data.purchases.map(purchase => {
204
+ const name = purchase.product.name ?? purchase.product.sku ?? ('Purchase ' + purchase.id);
205
+ return name + '\n' + purchase.grant_usage.map(row => row.key + ': ' + row.usage_status
206
+ + '; promised ' + row.purchased_quantity + ', granted ' + row.granted_quantity
207
+ + ', consumed ' + row.consumed_quantity + ', usable ' + row.usable_quantity).join('\n');
208
+ }).join('\n\n') || 'No captured purchases yet.';
209
+ say('History page ' + data.pagination.page + ' of ' + data.pagination.last_page);
210
+ }
211
+ async function useTenTimber() {
212
+ if (spending || game.paused) return;
213
+ const options = playerOptions();
214
+ if (pendingUse && pendingUse.playerId !== game.playerId) {
215
+ throw new Error('Restore the original account before retrying its pending action.');
216
+ }
217
+ // Create ONCE for this gameplay intent. A failed retry keeps this same object.
218
+ pendingUse ??= { playerId: game.playerId, action_id: createMicrotransactionNonce(),
219
+ key: 'timber', quantity: 10 };
220
+ spending = true;
221
+ try {
222
+ await api.consume(titleId, { key: pendingUse.key, quantity: pendingUse.quantity,
223
+ action_id: pendingUse.action_id, environment }, options);
224
+ const current = await api.listEntitlements(titleId, { environment }, options);
225
+ replaceInventoryInYourGame(current.data.data.entitlements);
226
+ pendingUse = null; // Clear only after successful acknowledgement AND refresh.
227
+ say('10 Timber spent once. Inventory refreshed from Glitch.');
228
+ } finally { spending = false; } // Do not clear pendingUse on failure.
229
+ }
230
+ function button(label, action) {
231
+ const element = document.createElement('button');
232
+ element.type = 'button'; element.textContent = label;
233
+ element.onclick = () => void action().catch(showError);
234
+ shop.append(element);
235
+ }
236
+ replaceInventoryInYourGame([]);
237
+ button('Buy 100 Timber', () => openShop(false));
238
+ button('Restore purchases', () => openShop(true));
239
+ button('My purchases', () => showMyPurchases());
240
+ button('Use 10 Timber', useTenTimber); // Explicit gameplay action, NOT a purchase callback.
241
+ return game;
242
+ }
243
+ ```
244
+
245
+ Call `installTimberShop({titleId: YOUR_TITLE_ID, productId: YOUR_TIMBER_PRODUCT_ID,
246
+ apiBaseUrl: YOUR_API_BASE, checkoutOrigin: YOUR_CHECKOUT_ORIGIN})` from your existing
247
+ game initialization. These four capitalized names describe your configuration;
248
+ replace them with the actual values, not credentials. `environment` is optional
249
+ and defaults to sandbox. The example assumes an eligible US/USD price; choose a
250
+ server-supported country/currency for your real player instead of guessing from IP.
251
+
252
+ **`replaceInventoryInYourGame` and `setGamePaused` are example game functions, not
253
+ SDK APIs.** Their working demo bodies update the displayed Timber and `game`
254
+ object. In your game, replace those bodies with your engine's inventory assignment
255
+ and pause/input/audio controls. Do not replace assignment with `+= productQuantity`:
256
+ callbacks, restores and refreshes can occur more than once. The callback must never
257
+ call `consume`; that state-changing API belongs to an explicit gameplay action.
258
+
259
+ The sample retains `pendingUse` across failed button retries. For reload recovery,
260
+ persist the nonsecret action intent and its account binding in your game's durable
261
+ command queue, then retry the same action ID after sign-in. Never save the player
262
+ token with it, invent a new ID for an uncertain retry, or infer a particular action's
263
+ success from aggregate purchase-history totals. Coordinate actual building creation
264
+ idempotently with that same gameplay action; the demo only spends the resource.
265
+
266
+ The verified result is the actual claim DTO. The helper validates matching
267
+ title/session/order and, on refresh, the same player. It ignores duplicate codes
268
+ even after a network timeout because the server may already have consumed them.
269
+ It does not grant items from window data or continuously poll. A close message
270
+ uses `{type:'glitch.microtransaction.close',version:1,title_id,
271
+ checkout_session_id,nonce}` and must match the same exact origin/source/session.
272
+ The dialog remains mounted until an in-flight claim and inventory callback finish.
273
+ Close before a successful claim can refresh limited receipt status only; show
274
+ pending/restore guidance, never claim that a checkout capability grants inventory.
275
+
276
+ `player_token` lasts 15 minutes and is restricted to one title/player/environment.
277
+ Keep it in memory and pass `{playerToken}` per request to `getOrder`,
278
+ `listEntitlements` and `consume`. Never install it in global `Glitch.util.Session`
279
+ or `Requests.setAuthToken`, log it, or put it in URLs. The SDK's per-request
280
+ headers prevent simultaneous account/checkout requests from overwriting auth.
281
+
282
+ ### Inventory, purchase history and usage are different
283
+
284
+ Use `listEntitlements` to replace the current player inventory. Use the optional
285
+ `listMyPurchases(titleId, {environment,page,per_page}, {playerToken})` to show the
286
+ signed-in player's captured purchases and their lot-level usage; it does not grant
287
+ items or let a game pick another `user_id` or `player_id`. A normal user JWT selects
288
+ its own user; a `gl_player` token selects its bound title/player/environment and
289
+ requires the exact approved Origin. MCP and install tokens cannot use this route.
290
+ Admin `listOrders` remains a separate developer reporting API.
291
+
292
+ Read `response.data.data`:
293
+
294
+ - `title_id`, `player_id`, `environment`, `purchases`, and
295
+ `pagination:{page,per_page,total,last_page,has_more_pages}`.
296
+ - `page` defaults to 1 (1–10000), `per_page` to 20 (1–100). Results are ordered by
297
+ `created_at DESC, id DESC`. Pass the next page number when `has_more_pages` is true.
298
+ No cursor or product filter exists. The HTTP API rejects unknown selectors with 422;
299
+ the SDK rejects invalid filter names/ranges before transport.
300
+ - Each purchase includes the immutable product snapshot, payment/fulfillment state,
301
+ `grant_usage`, `has_consumed_grants`, and `has_usable_grants`. Captured purchases
302
+ remain visible after refund, dispute or quarantine; unpaid attempts are excluded.
303
+ Historical snapshots may have null product `sku`, `name`, `type`, or `version`;
304
+ show a receipt-ID fallback rather than inventing a current catalog value.
305
+ - Each grant usage row has `grant_id` (nullable), `key`, `kind`,
306
+ `purchased_quantity`, `granted_quantity`, `acquired_quantity`, `remaining_quantity`,
307
+ `consumed_quantity`, `revoked_quantity`, `refunded_quantity`,
308
+ `unrecoverable_quantity`, `expires_at`, `expired`, `usable_quantity`, `is_used`,
309
+ and `usage_status`.
310
+
311
+ `purchased_quantity` is what the frozen product promised (grant quantity × order
312
+ quantity). `granted_quantity` and its alias `acquired_quantity` are what was actually
313
+ granted. Without a lot, `grant_id` is null and actual granted/remaining/consumed
314
+ quantities are zero even though promised quantity can be positive. Do not grant
315
+ missing items just because purchase history lists the promised amount.
316
+
317
+ For consumables, `consumed_quantity = acquired_quantity - remaining_quantity -
318
+ revoked_quantity`. Refunded quantity is bounded `revoked_quantity +
319
+ unrecoverable_quantity`; **unrecoverable overlaps consumed**, so never subtract it
320
+ twice. Refund recovery is not gameplay use. Durable/pass `is_used` is **null** because
321
+ ownership does not prove gameplay usage; inspect `usable_quantity` and `expired`
322
+ instead. Statuses are `unused`, `partially_used`, `used_up`, `owned`, `expired`,
323
+ `revoked`, `not_delivered`, or `unavailable`. Use the server's fields, not a locally
324
+ invented “used” checkbox or a raw remaining count that ignores expiry/restrictions.
325
+
326
+ After expiry/reload or a lost claim response, call the anonymous-safe
327
+ `createRestoreSession(titleId,{return_origin,nonce,environment})` and pass its
328
+ returned `intent:'restore'` session to `openMicrotransactionRestoreOverlay` with
329
+ the same hooks shown above. The account signs in inside the modal, then Glitch
330
+ selects an owned paid purchase and sends its verified handoff. No original receipt
331
+ ID or account JWT is required from the game, and restore never calls `/checkout`.
332
+ The new session ID is known before authentication and remains strictly pinned.
333
+ Restoring cannot duplicate ownership or recreate spent consumables; fully refunded,
334
+ unpaid or failed purchases cannot issue a paid handoff.
335
+
336
+ Restore can issue a new game token only when this account still has an eligible
337
+ active purchase. An account with only fully refunded items may receive
338
+ `no_purchases_to_restore`; the hosted sign-in/restore UI or the example's error
339
+ handler should explain that clearly. An expired scoped token still requires
340
+ authentication. The owner JWT in a Glitch-authenticated context can read all
341
+ captured history, and an existing valid scoped token can read the same history,
342
+ but this example does not promise a new game token after every refund/expiry.
343
+ Do not bypass that boundary, expose the account JWT to the game, repurchase just
344
+ to obtain history access, or invent a read-only-authentication endpoint.
345
+
346
+ Lower-level `createMicrotransactionBridge` remains available for existing in-game
347
+ iframe implementations; pass the actual iframe's `contentWindow`. Its `refresh()`
348
+ works only after a successful claim. The older JWT-only `restoreHandoff` and
349
+ receipt-pinned `createMicrotransactionRestoreBridge` are specialized trusted
350
+ hosted-account flows, not the default anonymous-game recovery path.
351
+
352
+ ## Gameplay, refunds and failures
353
+
354
+ - Use server entitlements, not mutable cloud saves. `consume` atomically spends
355
+ consumable units with a unique gameplay `action_id`; reuse that ID for retries.
356
+ Free local gameplay cannot mint paid balances. Pass expiry is server-authoritative.
357
+ - Keep payment, fulfillment and settlement independent. Paid can coexist with
358
+ pending server delivery. Replaying delivery reuses immutable IDs and never
359
+ grants twice. Provider and game messages are at-least-once delivery.
360
+ - Earnings `transferred_minor` means money transferred to a provider balance,
361
+ not a confirmed bank deposit. Preserve `bank_payout_status` and reserve/reconciliation
362
+ fields; never relabel pending or transferred balances as paid bank payouts.
363
+ - `requestRefund` is an owning account's support request. `refundOrder` requires
364
+ a separately approved financial administrator; pending/unknown is not completed.
365
+ Preserve historical orders and reverse commission proportionately. Refunds
366
+ use the original provider/account, not the currently preferred payment route.
367
+ - Handle HTTP 401/403 for account/scope, 404 for unavailable or cross-title IDs,
368
+ 409 for idempotency/state/approval conflicts, 410 for expired sessions/claims,
369
+ 422 for invalid inputs/revenue policy, 429 for rate limits and 503 for provider
370
+ coverage. Do not retry a hard decline/fraud block through another provider.
371
+ - Ads-off is an actual per-title delivery policy. The backend rejects removing
372
+ the final working revenue model. Provider outages leave ads off and existing
373
+ ownership intact. Sandbox products do not constitute production monetization.
374
+
375
+ ## MCP
376
+
377
+ Use `mcpCapabilities` to discover exact schemas, abilities, approval flags and
378
+ examples. `mcpOperation` always targets the authenticated MCP facade; it never
379
+ uses a game's runtime token. `mcpUploadMedia` uses the same authorized Media
380
+ pipeline. The companion `glitch-mcp` package supplies explicit tools, a
381
+ `glitch://microtransactions/setup` resource, dynamic title schema resources and
382
+ the `glitch_setup_microtransactions` prompt. A model must not auto-approve live
383
+ prices, provider activation, financial actions or disabling the last revenue model.
384
+ Developer MCP read tools do not impersonate players. The self-only runtime purchase
385
+ history API is documented for game code, not exposed as an arbitrary-player MCP tool.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glitch-javascript-sdk",
3
- "version": "3.10.8",
3
+ "version": "3.15.0",
4
4
  "description": "Javascript SDK for Glitch",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -9,13 +9,15 @@
9
9
  "module": "dist/esm/index.js",
10
10
  "files": [
11
11
  "dist",
12
- "src"
12
+ "src",
13
+ "guides"
13
14
  ],
14
15
  "types": "dist/index.d.ts",
15
16
  "scripts": {
16
- "test": "node scripts/test-hosting-routes.cjs && node scripts/test-game-advertising-routes.cjs && node scripts/test-game-design-routes.cjs && node scripts/test-discord-media-routes.cjs && node scripts/test-user-email-delivery-routes.cjs && node scripts/test-game-show-ticket-routes.cjs",
17
+ "test": "node scripts/test-hosting-routes.cjs && node scripts/test-game-advertising-routes.cjs && node scripts/test-game-design-routes.cjs && node scripts/test-discord-media-routes.cjs && node scripts/test-user-email-delivery-routes.cjs && node scripts/test-game-show-ticket-routes.cjs && node scripts/test-festival-networking-routes.cjs && node scripts/test-microtransaction-routes.cjs && node scripts/test-microtransaction-overlay.cjs && node scripts/test-microtransaction-tutorial.cjs",
17
18
  "build": "rm -Rf dist && rollup -c --bundleConfigAsCjs",
18
- "build-docs": "rm -rf docs && typedoc --tsconfig tsconfig.json src/"
19
+ "build-docs": "rm -rf docs && typedoc --tsconfig tsconfig.json src/",
20
+ "test:package": "node scripts/test-package-exports.cjs"
19
21
  },
20
22
  "author": "",
21
23
  "license": "ISC",
@@ -27,6 +29,7 @@
27
29
  "@types/isomorphic-form-data": "^2.0.4",
28
30
  "@types/node": "^20.10.5",
29
31
  "get-file-object-from-local-path": "^1.0.2",
32
+ "jsdom": "^26.1.0",
30
33
  "rollup": "^4.9.1",
31
34
  "rollup-plugin-dts": "^6.1.0",
32
35
  "rollup-plugin-node-builtins": "^2.1.2",
@@ -0,0 +1,216 @@
1
+ import { AxiosProgressEvent, AxiosPromise, AxiosRequestConfig } from 'axios';
2
+ import FestivalNetworkingRoute from '../routes/FestivalNetworkingRoute';
3
+ import Requests from '../util/Requests';
4
+
5
+ export type FestivalPostKind = 'discussion' | 'job' | 'talent';
6
+ export type FestivalPostState = 'active' | 'locked' | 'archived' | 'hidden' | 'deleted' | 'removed' | 'paused' | 'filled' | 'expired' | 'closed';
7
+ export type FestivalApplicationState = 'submitted' | 'viewed' | 'shortlisted' | 'interview' | 'accepted' | 'rejected' | 'withdrawn' | 'closed';
8
+ export type FestivalWorkType = 'full_time' | 'part_time' | 'contract' | 'gig';
9
+ export type FestivalRequestOptions = Pick<AxiosRequestConfig, 'signal' | 'timeout'>;
10
+
11
+ export interface FestivalNetworkingSettings {
12
+ discussions_enabled: boolean;
13
+ jobs_enabled: boolean;
14
+ employer_posts_enabled: boolean;
15
+ talent_posts_enabled: boolean;
16
+ matching_enabled: boolean;
17
+ voting_enabled: boolean;
18
+ comments_enabled: boolean;
19
+ media_enabled: boolean;
20
+ /** Registration or a valid ticket is mandatory; cannot be disabled. */
21
+ require_registration: true;
22
+ public_viewing: false;
23
+ anonymous_enabled: false;
24
+ categories: string[];
25
+ skills: string[];
26
+ }
27
+
28
+ export interface FestivalPostInput {
29
+ kind: FestivalPostKind;
30
+ title: string;
31
+ /** Sanitized HTML from the shared WYSIWYG editor. */
32
+ content: string;
33
+ visibility?: 'public' | 'unlisted' | 'private';
34
+ category?: string | null;
35
+ tags?: string[];
36
+ skills?: string[];
37
+ preferred_skills?: string[];
38
+ job_types?: FestivalWorkType[];
39
+ company?: string | null;
40
+ organization_id?: string | null;
41
+ work_arrangement?: 'remote' | 'onsite' | 'hybrid' | null;
42
+ experience?: 'any' | 'entry' | 'junior' | 'mid' | 'senior' | 'lead' | null;
43
+ location?: string | null;
44
+ availability?: 'immediately' | 'within_30_days' | 'specific_date' | 'flexible' | 'unavailable';
45
+ availability_date?: string | null;
46
+ deadline?: string | null;
47
+ expires_at?: string | null;
48
+ compensation_type?: 'negotiable' | 'yearly' | 'monthly' | 'hourly' | 'flat_fee';
49
+ compensation_min?: number | null;
50
+ compensation_max?: number | null;
51
+ currency?: string | null;
52
+ portfolio_url?: string | null;
53
+ application_method?: 'internal' | 'external';
54
+ application_url?: string | null;
55
+ /** UserMedia IDs returned by uploadMedia, NOT Media IDs or clip-library selections. Max 8. */
56
+ media_ids?: string[];
57
+ public_compensation?: boolean;
58
+ public_location?: boolean;
59
+ public_availability?: boolean;
60
+ public_portfolio?: boolean;
61
+ }
62
+
63
+ export interface FestivalNetworkingFilters {
64
+ kind?: FestivalPostKind;
65
+ view?: 'all' | 'mine' | 'saved' | 'hidden' | 'comments';
66
+ sort?: 'new' | 'hot' | 'top' | 'discussed' | 'compensation' | 'deadline';
67
+ window?: 'today' | 'week' | 'month' | 'festival' | 'all';
68
+ q?: string;
69
+ category?: string;
70
+ skill?: string;
71
+ job_type?: FestivalWorkType;
72
+ arrangement?: 'remote' | 'onsite' | 'hybrid';
73
+ experience?: string;
74
+ location?: string;
75
+ company?: string;
76
+ availability?: string;
77
+ currency?: string;
78
+ compensation_type?: string;
79
+ min_compensation?: number;
80
+ tag?: string;
81
+ author?: string;
82
+ has_comments?: boolean;
83
+ has_portfolio?: boolean;
84
+ media_type?: 'image' | 'video';
85
+ page?: number;
86
+ per_page?: number;
87
+ }
88
+
89
+ export interface FestivalMediaUpload {
90
+ id: string;
91
+ user_media_id: string;
92
+ media_id: string;
93
+ url: string;
94
+ mime_type: string;
95
+ size: number;
96
+ title: string;
97
+ processing_status: 'completed' | 'pending' | 'processing' | 'failed';
98
+ }
99
+
100
+ export interface FestivalNetworkingProfile { id: string | null; name: string; }
101
+ export interface FestivalNetworkingResponse<T> {
102
+ data: T;
103
+ message?: string;
104
+ meta?: { current_page: number; last_page: number; total?: number; };
105
+ has_access?: boolean;
106
+ can_manage?: boolean;
107
+ can_moderate?: boolean;
108
+ user?: FestivalNetworkingProfile | null;
109
+ show?: { id: string; name: string; };
110
+ audit?: Array<Record<string, unknown>>;
111
+ }
112
+ export interface FestivalPost {
113
+ id: string;
114
+ game_show_id: string;
115
+ parent_id: string | null;
116
+ kind: FestivalPostKind | 'comment';
117
+ title: string;
118
+ content: string;
119
+ state: FestivalPostState;
120
+ visibility: 'public' | 'unlisted' | 'private';
121
+ details: Partial<FestivalPostInput>;
122
+ author: FestivalNetworkingProfile | null;
123
+ created_at: string;
124
+ updated_at: string;
125
+ score: number;
126
+ my_vote: -1 | 0 | 1;
127
+ saved: boolean;
128
+ is_owner: boolean;
129
+ can_edit: boolean;
130
+ comment_count: number;
131
+ media: Array<{ id: string; user_media_id?: string | null; url: string; mime_type: string; title: string | null; }>;
132
+ my_application?: { id: string; status: FestivalApplicationState; } | null;
133
+ match?: { score: number; reasons: string[]; missing_required_skills: string[]; disclaimer: string; };
134
+ }
135
+ export interface FestivalApplicationInput { message?: string; portfolio?: string[]; }
136
+ export interface FestivalConversation {
137
+ id: string;
138
+ festival_application_id: string;
139
+ can_send: boolean;
140
+ read_only_reason: string | null;
141
+ festival_context: { game_show_id: string; post_id: string | null; title: string; kind: FestivalPostKind; application_status: FestivalApplicationState } | null;
142
+ users: Array<{ id: string | null; display_name: string; avatar: string | null }>;
143
+ messages: Array<{ id: string; thread_id: string; user_id: string | null; message: string; client_message_id: string | null; created_at: string; updated_at: string; user: { id: string | null; display_name: string; avatar: string | null } }>;
144
+ }
145
+ export interface FestivalPreferences { notifications?: boolean; blocked_users?: string[]; blocked_companies?: string[]; }
146
+ export interface FestivalReportInput {
147
+ reason: 'spam' | 'harassment' | 'hate' | 'sexual_content' | 'scam' | 'job_scam' | 'copyright' | 'malicious_link' | 'misleading' | 'other';
148
+ explanation?: string;
149
+ }
150
+
151
+ /** Festival-scoped discussions, talent, jobs, moderation and new owned media uploads. */
152
+ export default class FestivalNetworking {
153
+ private static request<T>(name: string, show_id: string, data?: object, ids: Record<string, string> = {}, params?: object, options?: FestivalRequestOptions): AxiosPromise<FestivalNetworkingResponse<T>> {
154
+ const replacements = Object.fromEntries(Object.entries({ show_id, ...ids }).map(([key, id]) => [key, encodeURIComponent(id)]));
155
+ return Requests.processRoute<T>(FestivalNetworkingRoute.routes[name], data, replacements, params, options);
156
+ }
157
+
158
+ /** Read enabled tools and the current account's registration/ticket access. */
159
+ static settings<T = FestivalNetworkingSettings>(id: string, options?: FestivalRequestOptions) { return this.request<T>('settings', id, undefined, {}, undefined, options); }
160
+ /** Organizer-only settings update; admission remains mandatory. */
161
+ static updateSettings<T = FestivalNetworkingSettings>(id: string, data: Partial<FestivalNetworkingSettings>) { return this.request<T>('updateSettings', id, data); }
162
+ /** Search posts; compensation comparisons require currency and period. */
163
+ static listPosts<T = FestivalPost[]>(id: string, params?: FestivalNetworkingFilters, options?: FestivalRequestOptions) { return this.request<T>('listPosts', id, undefined, {}, params, options); }
164
+ /** Create a post with rich HTML and optional newly uploaded UserMedia IDs. */
165
+ static createPost<T = FestivalPost>(id: string, data: FestivalPostInput) { return this.request<T>('createPost', id, data); }
166
+ /** Direct links still require admission and content visibility permission. */
167
+ static getPost<T = FestivalPost>(id: string, post_id: string, options?: FestivalRequestOptions) { return this.request<T>('getPost', id, undefined, { post_id }, undefined, options); }
168
+ /** Edit or soft-delete via state; omit media_ids to preserve current attachments. */
169
+ static updatePost<T = FestivalPost>(id: string, post_id: string, data: Partial<FestivalPostInput> & { state?: FestivalPostState }) { return this.request<T>('updatePost', id, data, { post_id }); }
170
+ /** Paginated direct replies; fetch children to expand a thread. */
171
+ static listComments<T = FestivalPost[]>(id: string, post_id: string, params?: { page?: number }, options?: FestivalRequestOptions) { return this.request<T>('listComments', id, undefined, { post_id }, params, options); }
172
+ /** Add a rich-text reply, subject to locking and five-level nesting. */
173
+ static createComment<T = FestivalPost>(id: string, post_id: string, data: { content: string }) { return this.request<T>('createComment', id, data, { post_id }); }
174
+ /** Set, replace, or remove a vote/save/hide idempotently. */
175
+ static setInteraction<T = FestivalPost>(id: string, post_id: string, data: { action: 'vote' | 'saved' | 'hidden'; value: -1 | 0 | 1 }) { return this.request<T>('setInteraction', id, data, { post_id }); }
176
+ /** Apply or express interest once; the original listing is snapshotted. */
177
+ static apply<T = Record<string, unknown>>(id: string, post_id: string, data: FestivalApplicationInput) { return this.request<T>('apply', id, data, { post_id }); }
178
+ /** Discovery matches for the current user's own job/talent listing. */
179
+ static matches<T = FestivalPost[]>(id: string, post_id: string, params?: { page?: number }, options?: FestivalRequestOptions) { return this.request<T>('matches', id, undefined, { post_id }, params, options); }
180
+ /** Report suspicious content privately to festival moderators. */
181
+ static report<T = never>(id: string, post_id: string, data: FestivalReportInput) { return this.request<T>('report', id, data, { post_id }); }
182
+ /** Only the applicant and listing owner receive application records. */
183
+ static applications<T = Array<Record<string, unknown>>>(id: string, params?: { page?: number }, options?: FestivalRequestOptions) { return this.request<T>('applications', id, undefined, {}, params, options); }
184
+ /** Applicant withdrawal or owner-managed status changes. */
185
+ static updateApplication<T = Record<string, unknown>>(id: string, application_id: string, data: { status: Exclude<FestivalApplicationState, 'submitted'> }) { return this.request<T>('updateApplication', id, data, { application_id }); }
186
+ /** Open the application/talent inquiry's private shared-inbox conversation, creating it once for legacy applications. Only the applicant and original listing owner may call this. Use Messages.getThread/sendMessage for subsequent conversation activity. */
187
+ static conversation<T = FestivalConversation>(id: string, application_id: string) { return this.request<T>('conversation', id, {}, { application_id }); }
188
+ /** Moderator-only report queue and audit history. */
189
+ static moderation<T = Array<Record<string, unknown>>>(id: string, params?: { page?: number }, options?: FestivalRequestOptions) { return this.request<T>('moderation', id, undefined, {}, params, options); }
190
+ /** Moderation remains available even while the board is disabled. */
191
+ static moderatePost<T = FestivalPost>(id: string, post_id: string, data: { state?: 'active' | 'hidden' | 'locked' | 'deleted' | 'removed'; remove_media?: true }) { return this.request<T>('moderatePost', id, data, { post_id }); }
192
+ /** Resolve, dismiss or begin reviewing a report. */
193
+ static resolveReport<T = never>(id: string, report_id: string, data: { status: 'under_review' | 'resolved' | 'dismissed' }) { return this.request<T>('resolveReport', id, data, { report_id }); }
194
+ /** Moderator-only participant restrictions. */
195
+ static restrictMember<T = never>(id: string, user_id: string, data: { banned: boolean }) { return this.request<T>('restrictMember', id, data, { user_id }); }
196
+ /** Current user's privacy and notification preferences. */
197
+ static preferences<T = FestivalPreferences>(id: string, options?: FestivalRequestOptions) { return this.request<T>('preferences', id, undefined, {}, undefined, options); }
198
+ /** Set notification opt-out and blocked participants/companies. */
199
+ static updatePreferences<T = FestivalPreferences>(id: string, data: FestivalPreferences) { return this.request<T>('updatePreferences', id, data); }
200
+ /** Uploaded festival attachments only; not the gameplay clip library. */
201
+ static media<T = Array<Record<string, unknown>>>(id: string, params?: { page?: number }, options?: FestivalRequestOptions) { return this.request<T>('media', id, undefined, {}, params, options); }
202
+ /**
203
+ * Upload a new image (10 MB max) or video (100 MB max), using the existing media pipeline.
204
+ * The response ID is an owned UserMedia ID for createPost/updatePost media_ids.
205
+ * @param file New file selected by the attendee; supported images exclude SVG.
206
+ * @param data Which enabled board the upload is for.
207
+ * @param onUploadProgress Transfer progress; 100% may still require conversion before completion.
208
+ */
209
+ static uploadMedia<T = FestivalMediaUpload>(id: string, file: File | Blob, data: { kind: FestivalPostKind }, onUploadProgress?: (event: AxiosProgressEvent) => void, options?: FestivalRequestOptions): AxiosPromise<FestivalNetworkingResponse<T>> {
210
+ return Requests.uploadFile<T>(FestivalNetworkingRoute.routes.uploadMedia.url.replace('{show_id}', encodeURIComponent(id)), 'media', file, data, undefined, onUploadProgress, options);
211
+ }
212
+ /** Organizations the authenticated user is authorized to represent. */
213
+ static organizations<T = Array<{ id: string; name: string }>>(id: string, options?: FestivalRequestOptions) { return this.request<T>('organizations', id, undefined, {}, undefined, options); }
214
+ /** Organizer-only aggregate participation metrics. */
215
+ static analytics<T = Record<string, unknown>>(id: string, options?: FestivalRequestOptions) { return this.request<T>('analytics', id, undefined, {}, undefined, options); }
216
+ }
@@ -17,7 +17,10 @@ class Messages {
17
17
  }
18
18
 
19
19
  /**
20
- * Send a new message that will be added to a thread
20
+ * Send a new message that will be added to a thread. Festival-scoped threads
21
+ * enforce current admission, blocking and read-only state server-side.
22
+ * Include an optional client_message_id UUID and reuse it when retrying a
23
+ * timed-out request to prevent duplicate messages and notifications.
21
24
  *
22
25
  * @see https://api.glitch.fun/api/documentation#/Messages/storeMessage
23
26
  *