[web] Refactoring - Cast (#4178)
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
toB64URLSafeNoPaddingString,
|
||||
} from "@/base/crypto/libsodium";
|
||||
import { isDevBuild } from "@/base/env";
|
||||
import { clientPackageHeader, ensureOk, HTTPError } from "@/base/http";
|
||||
import { ensureOk, HTTPError, publicRequestHeaders } from "@/base/http";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import { TwoFactorAuthorizationResponse } from "@/base/types/credentials";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
@@ -412,7 +412,7 @@ export const beginPasskeyAuthentication = async (
|
||||
const url = await apiURL("/users/two-factor/passkeys/begin");
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: clientPackageHeader(),
|
||||
headers: publicRequestHeaders(),
|
||||
body: JSON.stringify({ sessionID: passkeySessionID }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getCastData, register } from "services/pair";
|
||||
import { advertiseOnChromecast } from "../services/chromecast-receiver";
|
||||
|
||||
export default function Index() {
|
||||
const [publicKeyB64, setPublicKeyB64] = useState<string | undefined>();
|
||||
const [privateKeyB64, setPrivateKeyB64] = useState<string | undefined>();
|
||||
const [publicKey, setPublicKey] = useState<string | undefined>();
|
||||
const [privateKey, setPrivateKey] = useState<string | undefined>();
|
||||
const [pairingCode, setPairingCode] = useState<string | undefined>();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -18,8 +18,8 @@ export default function Index() {
|
||||
useEffect(() => {
|
||||
if (!pairingCode) {
|
||||
void register().then((r) => {
|
||||
setPublicKeyB64(r.publicKeyB64);
|
||||
setPrivateKeyB64(r.privateKeyB64);
|
||||
setPublicKey(r.publicKey);
|
||||
setPrivateKey(r.privateKey);
|
||||
setPairingCode(r.pairingCode);
|
||||
});
|
||||
} else {
|
||||
@@ -31,14 +31,17 @@ export default function Index() {
|
||||
}, [pairingCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!publicKeyB64 || !privateKeyB64 || !pairingCode) return;
|
||||
if (!publicKey || !privateKey || !pairingCode) return;
|
||||
|
||||
const pollTick = async () => {
|
||||
const registration = { publicKeyB64, privateKeyB64, pairingCode };
|
||||
try {
|
||||
// TODO:
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const data = await getCastData(registration);
|
||||
const data = await getCastData({
|
||||
publicKey,
|
||||
privateKey,
|
||||
pairingCode,
|
||||
});
|
||||
if (!data) {
|
||||
// No one has connected yet.
|
||||
return;
|
||||
@@ -58,7 +61,7 @@ export default function Index() {
|
||||
|
||||
const interval = setInterval(pollTick, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [publicKeyB64, privateKeyB64, pairingCode, router]);
|
||||
}, [publicKey, privateKey, pairingCode, router]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { boxSealOpen, generateKeyPair } from "@/base/crypto/libsodium";
|
||||
import { boxSealOpen, generateKeyPair } from "@/base/crypto";
|
||||
import { ensureOk, publicRequestHeaders } from "@/base/http";
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import { wait } from "@/utils/promise";
|
||||
import castGateway from "@ente/shared/network/cast";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface Registration {
|
||||
/** A pairing code shown on the screen. A client can use this to connect. */
|
||||
pairingCode: string;
|
||||
/** The public part of the keypair we registered with the server. */
|
||||
publicKeyB64: string;
|
||||
/** The private part of the keypair we registered with the server. */
|
||||
privateKeyB64: string;
|
||||
/**
|
||||
* A base64 string representation of the public part of the keypair we
|
||||
* registered with the server.
|
||||
*/
|
||||
publicKey: string;
|
||||
/**
|
||||
* A base64 string representation of the private part of the keypair we
|
||||
* registered with the server.
|
||||
*/
|
||||
privateKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,14 +83,13 @@ export interface Registration {
|
||||
*/
|
||||
export const register = async (): Promise<Registration> => {
|
||||
// Generate keypair.
|
||||
const { publicKey: publicKeyB64, privateKey: privateKeyB64 } =
|
||||
await generateKeyPair();
|
||||
const { publicKey, privateKey } = await generateKeyPair();
|
||||
|
||||
// Register keypair with museum to get a pairing code.
|
||||
let pairingCode: string | undefined;
|
||||
while (true) {
|
||||
try {
|
||||
pairingCode = await castGateway.registerDevice(publicKeyB64);
|
||||
pairingCode = await registerDevice(publicKey);
|
||||
} catch (e) {
|
||||
log.error("Failed to register public key with server", e);
|
||||
}
|
||||
@@ -90,7 +98,25 @@ export const register = async (): Promise<Registration> => {
|
||||
await wait(10000);
|
||||
}
|
||||
|
||||
return { pairingCode, publicKeyB64, privateKeyB64 };
|
||||
return { pairingCode, publicKey, privateKey };
|
||||
};
|
||||
|
||||
/**
|
||||
* Register the given {@link publicKey} with remote.
|
||||
*
|
||||
* @returns A device code that can be used to pair with us.
|
||||
*/
|
||||
const registerDevice = async (publicKey: string) => {
|
||||
const res = await fetch(await apiURL("/cast/device-info/"), {
|
||||
method: "POST",
|
||||
headers: publicRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
publicKey,
|
||||
}),
|
||||
});
|
||||
ensureOk(res);
|
||||
return z.object({ deviceCode: z.string() }).parse(await res.json())
|
||||
.deviceCode;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,11 +129,11 @@ export const register = async (): Promise<Registration> => {
|
||||
* See: [Note: Pairing protocol].
|
||||
*/
|
||||
export const getCastData = async (registration: Registration) => {
|
||||
const { pairingCode, publicKeyB64, privateKeyB64 } = registration;
|
||||
const { pairingCode, publicKey, privateKey } = registration;
|
||||
|
||||
// The client will send us the encrypted payload using our public key that
|
||||
// we registered with museum.
|
||||
const encryptedCastData = await castGateway.getCastData(pairingCode);
|
||||
const encryptedCastData = await getEncryptedCastData(pairingCode);
|
||||
if (!encryptedCastData) return;
|
||||
|
||||
// Decrypt it using the private key of the pair and return the plaintext
|
||||
@@ -115,11 +141,29 @@ export const getCastData = async (registration: Registration) => {
|
||||
// slideshow for some collection.
|
||||
const decryptedCastData = await boxSealOpen(
|
||||
encryptedCastData,
|
||||
publicKeyB64,
|
||||
privateKeyB64,
|
||||
publicKey,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
// TODO:
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(atob(decryptedCastData));
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch encrypted cast data corresponding to the given {@link code} from remote
|
||||
* if a client has already paired using it.
|
||||
*/
|
||||
const getEncryptedCastData = async (code: string) => {
|
||||
const res = await fetch(await apiURL(`/cast/cast-data/${code}`), {
|
||||
headers: publicRequestHeaders(),
|
||||
});
|
||||
ensureOk(res);
|
||||
return z
|
||||
.object({
|
||||
// encCastData will be null if pairing hasn't happened yet for the
|
||||
// given code.
|
||||
encCastData: z.string().nullish().transform(nullToUndefined),
|
||||
})
|
||||
.parse(await res.json()).encCastData;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TitledMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { boxSeal } from "@/base/crypto/libsodium";
|
||||
import { boxSeal } from "@/base/crypto";
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { photosDialogZIndex } from "@/new/photos/components/utils/z-index";
|
||||
@@ -188,21 +188,23 @@ export const AlbumCastDialog: React.FC<AlbumCastDialogProps> = ({
|
||||
)}
|
||||
{view == "pin" && (
|
||||
<>
|
||||
<Typography>
|
||||
<Trans
|
||||
i18nKey="visit_cast_url"
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://cast.ente.io"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
values={{ url: "cast.ente.io" }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography>{t("enter_cast_pin_code")}</Typography>
|
||||
<Stack sx={{ gap: 2, mb: 2 }}>
|
||||
<Typography>
|
||||
<Trans
|
||||
i18nKey="visit_cast_url"
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://cast.ente.io"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
values={{ url: "cast.ente.io" }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography>{t("enter_cast_pin_code")}</Typography>
|
||||
</Stack>
|
||||
<SingleInputForm
|
||||
callback={onSubmit}
|
||||
fieldType="text"
|
||||
@@ -211,7 +213,11 @@ export const AlbumCastDialog: React.FC<AlbumCastDialogProps> = ({
|
||||
buttonText={t("pair_device_to_tv")}
|
||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||
/>
|
||||
<Button variant="text" onClick={() => setView("choose")}>
|
||||
<Button
|
||||
variant="text"
|
||||
fullWidth
|
||||
onClick={() => setView("choose")}
|
||||
>
|
||||
{t("GO_BACK")}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { clientPackageName, isDesktop } from "@/base/app";
|
||||
import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import { encryptToB64, generateEncryptionKey } from "@/base/crypto/libsodium";
|
||||
import { clientPackageHeader, HTTPError } from "@/base/http";
|
||||
import { HTTPError, publicRequestHeaders } from "@/base/http";
|
||||
import log from "@/base/log";
|
||||
import { accountsAppOrigin, apiURL } from "@/base/origins";
|
||||
import { TwoFactorAuthorizationResponse } from "@/base/types/credentials";
|
||||
@@ -229,7 +229,7 @@ export const checkPasskeyVerificationStatus = async (
|
||||
const url = await apiURL("/users/two-factor/passkeys/get-token");
|
||||
const params = new URLSearchParams({ sessionID });
|
||||
const res = await fetch(`${url}?${params.toString()}`, {
|
||||
headers: clientPackageHeader(),
|
||||
headers: publicRequestHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status == 404 || res.status == 410)
|
||||
|
||||
@@ -83,3 +83,9 @@ export const _decryptMetadataJSON = async (r: {
|
||||
},
|
||||
r.keyB64,
|
||||
);
|
||||
|
||||
export const _generateKeyPair = libsodium.generateKeyPair;
|
||||
|
||||
export const _boxSeal = libsodium.boxSeal;
|
||||
|
||||
export const _boxSealOpen = libsodium.boxSealOpen;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* @file Higher level functions that use the ontology of Ente's requirements.
|
||||
*
|
||||
* For more detailed documentation of specific functions, see the corresponding
|
||||
* function in `libsodium.ts`.
|
||||
*
|
||||
* [Note: Crypto code hierarchy]
|
||||
*
|
||||
* 1. @/base/crypto (Crypto API for our code)
|
||||
@@ -172,8 +175,6 @@ export const encryptThumbnail = (data: BytesOrB64, key: BytesOrB64) =>
|
||||
/**
|
||||
* Encrypt the given data using chunked streaming encryption, but process all
|
||||
* the chunks in one go.
|
||||
*
|
||||
* For more details, see {@link encryptStreamBytes} in `libsodium.ts`.
|
||||
*/
|
||||
export const encryptStreamBytes = async (data: Uint8Array, key: BytesOrB64) =>
|
||||
inWorker()
|
||||
@@ -182,8 +183,6 @@ export const encryptStreamBytes = async (data: Uint8Array, key: BytesOrB64) =>
|
||||
|
||||
/**
|
||||
* Prepare for chunked streaming encryption using {@link encryptStreamChunk}.
|
||||
*
|
||||
* For more details, see {@link initChunkEncryption} in `libsodium.ts`.
|
||||
*/
|
||||
export const initChunkEncryption = async (key: BytesOrB64) =>
|
||||
inWorker()
|
||||
@@ -192,8 +191,6 @@ export const initChunkEncryption = async (key: BytesOrB64) =>
|
||||
|
||||
/**
|
||||
* Encrypt a chunk as part of a chunked streaming encryption.
|
||||
*
|
||||
* For more details, see {@link encryptStreamChunk} in `libsodium.ts`.
|
||||
*/
|
||||
export const encryptStreamChunk = async (
|
||||
data: Uint8Array,
|
||||
@@ -346,3 +343,33 @@ export const decryptMetadataJSON = (r: {
|
||||
inWorker()
|
||||
? ei._decryptMetadataJSON(r)
|
||||
: sharedCryptoWorker().then((w) => w.decryptMetadataJSON(r));
|
||||
|
||||
/**
|
||||
* Generate a new public/private keypair.
|
||||
*/
|
||||
export const generateKeyPair = async () =>
|
||||
inWorker()
|
||||
? ei._generateKeyPair()
|
||||
: sharedCryptoWorker().then((w) => w.generateKeyPair());
|
||||
|
||||
/**
|
||||
* Public key encryption.
|
||||
*/
|
||||
export const boxSeal = async (data: string, publicKey: string) =>
|
||||
inWorker()
|
||||
? ei._boxSeal(data, publicKey)
|
||||
: sharedCryptoWorker().then((w) => w.boxSeal(data, publicKey));
|
||||
|
||||
/**
|
||||
* Decrypt the result of {@link boxSeal}.
|
||||
*/
|
||||
export const boxSealOpen = async (
|
||||
encryptedData: string,
|
||||
publicKey: string,
|
||||
secretKey: string,
|
||||
) =>
|
||||
inWorker()
|
||||
? ei._boxSealOpen(encryptedData, publicKey, secretKey)
|
||||
: sharedCryptoWorker().then((w) =>
|
||||
w.boxSealOpen(encryptedData, publicKey, secretKey),
|
||||
);
|
||||
|
||||
@@ -663,6 +663,64 @@ export async function completeChunkHashing(hashState: sodium.StateAddress) {
|
||||
return hashString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new public/private keypair for use with public-key encryption
|
||||
* functions, and return their base64 string representations.
|
||||
*
|
||||
* These keys are suitable for being used with the {@link boxSeal} and
|
||||
* {@link boxSealOpen} functions.
|
||||
*/
|
||||
export const generateKeyPair = async () => {
|
||||
await sodium.ready;
|
||||
const keyPair = sodium.crypto_box_keypair();
|
||||
return {
|
||||
publicKey: await toB64(keyPair.publicKey),
|
||||
privateKey: await toB64(keyPair.privateKey),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Public key encryption.
|
||||
*
|
||||
* Encrypt the given {@link data} using the given {@link publicKey}.
|
||||
*
|
||||
* This function performs asymmetric (public-key) encryption. To decrypt the
|
||||
* result, use {@link boxSealOpen}.
|
||||
*
|
||||
* @param data The input data to encrypt, represented as a base64 string.
|
||||
*
|
||||
* @param publicKey The public key to use for encryption (as a base64 string).
|
||||
*
|
||||
* @returns The encrypted data (as a base64 string).
|
||||
*/
|
||||
export const boxSeal = async (data: string, publicKey: string) => {
|
||||
await sodium.ready;
|
||||
return toB64(
|
||||
sodium.crypto_box_seal(await fromB64(data), await fromB64(publicKey)),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrypt the result of {@link boxSeal}.
|
||||
*
|
||||
* All parameters, and the result, are base64 string representations of the
|
||||
* underlying data.
|
||||
*/
|
||||
export const boxSealOpen = async (
|
||||
encryptedData: string,
|
||||
publicKey: string,
|
||||
secretKey: string,
|
||||
) => {
|
||||
await sodium.ready;
|
||||
return toB64(
|
||||
sodium.crypto_box_seal_open(
|
||||
await fromB64(encryptedData),
|
||||
await fromB64(publicKey),
|
||||
await fromB64(secretKey),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export async function deriveKey(
|
||||
passphrase: string,
|
||||
salt: string,
|
||||
@@ -732,47 +790,6 @@ export async function generateSaltToDeriveKey() {
|
||||
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new public/private keypair, and return their base64
|
||||
* representations.
|
||||
*/
|
||||
export const generateKeyPair = async () => {
|
||||
await sodium.ready;
|
||||
const keyPair = sodium.crypto_box_keypair();
|
||||
return {
|
||||
publicKey: await toB64(keyPair.publicKey),
|
||||
privateKey: await toB64(keyPair.privateKey),
|
||||
};
|
||||
};
|
||||
|
||||
export async function boxSealOpen(
|
||||
input: string,
|
||||
publicKey: string,
|
||||
secretKey: string,
|
||||
) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_box_seal_open(
|
||||
await fromB64(input),
|
||||
await fromB64(publicKey),
|
||||
await fromB64(secretKey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the given {@link input} using the given {@link publicKey}.
|
||||
*
|
||||
* This function performs asymmetric (public-key) encryption. To decrypt the
|
||||
* result, use {@link boxSealOpen}.
|
||||
*/
|
||||
export async function boxSeal(input: string, publicKey: string) {
|
||||
await sodium.ready;
|
||||
return await toB64(
|
||||
sodium.crypto_box_seal(await fromB64(input), await fromB64(publicKey)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateSubKey(
|
||||
key: string,
|
||||
subKeyLength: number,
|
||||
|
||||
@@ -33,6 +33,9 @@ export class CryptoWorker {
|
||||
decryptStreamChunk = ei._decryptStreamChunk;
|
||||
decryptMetadataJSON_New = ei._decryptMetadataJSON_New;
|
||||
decryptMetadataJSON = ei._decryptMetadataJSON;
|
||||
generateKeyPair = ei._generateKeyPair;
|
||||
boxSeal = ei._boxSeal;
|
||||
boxSealOpen = ei._boxSealOpen;
|
||||
|
||||
// TODO: -- AUDIT BELOW --
|
||||
|
||||
@@ -93,18 +96,6 @@ export class CryptoWorker {
|
||||
return libsodium.generateSaltToDeriveKey();
|
||||
}
|
||||
|
||||
async generateKeyPair() {
|
||||
return libsodium.generateKeyPair();
|
||||
}
|
||||
|
||||
async boxSealOpen(input: string, publicKey: string, secretKey: string) {
|
||||
return libsodium.boxSealOpen(input, publicKey, secretKey);
|
||||
}
|
||||
|
||||
async boxSeal(input: string, publicKey: string) {
|
||||
return libsodium.boxSeal(input, publicKey);
|
||||
}
|
||||
|
||||
async generateSubKey(
|
||||
key: string,
|
||||
subKeyLength: number,
|
||||
|
||||
@@ -15,10 +15,12 @@ export const authenticatedRequestHeaders = async () => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* Return a headers object with "X-Client-Package" header set to the client
|
||||
* package name of the current app.
|
||||
* Return headers that should be passed alongwith (almost) all unauthenticated
|
||||
* `fetch` calls that we make to our API servers.
|
||||
*
|
||||
* - The client package name.
|
||||
*/
|
||||
export const clientPackageHeader = () => ({
|
||||
export const publicRequestHeaders = () => ({
|
||||
"X-Client-Package": clientPackageName,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/base/crypto";
|
||||
import {
|
||||
authenticatedRequestHeaders,
|
||||
clientPackageHeader,
|
||||
publicRequestHeaders,
|
||||
retryEnsuringHTTPOk,
|
||||
} from "@/base/http";
|
||||
import { ensureAuthToken } from "@/base/local-user";
|
||||
@@ -617,7 +617,7 @@ const photos_downloadThumbnail = async (file: EnteFile) => {
|
||||
return fetch(
|
||||
`${customOrigin}/files/preview/${file.id}?${params.toString()}`,
|
||||
{
|
||||
headers: clientPackageHeader(),
|
||||
headers: publicRequestHeaders(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -676,7 +676,7 @@ const photos_downloadFile = async (file: EnteFile): Promise<Response> => {
|
||||
return fetch(
|
||||
`${customOrigin}/files/download/${file.id}?${params.toString()}`,
|
||||
{
|
||||
headers: clientPackageHeader(),
|
||||
headers: publicRequestHeaders(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -714,7 +714,7 @@ const publicAlbums_downloadThumbnail = async (
|
||||
return fetch(
|
||||
`${customOrigin}/public-collection/files/preview/${file.id}?${params.toString()}`,
|
||||
{
|
||||
headers: clientPackageHeader(),
|
||||
headers: publicRequestHeaders(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -722,7 +722,7 @@ const publicAlbums_downloadThumbnail = async (
|
||||
`https://public-albums.ente.io/preview/?fileID=${file.id}`,
|
||||
{
|
||||
headers: {
|
||||
...clientPackageHeader(),
|
||||
...publicRequestHeaders(),
|
||||
"X-Auth-Access-Token": accessToken,
|
||||
...(accessTokenJWT
|
||||
? { "X-Auth-Access-Token-JWT": accessTokenJWT }
|
||||
@@ -758,7 +758,7 @@ const publicAlbums_downloadFile = async (
|
||||
`https://public-albums.ente.io/download/?fileID=${file.id}`,
|
||||
{
|
||||
headers: {
|
||||
...clientPackageHeader(),
|
||||
...publicRequestHeaders(),
|
||||
"X-Auth-Access-Token": accessToken,
|
||||
...(accessTokenJWT
|
||||
? { "X-Auth-Access-Token-JWT": accessTokenJWT }
|
||||
|
||||
@@ -56,16 +56,6 @@ class CastGateway {
|
||||
return resp.data.publicKey;
|
||||
}
|
||||
|
||||
public async registerDevice(publicKey: string): Promise<string> {
|
||||
const resp = await HTTPService.post(
|
||||
await apiURL("/cast/device-info/"),
|
||||
{
|
||||
publicKey: publicKey,
|
||||
},
|
||||
);
|
||||
return resp.data.deviceCode;
|
||||
}
|
||||
|
||||
public async publishCastPayload(
|
||||
code: string,
|
||||
castPayload: string,
|
||||
|
||||
Reference in New Issue
Block a user