gamedev 0.2.2 → 0.2.4

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
@@ -61788,6 +61788,9 @@ async function admin(fastify2, {
61788
61788
  return { builder: true, deploy: true };
61789
61789
  }
61790
61790
  async function getCapabilitiesFromAuthToken(token) {
61791
+ if (allowsOpenAdminAccess(process.env)) {
61792
+ return { builder: true, deploy: true };
61793
+ }
61791
61794
  if (!token || !db2) return { builder: false, deploy: false };
61792
61795
  const worldId = world2?.network?.worldId || process.env.WORLD_ID;
61793
61796
  const claims = await readJWT(token, { worldId });
@@ -62193,9 +62196,10 @@ async function admin(fastify2, {
62193
62196
  ws2.close();
62194
62197
  return;
62195
62198
  }
62199
+ const openAdminAccess = allowsOpenAdminAccess(process.env);
62196
62200
  const codeCapabilities = getCapabilitiesFromAdminCode(data?.code);
62197
- let builderOk = codeCapabilities.builder;
62198
- let deployOk = codeCapabilities.deploy;
62201
+ let builderOk = openAdminAccess || codeCapabilities.builder;
62202
+ let deployOk = openAdminAccess || codeCapabilities.deploy;
62199
62203
  if (!builderOk || !deployOk) {
62200
62204
  const payloadToken = typeof data?.authToken === "string" ? data.authToken.trim() : "";
62201
62205
  const headerToken = getRuntimeAuthTokenFromRequest(req) || "";
@@ -63145,17 +63149,13 @@ async function createStandaloneGuestSession({
63145
63149
  };
63146
63150
  }
63147
63151
  function buildCliAuthPage({
63148
- callbackUrl,
63149
63152
  sessionId,
63150
- state,
63151
63153
  worldId,
63152
63154
  requiredCapability = "builder",
63153
63155
  publicAuthUrl = null
63154
63156
  } = {}) {
63155
63157
  const config = JSON.stringify({
63156
- callbackUrl: normalizeString2(callbackUrl),
63157
63158
  sessionId: normalizeString2(sessionId),
63158
- state: normalizeString2(state),
63159
63159
  worldId: normalizeString2(worldId),
63160
63160
  requiredCapability: normalizeString2(requiredCapability) || "builder",
63161
63161
  publicAuthUrl: hasValue2(publicAuthUrl) ? publicAuthUrl.trim() : null
@@ -63544,18 +63544,6 @@ function buildCliAuthPage({
63544
63544
  return !!capabilities?.builder
63545
63545
  }
63546
63546
 
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
63547
  async function fetchStatus(token) {
63560
63548
  const response = await fetch(\`\${apiBaseUrl()}/auth/cli/status\`, {
63561
63549
  headers: {
@@ -63613,47 +63601,24 @@ function buildCliAuthPage({
63613
63601
  return token
63614
63602
  }
63615
63603
 
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
63634
- }
63635
-
63636
- const callbackUrl = validateCallbackUrl(config.callbackUrl)
63637
- if (!callbackUrl) {
63638
- throw new Error('Invalid callback URL')
63604
+ async function submitToken(token) {
63605
+ if (!config.sessionId) {
63606
+ throw new Error('Invalid session id')
63639
63607
  }
63640
- const response = await fetch(callbackUrl, {
63608
+ const response = await fetch(\`\${apiBaseUrl()}/auth/cli/session/\${encodeURIComponent(config.sessionId)}\`, {
63641
63609
  method: 'POST',
63642
63610
  headers: {
63643
63611
  'content-type': 'application/json',
63612
+ accept: 'application/json',
63644
63613
  },
63645
63614
  body: JSON.stringify({
63646
- state: config.state,
63647
- worldId: status?.worldId || config.worldId || '',
63648
63615
  worldUrl: worldRootUrl(),
63649
63616
  authToken: token,
63650
- user: status?.user || null,
63651
- capabilities: status?.capabilities || null,
63652
63617
  }),
63653
63618
  })
63654
63619
  if (!response.ok) {
63655
63620
  const payload = await response.json().catch(() => null)
63656
- throw new Error(payload?.error || 'callback_failed')
63621
+ throw new Error(payload?.error || 'session_complete_failed')
63657
63622
  }
63658
63623
  }
63659
63624
 
@@ -63695,7 +63660,7 @@ function buildCliAuthPage({
63695
63660
  'success'
63696
63661
  )
63697
63662
  setHint('Permission confirmed. The CLI is storing this world token locally.', 'success')
63698
- await submitToken(token, status)
63663
+ await submitToken(token)
63699
63664
  clearInterval(polling)
63700
63665
  setTimeout(() => {
63701
63666
  window.close()
@@ -65234,21 +65199,17 @@ async function handleCliAuthPage(req, reply) {
65234
65199
  if (!isRuntimeReady(runtimeState)) {
65235
65200
  return sendRuntimeNotReady2(reply, runtimeState, { html: true });
65236
65201
  }
65237
- const callbackUrl = typeof req.query?.callback === "string" ? req.query.callback.trim() : "";
65238
65202
  const sessionId = typeof req.query?.session === "string" ? req.query.session.trim() : "";
65239
- const state = typeof req.query?.state === "string" ? req.query.state.trim() : "";
65240
65203
  const worldId = typeof req.query?.worldId === "string" ? req.query.worldId.trim() : resolveBoundWorldId() || "";
65241
65204
  const requiredCapability = typeof req.query?.required === "string" ? req.query.required.trim() : "builder";
65242
- if (!sessionId && (!callbackUrl || !state)) {
65205
+ if (!sessionId) {
65243
65206
  return reply.code(400).type("text/html").send(
65244
- "<!doctype html><html><body>Missing session or callback parameters.</body></html>"
65207
+ "<!doctype html><html><body>Missing session.</body></html>"
65245
65208
  );
65246
65209
  }
65247
65210
  reply.type("text/html").send(
65248
65211
  buildCliAuthPage({
65249
- callbackUrl,
65250
65212
  sessionId,
65251
- state,
65252
65213
  worldId,
65253
65214
  requiredCapability,
65254
65215
  publicAuthUrl: process.env.PUBLIC_AUTH_URL || null