gamedev 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,3 @@
1
- import crypto from 'crypto'
2
- import http from 'http'
3
1
  import { spawn } from 'child_process'
4
2
 
5
3
  import { joinUrl, normalizeWorldAdminBaseUrl } from './helpers.js'
@@ -58,138 +56,6 @@ export async function fetchCliAuthStatus({ worldUrl, authToken }) {
58
56
  return data
59
57
  }
60
58
 
61
- function createCallbackServer({ worldId, worldUrl, state, timeoutMs = 10 * 60 * 1000 } = {}) {
62
- const expectedState = typeof state === 'string' && state.trim() ? state.trim() : crypto.randomUUID()
63
- let settled = false
64
- let timeoutId = null
65
- let server = null
66
- let startupResolve = null
67
- let startupReject = null
68
- let startupState = 'pending'
69
- const startup = new Promise((resolve, reject) => {
70
- startupResolve = resolve
71
- startupReject = reject
72
- })
73
-
74
- const close = async () => {
75
- if (!server) return
76
- const target = server
77
- server = null
78
- await new Promise(resolve => target.close(() => resolve()))
79
- }
80
-
81
- const result = new Promise((resolve, reject) => {
82
- server = http.createServer(async (req, res) => {
83
- res.setHeader('Access-Control-Allow-Origin', '*')
84
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
85
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
86
- if (req.method === 'OPTIONS') {
87
- res.statusCode = 204
88
- res.end()
89
- return
90
- }
91
-
92
- if (req.method !== 'POST' || req.url !== '/callback') {
93
- res.statusCode = 404
94
- res.end('Not found')
95
- return
96
- }
97
-
98
- const chunks = []
99
- req.on('data', chunk => {
100
- chunks.push(chunk)
101
- })
102
- req.on('error', async error => {
103
- if (settled) return
104
- settled = true
105
- clearTimeout(timeoutId)
106
- res.statusCode = 500
107
- res.end(JSON.stringify({ error: 'callback_read_failed' }))
108
- await close()
109
- reject(error instanceof Error ? error : createError('callback_read_failed'))
110
- })
111
- req.on('end', async () => {
112
- let payload
113
- try {
114
- payload = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')
115
- } catch {
116
- res.statusCode = 400
117
- res.end(JSON.stringify({ error: 'invalid_payload' }))
118
- return
119
- }
120
-
121
- const receivedState = typeof payload?.state === 'string' ? payload.state.trim() : ''
122
- const authToken = typeof payload?.authToken === 'string' ? payload.authToken.trim() : ''
123
- const receivedWorldId = typeof payload?.worldId === 'string' ? payload.worldId.trim() : ''
124
- const receivedWorldUrl = typeof payload?.worldUrl === 'string' ? payload.worldUrl.trim() : ''
125
-
126
- if (!authToken || !receivedState || receivedState !== expectedState) {
127
- res.statusCode = 400
128
- res.end(JSON.stringify({ error: 'invalid_callback_state' }))
129
- return
130
- }
131
- if (worldId && receivedWorldId && receivedWorldId !== worldId) {
132
- res.statusCode = 409
133
- res.end(JSON.stringify({ error: 'world_id_mismatch' }))
134
- return
135
- }
136
- if (worldUrl && receivedWorldUrl && normalizeWorldAdminBaseUrl(receivedWorldUrl) !== normalizeWorldAdminBaseUrl(worldUrl)) {
137
- res.statusCode = 409
138
- res.end(JSON.stringify({ error: 'world_url_mismatch' }))
139
- return
140
- }
141
-
142
- settled = true
143
- clearTimeout(timeoutId)
144
- res.statusCode = 200
145
- res.setHeader('Content-Type', 'application/json')
146
- res.end(JSON.stringify({ ok: true }))
147
- await close()
148
- resolve(payload)
149
- })
150
- })
151
-
152
- server.listen(0, '127.0.0.1', () => {
153
- if (startupState === 'pending') {
154
- startupState = 'ready'
155
- startupResolve()
156
- }
157
- timeoutId = setTimeout(async () => {
158
- if (settled) return
159
- settled = true
160
- await close()
161
- reject(createError('auth_timeout', 'Timed out waiting for browser authentication'))
162
- }, timeoutMs)
163
- })
164
-
165
- server.on('error', async error => {
166
- if (startupState === 'pending') {
167
- startupState = 'failed'
168
- startupReject(error instanceof Error ? error : createError('callback_server_failed'))
169
- }
170
- if (settled) return
171
- settled = true
172
- clearTimeout(timeoutId)
173
- await close()
174
- reject(error instanceof Error ? error : createError('callback_server_failed'))
175
- })
176
- })
177
-
178
- return {
179
- state: expectedState,
180
- async getCallbackUrl() {
181
- await startup
182
- const address = server.address()
183
- if (!address || typeof address === 'string') {
184
- throw createError('callback_server_failed')
185
- }
186
- return `http://127.0.0.1:${address.port}/callback`
187
- },
188
- result,
189
- close,
190
- }
191
- }
192
-
193
59
  async function launchBrowser(url) {
194
60
  const commands =
195
61
  process.platform === 'darwin'
@@ -218,9 +84,7 @@ async function launchBrowser(url) {
218
84
  export function buildCliAuthUrl({
219
85
  worldUrl,
220
86
  worldId,
221
- callbackUrl,
222
87
  sessionId,
223
- state,
224
88
  requiredCapability = 'builder',
225
89
  } = {}) {
226
90
  const normalizedWorldUrl = normalizeWorldAdminBaseUrl(worldUrl)
@@ -228,10 +92,8 @@ export function buildCliAuthUrl({
228
92
  throw createError('invalid_world_url', 'Invalid world URL')
229
93
  }
230
94
  const url = new URL(joinUrl(normalizedWorldUrl, '/auth/cli'))
231
- if (callbackUrl) url.searchParams.set('callback', callbackUrl)
232
95
  if (sessionId) url.searchParams.set('session', sessionId)
233
96
  if (worldId) url.searchParams.set('worldId', worldId)
234
- if (state) url.searchParams.set('state', state)
235
97
  url.searchParams.set('required', normalizeCapability(requiredCapability))
236
98
  return url.toString()
237
99
  }
@@ -254,12 +116,6 @@ async function createRemoteCliAuthSession({ worldUrl, worldId, requiredCapabilit
254
116
  })
255
117
  const data = await response.json().catch(() => null)
256
118
  if (!response.ok) {
257
- if (response.status === 404 || response.status === 405) {
258
- throw createError('cli_auth_session_unsupported', 'World does not support server-mediated CLI auth sessions', {
259
- status: response.status,
260
- data,
261
- })
262
- }
263
119
  throw createError(
264
120
  data?.error || `session_create_failed:${response.status}`,
265
121
  data?.message || 'Failed to create CLI auth session',
@@ -333,7 +189,7 @@ async function openCliAuthUrl(authUrl, { log = console } = {}) {
333
189
  log?.log?.('Opening browser for world auth...')
334
190
  }
335
191
 
336
- async function runLoopbackBrowserCliAuth({
192
+ export async function runBrowserCliAuth({
337
193
  rootDir = process.cwd(),
338
194
  worldUrl,
339
195
  worldId,
@@ -341,89 +197,33 @@ async function runLoopbackBrowserCliAuth({
341
197
  timeoutMs,
342
198
  log = console,
343
199
  } = {}) {
344
- const callback = createCallbackServer({
200
+ const session = await createRemoteCliAuthSession({
201
+ worldUrl,
202
+ worldId,
203
+ requiredCapability,
204
+ })
205
+ const authUrl = buildCliAuthUrl({
206
+ worldUrl,
345
207
  worldId,
208
+ sessionId: session.sessionId,
209
+ requiredCapability,
210
+ })
211
+ await openCliAuthUrl(authUrl, { log })
212
+ const payload = await waitForRemoteCliAuthSession({
346
213
  worldUrl,
214
+ sessionId: session.sessionId,
347
215
  timeoutMs,
348
216
  })
349
- try {
350
- const callbackUrl = await callback.getCallbackUrl()
351
- const authUrl = buildCliAuthUrl({
352
- worldUrl,
353
- worldId,
354
- callbackUrl,
355
- state: callback.state,
356
- requiredCapability,
357
- })
358
-
359
- await openCliAuthUrl(authUrl, { log })
360
-
361
- const payload = await callback.result
362
- const entry = writeProjectAuthEntry(rootDir, {
363
- worldUrl: payload.worldUrl || worldUrl,
364
- worldId: payload.worldId || worldId,
365
- authToken: payload.authToken,
366
- userId: payload?.user?.id || null,
367
- userName: payload?.user?.name || null,
368
- })
369
- return {
370
- entry,
371
- capabilities: payload?.capabilities || null,
372
- }
373
- } finally {
374
- await callback.close().catch(() => {})
375
- }
376
- }
377
-
378
- export async function runBrowserCliAuth({
379
- rootDir = process.cwd(),
380
- worldUrl,
381
- worldId,
382
- requiredCapability = 'builder',
383
- timeoutMs,
384
- log = console,
385
- } = {}) {
386
- try {
387
- const session = await createRemoteCliAuthSession({
388
- worldUrl,
389
- worldId,
390
- requiredCapability,
391
- })
392
- const authUrl = buildCliAuthUrl({
393
- worldUrl,
394
- worldId,
395
- sessionId: session.sessionId,
396
- requiredCapability,
397
- })
398
- await openCliAuthUrl(authUrl, { log })
399
- const payload = await waitForRemoteCliAuthSession({
400
- worldUrl,
401
- sessionId: session.sessionId,
402
- timeoutMs,
403
- })
404
- const entry = writeProjectAuthEntry(rootDir, {
405
- worldUrl: payload.worldUrl || worldUrl,
406
- worldId: payload.worldId || worldId,
407
- authToken: payload.authToken,
408
- userId: payload?.user?.id || null,
409
- userName: payload?.user?.name || null,
410
- })
411
- return {
412
- entry,
413
- capabilities: payload?.capabilities || null,
414
- }
415
- } catch (error) {
416
- if (error?.code === 'cli_auth_session_unsupported') {
417
- return runLoopbackBrowserCliAuth({
418
- rootDir,
419
- worldUrl,
420
- worldId,
421
- requiredCapability,
422
- timeoutMs,
423
- log,
424
- })
425
- }
426
- throw error
217
+ const entry = writeProjectAuthEntry(rootDir, {
218
+ worldUrl: payload.worldUrl || worldUrl,
219
+ worldId: payload.worldId || worldId,
220
+ authToken: payload.authToken,
221
+ userId: payload?.user?.id || null,
222
+ userName: payload?.user?.name || null,
223
+ })
224
+ return {
225
+ entry,
226
+ capabilities: payload?.capabilities || null,
427
227
  }
428
228
  }
429
229
 
@@ -260,14 +260,14 @@ export class HyperfyCLI {
260
260
  return process.env.HYPERFY_TARGET_CONFIRM === 'true'
261
261
  }
262
262
 
263
- async _connectAdminClient() {
263
+ async _connectAdminClient({ requiredCapability = 'builder' } = {}) {
264
264
  this._requireWorldUrl()
265
265
  this._requireWorldId()
266
266
  const auth = await ensureProjectAuth({
267
267
  rootDir: this.rootDir,
268
268
  worldUrl: this.worldUrl,
269
269
  worldId: this.worldId,
270
- requiredCapability: 'builder',
270
+ requiredCapability,
271
271
  interactive: process.stdin.isTTY,
272
272
  log: console,
273
273
  })
@@ -411,7 +411,7 @@ export class HyperfyCLI {
411
411
 
412
412
  console.log(`🚀 Deploying app: ${appName}`)
413
413
 
414
- const server = await this._connectAdminClient()
414
+ const server = await this._connectAdminClient({ requiredCapability: 'deploy' })
415
415
  try {
416
416
  if (!options.dryRun && !options.yes && this._shouldConfirmDeployTarget()) {
417
417
  const target = process.env.HYPERFY_TARGET ? ` "${process.env.HYPERFY_TARGET}"` : ''
@@ -452,7 +452,7 @@ export class HyperfyCLI {
452
452
 
453
453
  async rollback(snapshotId) {
454
454
  console.log(`⏪ Rolling back deploy snapshot...`)
455
- const server = await this._connectAdminClient()
455
+ const server = await this._connectAdminClient({ requiredCapability: 'deploy' })
456
456
  try {
457
457
  const owner = `hyperfy-cli:${process.env.HYPERFY_TARGET || 'default'}:${process.pid}`
458
458
  const lock = await server.client.acquireDeployLock({ owner })
@@ -486,7 +486,7 @@ export class HyperfyCLI {
486
486
 
487
487
  async status() {
488
488
  console.log(`📊 Admin Status`)
489
- const server = await this._connectAdminClient()
489
+ const server = await this._connectAdminClient({ requiredCapability: 'builder' })
490
490
  try {
491
491
  const snapshot = await server.client.getSnapshot()
492
492
  const blueprints = Array.isArray(snapshot?.blueprints) ? snapshot.blueprints.length : 0
@@ -572,7 +572,7 @@ export class HyperfyCLI {
572
572
  return false
573
573
  }
574
574
 
575
- const server = await this._connectAdminClient()
575
+ const server = await this._connectAdminClient({ requiredCapability: 'deploy' })
576
576
  try {
577
577
  const result = await server.resolveSyncConflict(id, { use })
578
578
  if (result?.alreadyResolved) {
@@ -592,7 +592,7 @@ export class HyperfyCLI {
592
592
  }
593
593
 
594
594
  async syncResolveInteractive() {
595
- const server = await this._connectAdminClient()
595
+ const server = await this._connectAdminClient({ requiredCapability: 'deploy' })
596
596
  try {
597
597
  const summary = await server.promptAndResolveSyncConflicts()
598
598
  if (!summary?.prompted && summary?.remaining > 0) {
package/bin/gamedev.mjs CHANGED
@@ -486,7 +486,7 @@ async function startCommand(args = []) {
486
486
  rootDir: projectDir,
487
487
  worldUrl: env.WORLD_URL,
488
488
  worldId: env.WORLD_ID,
489
- requiredCapability: 'builder',
489
+ requiredCapability: 'deploy',
490
490
  interactive: process.stdin.isTTY,
491
491
  log: console,
492
492
  })
@@ -584,7 +584,7 @@ async function appServerCommand(args = []) {
584
584
  rootDir: projectDir,
585
585
  worldUrl: env.WORLD_URL,
586
586
  worldId: env.WORLD_ID,
587
- requiredCapability: 'builder',
587
+ requiredCapability: 'deploy',
588
588
  interactive: process.stdin.isTTY,
589
589
  log: console,
590
590
  })
@@ -848,12 +848,12 @@ async function syncCommand(args) {
848
848
  return runSyncCommand({ command, args: commandArgs, rootDir: projectDir, helpPrefix: 'gamedev sync' })
849
849
  }
850
850
 
851
- async function connectAdminServer({ worldUrl, worldId, rootDir }) {
851
+ async function connectAdminServer({ worldUrl, worldId, rootDir, requiredCapability = 'builder' }) {
852
852
  const auth = await ensureProjectAuth({
853
853
  rootDir,
854
854
  worldUrl,
855
855
  worldId,
856
- requiredCapability: 'builder',
856
+ requiredCapability,
857
857
  interactive: process.stdin.isTTY,
858
858
  log: console,
859
859
  })
@@ -976,12 +976,18 @@ async function worldCommand(args) {
976
976
 
977
977
  let server
978
978
  try {
979
- server = await connectAdminServer({ worldUrl, worldId, rootDir: projectDir })
980
979
  if (action === 'export') {
980
+ server = await connectAdminServer({ worldUrl, worldId, rootDir: projectDir })
981
981
  const includeBuiltScripts = actionArgs.includes('--include-built-scripts')
982
982
  await server.exportWorldToDisk(undefined, { includeBuiltScripts })
983
983
  console.log('✅ World export complete')
984
984
  } else {
985
+ server = await connectAdminServer({
986
+ worldUrl,
987
+ worldId,
988
+ rootDir: projectDir,
989
+ requiredCapability: 'deploy',
990
+ })
985
991
  await server.importWorldFromDisk()
986
992
  console.log('✅ World import complete')
987
993
  }
package/build/index.js CHANGED
@@ -63145,17 +63145,13 @@ async function createStandaloneGuestSession({
63145
63145
  };
63146
63146
  }
63147
63147
  function buildCliAuthPage({
63148
- callbackUrl,
63149
63148
  sessionId,
63150
- state,
63151
63149
  worldId,
63152
63150
  requiredCapability = "builder",
63153
63151
  publicAuthUrl = null
63154
63152
  } = {}) {
63155
63153
  const config = JSON.stringify({
63156
- callbackUrl: normalizeString2(callbackUrl),
63157
63154
  sessionId: normalizeString2(sessionId),
63158
- state: normalizeString2(state),
63159
63155
  worldId: normalizeString2(worldId),
63160
63156
  requiredCapability: normalizeString2(requiredCapability) || "builder",
63161
63157
  publicAuthUrl: hasValue2(publicAuthUrl) ? publicAuthUrl.trim() : null
@@ -63544,18 +63540,6 @@ function buildCliAuthPage({
63544
63540
  return !!capabilities?.builder
63545
63541
  }
63546
63542
 
63547
- function validateCallbackUrl(value) {
63548
- try {
63549
- const parsed = new URL(value)
63550
- const hostname = parsed.hostname
63551
- if (!hostname) return null
63552
- if (hostname !== '127.0.0.1' && hostname !== 'localhost' && hostname !== '::1') return null
63553
- return parsed.toString()
63554
- } catch {
63555
- return null
63556
- }
63557
- }
63558
-
63559
63543
  async function fetchStatus(token) {
63560
63544
  const response = await fetch(\`\${apiBaseUrl()}/auth/cli/status\`, {
63561
63545
  headers: {
@@ -63613,47 +63597,24 @@ function buildCliAuthPage({
63613
63597
  return token
63614
63598
  }
63615
63599
 
63616
- async function submitToken(token, status) {
63617
- if (config.sessionId) {
63618
- const response = await fetch(\`\${apiBaseUrl()}/auth/cli/session/\${encodeURIComponent(config.sessionId)}\`, {
63619
- method: 'POST',
63620
- headers: {
63621
- 'content-type': 'application/json',
63622
- accept: 'application/json',
63623
- },
63624
- body: JSON.stringify({
63625
- worldUrl: worldRootUrl(),
63626
- authToken: token,
63627
- }),
63628
- })
63629
- if (!response.ok) {
63630
- const payload = await response.json().catch(() => null)
63631
- throw new Error(payload?.error || 'session_complete_failed')
63632
- }
63633
- return
63600
+ async function submitToken(token) {
63601
+ if (!config.sessionId) {
63602
+ throw new Error('Invalid session id')
63634
63603
  }
63635
-
63636
- const callbackUrl = validateCallbackUrl(config.callbackUrl)
63637
- if (!callbackUrl) {
63638
- throw new Error('Invalid callback URL')
63639
- }
63640
- const response = await fetch(callbackUrl, {
63604
+ const response = await fetch(\`\${apiBaseUrl()}/auth/cli/session/\${encodeURIComponent(config.sessionId)}\`, {
63641
63605
  method: 'POST',
63642
63606
  headers: {
63643
63607
  'content-type': 'application/json',
63608
+ accept: 'application/json',
63644
63609
  },
63645
63610
  body: JSON.stringify({
63646
- state: config.state,
63647
- worldId: status?.worldId || config.worldId || '',
63648
63611
  worldUrl: worldRootUrl(),
63649
63612
  authToken: token,
63650
- user: status?.user || null,
63651
- capabilities: status?.capabilities || null,
63652
63613
  }),
63653
63614
  })
63654
63615
  if (!response.ok) {
63655
63616
  const payload = await response.json().catch(() => null)
63656
- throw new Error(payload?.error || 'callback_failed')
63617
+ throw new Error(payload?.error || 'session_complete_failed')
63657
63618
  }
63658
63619
  }
63659
63620
 
@@ -63695,7 +63656,7 @@ function buildCliAuthPage({
63695
63656
  'success'
63696
63657
  )
63697
63658
  setHint('Permission confirmed. The CLI is storing this world token locally.', 'success')
63698
- await submitToken(token, status)
63659
+ await submitToken(token)
63699
63660
  clearInterval(polling)
63700
63661
  setTimeout(() => {
63701
63662
  window.close()
@@ -65234,21 +65195,17 @@ async function handleCliAuthPage(req, reply) {
65234
65195
  if (!isRuntimeReady(runtimeState)) {
65235
65196
  return sendRuntimeNotReady2(reply, runtimeState, { html: true });
65236
65197
  }
65237
- const callbackUrl = typeof req.query?.callback === "string" ? req.query.callback.trim() : "";
65238
65198
  const sessionId = typeof req.query?.session === "string" ? req.query.session.trim() : "";
65239
- const state = typeof req.query?.state === "string" ? req.query.state.trim() : "";
65240
65199
  const worldId = typeof req.query?.worldId === "string" ? req.query.worldId.trim() : resolveBoundWorldId() || "";
65241
65200
  const requiredCapability = typeof req.query?.required === "string" ? req.query.required.trim() : "builder";
65242
- if (!sessionId && (!callbackUrl || !state)) {
65201
+ if (!sessionId) {
65243
65202
  return reply.code(400).type("text/html").send(
65244
- "<!doctype html><html><body>Missing session or callback parameters.</body></html>"
65203
+ "<!doctype html><html><body>Missing session.</body></html>"
65245
65204
  );
65246
65205
  }
65247
65206
  reply.type("text/html").send(
65248
65207
  buildCliAuthPage({
65249
- callbackUrl,
65250
65208
  sessionId,
65251
- state,
65252
65209
  worldId,
65253
65210
  requiredCapability,
65254
65211
  publicAuthUrl: process.env.PUBLIC_AUTH_URL || null