Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
360 lines
11 KiB
JavaScript
360 lines
11 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const vm = require('node:vm')
|
|
const test = require('node:test')
|
|
const source = fs.readFileSync(require('node:path').join(__dirname, 'skin_site_bridge.js'), 'utf8')
|
|
|
|
function harness(origin = 'https://skin.starlight.cool', skinSize = 64) {
|
|
let token = null
|
|
const listeners = {},
|
|
messages = [],
|
|
requests = [],
|
|
drawCalls = []
|
|
const skinWidth = typeof skinSize === 'number' ? skinSize : skinSize.width
|
|
const skinHeight = typeof skinSize === 'number' ? skinSize : skinSize.height
|
|
let tick
|
|
let result = async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ payload: { uuid: 'one', username: 'One' } }),
|
|
})
|
|
const parent = {
|
|
postMessage(data, target) {
|
|
messages.push({ data, target })
|
|
},
|
|
}
|
|
const context = {
|
|
location: { origin },
|
|
window: {
|
|
parent,
|
|
addEventListener(type, callback) {
|
|
listeners[type] = callback
|
|
},
|
|
},
|
|
localStorage: {
|
|
getItem() {
|
|
return token
|
|
},
|
|
},
|
|
fetch(...args) {
|
|
requests.push(args)
|
|
return result(...args)
|
|
},
|
|
AbortController,
|
|
Blob,
|
|
Date,
|
|
FormData,
|
|
Uint8Array,
|
|
atob,
|
|
document: {
|
|
createElement() {
|
|
return {
|
|
getContext() {
|
|
return {
|
|
drawImage(...args) {
|
|
drawCalls.push(args)
|
|
},
|
|
}
|
|
},
|
|
toDataURL() {
|
|
return 'data:image/png;base64,SEVBRERBVEE='
|
|
},
|
|
}
|
|
},
|
|
},
|
|
Image: class {
|
|
naturalWidth = skinWidth
|
|
naturalHeight = skinHeight
|
|
set src(_value) {
|
|
queueMicrotask(() => this.onload?.())
|
|
}
|
|
},
|
|
setTimeout,
|
|
clearTimeout,
|
|
setInterval(callback) {
|
|
tick = callback
|
|
return 1
|
|
},
|
|
clearInterval() {},
|
|
}
|
|
vm.runInNewContext(source, context)
|
|
return {
|
|
drawCalls,
|
|
messages,
|
|
requests,
|
|
listeners,
|
|
parent,
|
|
token(value) {
|
|
token = value
|
|
},
|
|
result(value) {
|
|
result = value
|
|
},
|
|
async tick() {
|
|
tick?.()
|
|
await new Promise(setImmediate)
|
|
},
|
|
async connect(origin = 'http://localhost:5201', source = parent) {
|
|
listeners.message?.({ source, origin, data: { type: 'starlight-skin-session-connect' } })
|
|
await new Promise(setImmediate)
|
|
},
|
|
async message(data, origin = 'http://localhost:5201', source = parent) {
|
|
listeners.message?.({ source, origin, data })
|
|
await new Promise(setImmediate)
|
|
},
|
|
async waitForMessage(type, requestId, timeout = 2_000) {
|
|
const deadline = Date.now() + timeout
|
|
while (Date.now() < deadline) {
|
|
const message = messages.find(
|
|
(entry) =>
|
|
entry.data?.type === type &&
|
|
(requestId === undefined || entry.data?.requestId === requestId),
|
|
)
|
|
if (message) return message
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
}
|
|
throw new Error(`Timed out waiting for ${type}`)
|
|
},
|
|
}
|
|
}
|
|
|
|
test('bridge is restricted to the skin origin and an allowlisted parent', async () => {
|
|
assert.equal(harness('https://evil.example').listeners.message, undefined)
|
|
const h = harness()
|
|
h.token('test-token')
|
|
await h.connect('https://evil.example')
|
|
await h.connect('http://localhost:5201', {})
|
|
assert.equal(h.requests.length, 0)
|
|
assert.equal(h.messages.length, 0)
|
|
await h.connect()
|
|
assert.equal(h.requests[0][0], '/starlight/user')
|
|
assert.equal(h.requests[0][1].headers.Authorization, 'Bearer test-token')
|
|
assert.equal(h.requests[0][1].redirect, 'error')
|
|
assert.equal(h.messages.at(-1).data.status, 'signed-in')
|
|
assert.ok(h.messages.every((m) => m.target === 'http://localhost:5201'))
|
|
assert.ok(!JSON.stringify(h.messages).includes('test-token'))
|
|
})
|
|
|
|
test('logout, invalid token, network error and account switching replace old state', async () => {
|
|
const h = harness()
|
|
await h.connect()
|
|
assert.equal(h.messages.at(-1).data.status, 'signed-out')
|
|
h.token('first')
|
|
await h.tick()
|
|
assert.equal(h.messages.at(-1).data.user.username, 'One')
|
|
h.result(async () => ({ ok: false, status: 401 }))
|
|
h.token('expired')
|
|
await h.tick()
|
|
assert.equal(h.messages.at(-1).data.status, 'signed-out')
|
|
h.result(async () => {
|
|
throw Error('offline')
|
|
})
|
|
h.token('network-error')
|
|
await h.tick()
|
|
assert.equal(h.messages.at(-1).data.status, 'error')
|
|
h.token(null)
|
|
await h.tick()
|
|
assert.equal(h.messages.at(-1).data.status, 'signed-out')
|
|
})
|
|
|
|
test('a delayed response cannot restore the user after logout', async () => {
|
|
const h = harness()
|
|
let finish
|
|
h.result(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
finish = resolve
|
|
}),
|
|
)
|
|
h.token('first')
|
|
await h.connect()
|
|
h.token(null)
|
|
await h.tick()
|
|
finish({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ payload: { uuid: 'one', username: 'One' } }),
|
|
})
|
|
await new Promise(setImmediate)
|
|
assert.equal(h.messages.at(-1).data.status, 'signed-out')
|
|
assert.ok(!h.messages.some((m) => m.data.status === 'signed-in'))
|
|
})
|
|
|
|
test('luck requests stay on the skin origin and return only the validated score', async () => {
|
|
const h = harness()
|
|
h.token('test-token')
|
|
await h.connect()
|
|
h.result(async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ payload: { luck: 73 } }),
|
|
}))
|
|
await h.message({ type: 'starlight-skin-luck-request', requestId: 'skin-luck-1-1' })
|
|
const request = h.requests.at(-1)
|
|
assert.equal(request[0], '/starlight/luck')
|
|
assert.equal(request[1].headers.Authorization, 'Bearer test-token')
|
|
assert.equal(request[1].redirect, 'error')
|
|
const message = h.messages.at(-1)
|
|
assert.equal(message.target, 'http://localhost:5201')
|
|
assert.equal(message.data.type, 'starlight-skin-luck-result')
|
|
assert.equal(message.data.requestId, 'skin-luck-1-1')
|
|
assert.equal(message.data.ok, true)
|
|
assert.equal(message.data.luck, 73)
|
|
assert.ok(!JSON.stringify(h.messages).includes('test-token'))
|
|
})
|
|
|
|
test('luck requests reject untrusted parents, missing sessions, and invalid scores', async () => {
|
|
const h = harness()
|
|
await h.connect()
|
|
const before = h.requests.length
|
|
await h.message(
|
|
{ type: 'starlight-skin-luck-request', requestId: 'skin-luck-2-1' },
|
|
'https://evil.example',
|
|
)
|
|
assert.equal(h.requests.length, before)
|
|
await h.message({ type: 'starlight-skin-luck-request', requestId: 'skin-luck-2-2' })
|
|
assert.equal(h.requests.length, before)
|
|
assert.equal(h.messages.at(-1).data.ok, false)
|
|
|
|
h.token('test-token')
|
|
h.result(async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ payload: { luck: 101 } }),
|
|
}))
|
|
await h.message({ type: 'starlight-skin-luck-request', requestId: 'skin-luck-2-3' })
|
|
assert.equal(h.messages.at(-1).data.ok, false)
|
|
})
|
|
|
|
test('player requests return a sanitized complete player collection without exposing the token', async () => {
|
|
const h = harness()
|
|
h.token('test-token')
|
|
await h.connect()
|
|
h.result(async (url) => {
|
|
if (url.startsWith('/starlight/skin/player/skin/')) {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
payload: {
|
|
skin: url.endsWith('0123456789abcdef0123456789abcdef')
|
|
? '/textures/player-one.png'
|
|
: null,
|
|
},
|
|
}),
|
|
}
|
|
}
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
payload: [
|
|
{ uuid: '0123456789abcdef0123456789abcdef', name: 'PlayerOne', isMojang: false },
|
|
{ uuid: 'fedcba9876543210fedcba9876543210', name: 'Official', isMojang: true },
|
|
{ uuid: 'bad', name: 'Ignored', isMojang: false },
|
|
],
|
|
}),
|
|
}
|
|
})
|
|
await h.message({ type: 'starlight-skin-players-request', requestId: 'skin-players-1-1' })
|
|
await h.waitForMessage('starlight-skin-players-result', 'skin-players-1-1')
|
|
const request = h.requests.find(([url]) => url === '/starlight/skin/player')
|
|
assert.equal(request[0], '/starlight/skin/player')
|
|
assert.equal(request[1].headers.Authorization, 'Bearer test-token')
|
|
assert.equal(
|
|
h.requests.filter(([url]) => url.startsWith('/starlight/skin/player/skin/')).length,
|
|
2,
|
|
)
|
|
const message = h.messages.at(-1)
|
|
assert.equal(message.data.type, 'starlight-skin-players-result')
|
|
assert.equal(message.data.ok, true)
|
|
assert.equal(message.data.players.length, 2)
|
|
assert.equal(message.data.players[1].isMojang, true)
|
|
assert.equal(message.data.players[0].skinState, 'ready')
|
|
assert.equal(message.data.players[0].headDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
|
assert.equal(message.data.players[0].skinDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
|
assert.equal(message.data.players[0].model, 'default')
|
|
assert.equal(message.data.players[1].skinState, 'empty')
|
|
assert.equal(message.data.players[1].headDataUrl, undefined)
|
|
assert.ok(!JSON.stringify(h.messages).includes('test-token'))
|
|
})
|
|
|
|
test('skin updates upload a PNG to the selected skin-site player without exposing the token', async () => {
|
|
const h = harness()
|
|
h.token('test-token')
|
|
await h.connect()
|
|
h.result(async (url) => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
payload:
|
|
url === '/starlight/skin/player'
|
|
? [
|
|
{
|
|
uuid: '0123456789abcdef0123456789abcdef',
|
|
name: 'PlayerOne',
|
|
isMojang: false,
|
|
},
|
|
]
|
|
: { skin: null },
|
|
}),
|
|
}))
|
|
await h.message({ type: 'starlight-skin-players-request', requestId: 'skin-players-2-1' })
|
|
h.result(async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ success: true, payload: 'updated' }),
|
|
}))
|
|
await h.message({
|
|
type: 'starlight-skin-update-request',
|
|
requestId: 'skin-update-1-1',
|
|
playerId: '0123456789abcdef0123456789abcdef',
|
|
textureDataUrl: 'data:image/png;base64,SEVBRERBVEE=',
|
|
model: 'slim',
|
|
})
|
|
|
|
const request = h.requests.at(-1)
|
|
assert.equal(
|
|
request[0],
|
|
'/starlight/skin/player/skin/0123456789abcdef0123456789abcdef/SKIN',
|
|
)
|
|
assert.equal(request[1].method, 'PUT')
|
|
assert.equal(request[1].headers.Authorization, 'Bearer test-token')
|
|
assert.equal(request[1].body.get('model'), 'slim')
|
|
assert.equal(request[1].body.get('file').type, 'image/png')
|
|
assert.equal(h.messages.at(-1).data.type, 'starlight-skin-update-result')
|
|
assert.equal(h.messages.at(-1).data.ok, true)
|
|
assert.ok(!JSON.stringify(h.messages).includes('test-token'))
|
|
})
|
|
|
|
test('skin rendering uses the skin site crop and preserves the source texture dimensions', async () => {
|
|
const h = harness('https://skin.starlight.cool', { width: 64, height: 128 })
|
|
h.token('test-token')
|
|
await h.connect()
|
|
h.result(async (url) => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
payload:
|
|
url === '/starlight/skin/player'
|
|
? [
|
|
{
|
|
uuid: '0123456789abcdef0123456789abcdef',
|
|
name: 'HighResolutionPlayer',
|
|
isMojang: false,
|
|
},
|
|
]
|
|
: { skin: '/textures/hd.png', model: 'slim' },
|
|
}),
|
|
}))
|
|
await h.message({ type: 'starlight-skin-players-request', requestId: 'skin-players-3-1' })
|
|
|
|
const player = h.messages.at(-1).data.players[0]
|
|
assert.equal(player.skinState, 'ready')
|
|
assert.equal(player.headDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
|
assert.equal(player.skinDataUrl, 'data:image/png;base64,SEVBRERBVEE=')
|
|
assert.equal(player.model, 'slim')
|
|
assert.deepEqual(h.drawCalls[0].slice(1), [8, 8, 8, 8, 1, 1, 16, 16])
|
|
assert.deepEqual(h.drawCalls[1].slice(1), [40, 8, 8, 8, 0, 0, 18, 18])
|
|
})
|