cf2684b0d1
Co-authored-by: Claude <noreply@anthropic.com>
116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
import {type Client} from '@atproto/lex'
|
|
import {type DidString, type NsidString} from '@atproto/syntax'
|
|
import {type I18n} from '@lingui/core'
|
|
import {msg} from '@lingui/core/macro'
|
|
|
|
import {VIDEO_SERVICE_DID} from '#/lib/constants'
|
|
import {UploadLimitError} from '#/lib/media/video/errors'
|
|
import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers'
|
|
import {app, com} from '#/lexicons'
|
|
import {createVideoServiceClient} from './util'
|
|
|
|
export async function getServiceAuthToken({
|
|
client,
|
|
dispatchUrl,
|
|
aud,
|
|
lxm,
|
|
exp,
|
|
}: {
|
|
client: Client
|
|
/**
|
|
* The account's dispatch URL (the old `agent.dispatchUrl`: its PDS, falling
|
|
* back to the account service). Only needed when `aud` is omitted, so the
|
|
* default audience can be derived from the PDS host. A lex {@link Client} does
|
|
* not expose this - it resolves the PDS per request internally - so the caller,
|
|
* which holds the session, passes it in.
|
|
*/
|
|
dispatchUrl?: string | URL
|
|
aud?: string
|
|
lxm: NsidString
|
|
/**
|
|
* Unix timestamp in *seconds* at which the token expires. Fractional values
|
|
* are floored - see {@link toIntegerExp}. Defaults to the server's own
|
|
* short expiry when omitted.
|
|
*/
|
|
exp?: number
|
|
}) {
|
|
let resolvedAud = aud
|
|
if (!resolvedAud) {
|
|
if (!dispatchUrl) {
|
|
throw new Error('Missing service auth audience: no aud or dispatchUrl')
|
|
}
|
|
const pdsAud = getServiceAuthAudFromUrl(dispatchUrl)
|
|
if (!pdsAud) {
|
|
throw new Error('Agent does not have a PDS URL')
|
|
}
|
|
resolvedAud = pdsAud
|
|
}
|
|
const {token} = await client.call(com.atproto.server.getServiceAuth, {
|
|
aud: resolvedAud as DidString,
|
|
lxm,
|
|
exp: exp === undefined ? undefined : toIntegerExp(exp),
|
|
})
|
|
return token
|
|
}
|
|
|
|
/**
|
|
* Default lifetime for the video upload service auth token. Long enough to
|
|
* cover a slow upload of a large file, short enough to limit the damage if the
|
|
* token leaks.
|
|
*/
|
|
export const SERVICE_AUTH_TTL_SEC = 60 * 30
|
|
|
|
/**
|
|
* Build a service auth `exp` claim `ttlSec` seconds from now.
|
|
*
|
|
* Always use this instead of hand-rolling the arithmetic: `Date.now()` is in
|
|
* milliseconds, and dividing by 1000 without flooring yields a fractional
|
|
* timestamp that the endpoint rejects.
|
|
*/
|
|
export function serviceAuthExp(ttlSec: number = SERVICE_AUTH_TTL_SEC) {
|
|
return Math.floor(Date.now() / 1000) + Math.floor(ttlSec)
|
|
}
|
|
|
|
/**
|
|
* The lexicon types `exp` as an integer and it is serialized straight into the
|
|
* query string, so a fractional value fails validation and the upload dies
|
|
* before it starts. Floor here, at the single chokepoint every caller goes
|
|
* through, so a call site that forgets to cannot reintroduce the bug.
|
|
*/
|
|
function toIntegerExp(exp: number) {
|
|
if (!Number.isFinite(exp)) {
|
|
throw new Error(`Invalid service auth exp: ${exp}`)
|
|
}
|
|
return Math.floor(exp)
|
|
}
|
|
|
|
export async function getVideoUploadLimits(client: Client, i18n: I18n) {
|
|
const token = await getServiceAuthToken({
|
|
client,
|
|
lxm: 'app.bsky.video.getUploadLimits',
|
|
aud: VIDEO_SERVICE_DID,
|
|
})
|
|
const videoClient = createVideoServiceClient(token)
|
|
const limits = await videoClient
|
|
.call(app.bsky.video.getUploadLimits)
|
|
.catch(err => {
|
|
if (err instanceof Error) {
|
|
throw new UploadLimitError(err.message)
|
|
} else {
|
|
throw err
|
|
}
|
|
})
|
|
|
|
if (!limits.canUpload) {
|
|
if (limits.message) {
|
|
throw new UploadLimitError(limits.message)
|
|
} else {
|
|
throw new UploadLimitError(
|
|
i18n._(
|
|
msg`You have temporarily reached the limit for video uploads. Please try again later.`,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
}
|