@replit/extensions
Version:
The Replit Extensions client is a module that allows you to easily interact with the Workspace.
1 lines • 101 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/types/fs.ts","../src/types/themes.ts","../src/types/data.ts","../src/types/index.ts","../src/util/comlink.ts","../src/util/handshake.ts","../src/api/fs/index.ts","../src/api/fs/watching.ts","../src/api/replDb.ts","../src/api/me.ts","../src/api/theme.ts","../src/api/messages.ts","../src/api/data.ts","../src/api/session.ts","../src/api/experimental/index.ts","../src/api/experimental/auth.ts","../src/auth/ed25519.ts","../src/auth/base64.ts","../src/auth/verify.ts","../src/api/experimental/editor.ts","../src/api/commands.ts","../src/commands/index.ts","../src/api/internal/index.ts","../src/api/exec.ts","../src/api/debug.ts","../package.json","../src/util/patchConsole.ts"],"sourcesContent":["import { HandshakeStatus, ReplitInitArgs, ReplitInitOutput } from \"./types\";\nimport { extensionPort, proxy } from \"./util/comlink\";\nimport { getHandshakeStatus, setHandshakeStatus } from \"./util/handshake\";\nexport * from \"./api\";\nexport { extensionPort, proxy };\nexport * from \"./types\";\nexport * from \"./commands\";\nimport * as replit from \".\";\n\nimport { version } from \"../package.json\";\nimport { patchConsole } from \"./util/patchConsole\";\n\nexport { version };\n\nfunction promiseWithTimeout<T>(promise: Promise<T>, timeout: number) {\n return Promise.race([\n promise,\n new Promise((_resolve, reject) =>\n setTimeout(() => reject(new Error(\"timeout\")), timeout)\n ),\n ]);\n}\n\nasync function windowIsReady() {\n return new Promise<void>((resolve) => {\n if (document.readyState === \"complete\") {\n resolve();\n return;\n }\n\n const loadHandler = () => {\n resolve();\n window.removeEventListener(\"load\", loadHandler);\n };\n\n window.addEventListener(\"load\", loadHandler);\n });\n}\n\nexport async function init(args?: ReplitInitArgs): Promise<ReplitInitOutput> {\n if (extensionPort === null) {\n throw new Error(\"Extension must be initialized in a browser context\");\n }\n\n const onExtensionClick = () => {\n extensionPort.activatePane();\n };\n\n const windDown = () => {\n window.document.removeEventListener(\"click\", onExtensionClick);\n };\n\n try {\n if (window) {\n await windowIsReady();\n }\n\n await promiseWithTimeout(\n extensionPort.handshake({\n clientName: \"@replit/extensions\",\n clientVersion: version,\n }),\n args?.timeout || 2000\n );\n\n patchConsole();\n\n setHandshakeStatus(HandshakeStatus.Ready);\n\n if (window) {\n window.document.addEventListener(\"click\", onExtensionClick);\n }\n } catch (e) {\n setHandshakeStatus(HandshakeStatus.Error);\n console.error(e);\n windDown();\n throw e;\n }\n\n return {\n dispose: windDown,\n status: getHandshakeStatus(),\n };\n}\n","/**\n * A Filesystem node type\n */\nexport enum FsNodeType {\n File = \"FILE\",\n Directory = \"DIRECTORY\",\n}\n\n/**\n * A base interface for nodes, just includes\n * the type of the node and the path, This interface\n * does not expose the node's content/children\n */\nexport interface FsNode {\n path: string;\n type: FsNodeType;\n}\n\n/**\n * An array of Filesystem Nodes\n */\nexport type FsNodeArray = Array<FsNode>;\n\n/**\n * A directory child node - a file or a folder.\n */\nexport interface DirectoryChildNode {\n filename: string;\n type: FsNodeType;\n}\n\n/**\n * A file change event type\n */\nexport enum ChangeEventType {\n Create = \"CREATE\",\n Move = \"MOVE\",\n Delete = \"DELETE\",\n Modify = \"MODIFY\",\n}\n\n/**\n * Fired when a file is moved\n */\nexport interface MoveEvent {\n eventType: ChangeEventType.Move;\n node: FsNode;\n to: string;\n}\n\n/**\n * Fired when a file is deleted\n */\nexport interface DeleteEvent {\n eventType: ChangeEventType.Delete;\n node: FsNode;\n}\n\n/**\n * Fires when a non-text file is changed\n */\nexport type WatchFileOnChangeListener<T extends string | Blob = string> = (\n newContent: T\n) => void;\n\n/**\n * Fires when watching a non-text file fails\n */\nexport type WatchFileOnErrorListener = (error: string) => void;\n\n/**\n * Fires when a non-text file is moved or deleted\n */\nexport type WatchFileOnMoveOrDeleteListener = (\n moveOrDeleteEvent: MoveEvent | DeleteEvent\n) => void;\n\n/**\n * A set of listeners for watching a non-text file\n */\nexport interface WatchFileListeners<T extends string | Blob = string> {\n onChange: WatchFileOnChangeListener<T>;\n onError?: WatchFileOnErrorListener;\n onMoveOrDelete?: WatchFileOnMoveOrDeleteListener;\n}\n\n/**\n * A written text change for the WriteChange function exposed by WatchTextFileListeners.onReady\n */\nexport interface TextChange {\n from: number;\n to?: number;\n insert?: string;\n}\n\n/**\n * Writes a change to a watched file using the TextChange interface\n */\nexport type WriteChange = (changes: TextChange | Array<TextChange>) => void;\n\n/**\n * Returns the latest content of a watched file as a string\n */\nexport type GetLatestContent = () => string;\n\n/**\n * A set of listeners and values exposed by WatchTextFileListeners.onReady\n */\nexport interface TextFileReadyEvent {\n writeChange: WriteChange;\n getLatestContent: GetLatestContent;\n initialContent: string;\n}\n\n/**\n * Signifies a change when a text file's text content is updated\n */\nexport interface TextFileOnChangeEvent {\n changes: Array<TextChange>;\n latestContent: string;\n}\n\n/**\n * Fires when a text file watcher is ready\n */\nexport type WatchTextFileOnReadyListener = (\n readyEvent: TextFileReadyEvent\n) => void;\n\n/**\n * Fires when a watched text file's text content is updated\n */\nexport type WatchTextFileOnChangeListener = (\n changeEvent: TextFileOnChangeEvent\n) => void;\n\n/**\n * Fires when watching a text file fails\n */\nexport type WatchTextFileOnErrorListener = (error: string) => void;\n\n/**\n * Fires when a watched text file is moved or deleted\n */\nexport type WatchTextFileOnMoveOrDeleteListener = (\n moveOrDeleteEvent: MoveEvent | DeleteEvent\n) => void;\n\n/**\n * A set of listeners for watching a text file\n */\nexport interface WatchTextFileListeners {\n onReady: WatchTextFileOnReadyListener;\n onChange?: WatchTextFileOnChangeListener;\n onError?: WatchTextFileOnErrorListener;\n onMoveOrDelete?: WatchTextFileOnMoveOrDeleteListener;\n}\n\n/**\n * Fires when watching a directory fails\n */\nexport type WatchDirOnErrorListener = (\n err: Error,\n extraInfo?: Record<string, any>\n) => void;\n\n/**\n * Fires when a directory's child nodes change\n */\nexport type WatchDirOnChangeListener = (children: FsNodeArray) => void;\n\n/**\n * Fires when a watched directory is moved or deleted\n */\nexport type WatchDirOnMoveOrDeleteListener = (\n event: DeleteEvent | MoveEvent\n) => void;\n\n/**\n * A set of listeners for watching a directory\n */\nexport interface WatchDirListeners {\n onChange: WatchDirOnChangeListener;\n onMoveOrDelete?: WatchDirOnMoveOrDeleteListener;\n onError: WatchDirOnErrorListener;\n}\n","import { User } from \"./data\";\n\n/**\n * Alias for strings\n */\nexport type CssColor = string;\n\n/**\n * Global theme values interface\n */\nexport interface ThemeValuesGlobal {\n __typename?: string;\n backgroundRoot: CssColor;\n backgroundDefault: CssColor;\n backgroundHigher: CssColor;\n backgroundHighest: CssColor;\n backgroundOverlay: CssColor;\n foregroundDefault: CssColor;\n foregroundDimmer: CssColor;\n foregroundDimmest: CssColor;\n outlineDimmest: CssColor;\n outlineDimmer: CssColor;\n outlineDefault: CssColor;\n outlineStronger: CssColor;\n outlineStrongest: CssColor;\n accentPrimaryDimmest: CssColor;\n accentPrimaryDimmer: CssColor;\n accentPrimaryDefault: CssColor;\n accentPrimaryStronger: CssColor;\n accentPrimaryStrongest: CssColor;\n accentPositiveDimmest: CssColor;\n accentPositiveDimmer: CssColor;\n accentPositiveDefault: CssColor;\n accentPositiveStronger: CssColor;\n accentPositiveStrongest: CssColor;\n accentNegativeDimmest: CssColor;\n accentNegativeDimmer: CssColor;\n accentNegativeDefault: CssColor;\n accentNegativeStronger: CssColor;\n accentNegativeStrongest: CssColor;\n redDimmest: CssColor;\n redDimmer: CssColor;\n redDefault: CssColor;\n redStronger: CssColor;\n redStrongest: CssColor;\n orangeDimmest: CssColor;\n orangeDimmer: CssColor;\n orangeDefault: CssColor;\n orangeStronger: CssColor;\n orangeStrongest: CssColor;\n yellowDimmest: CssColor;\n yellowDimmer: CssColor;\n yellowDefault: CssColor;\n yellowStronger: CssColor;\n yellowStrongest: CssColor;\n limeDimmest: CssColor;\n limeDimmer: CssColor;\n limeDefault: CssColor;\n limeStronger: CssColor;\n limeStrongest: CssColor;\n greenDimmest: CssColor;\n greenDimmer: CssColor;\n greenDefault: CssColor;\n greenStronger: CssColor;\n greenStrongest: CssColor;\n tealDimmest: CssColor;\n tealDimmer: CssColor;\n tealDefault: CssColor;\n tealStronger: CssColor;\n tealStrongest: CssColor;\n blueDimmest: CssColor;\n blueDimmer: CssColor;\n blueDefault: CssColor;\n blueStronger: CssColor;\n blueStrongest: CssColor;\n blurpleDimmest: CssColor;\n blurpleDimmer: CssColor;\n blurpleDefault: CssColor;\n blurpleStronger: CssColor;\n blurpleStrongest: CssColor;\n purpleDimmest: CssColor;\n purpleDimmer: CssColor;\n purpleDefault: CssColor;\n purpleStronger: CssColor;\n purpleStrongest: CssColor;\n magentaDimmest: CssColor;\n magentaDimmer: CssColor;\n magentaDefault: CssColor;\n magentaStronger: CssColor;\n magentaStrongest: CssColor;\n pinkDimmest: CssColor;\n pinkDimmer: CssColor;\n pinkDefault: CssColor;\n pinkStronger: CssColor;\n pinkStrongest: CssColor;\n greyDimmest: CssColor;\n greyDimmer: CssColor;\n greyDefault: CssColor;\n greyStronger: CssColor;\n greyStrongest: CssColor;\n brownDimmest: CssColor;\n brownDimmer: CssColor;\n brownDefault: CssColor;\n brownStronger: CssColor;\n brownStrongest: CssColor;\n black: CssColor;\n white: CssColor;\n}\n\n/**\n * Enumerated Color Scheme\n */\nexport enum ColorScheme {\n Light = \"light\",\n Dark = \"dark\",\n}\n\n/**\n * Custom Theme GraphQL type\n */\nexport interface CustomTheme {\n author: User;\n colorScheme: ColorScheme;\n hasUnpublishedChanges: boolean;\n id: number;\n isCurrentUserThemeAuthor: boolean;\n isInstalledByCurrentUser: boolean;\n latestThemeVersion: ThemeVersion;\n numInstalls?: number;\n slug?: string;\n status?: \"public\" | \"private\";\n title?: string;\n}\n\n/**\n * Theme Syntax Highlighting Tag\n */\nexport interface ThemeSyntaxHighlightingTag {\n __typename: string;\n name: string;\n modifiers: null | Array<string>;\n}\n\n/**\n * Theme Syntax Highlighting Modifier\n */\nexport interface ThemeSyntaxHighlightingModifier {\n textDecoration?: string;\n fontSize?: string;\n fontWeight?: string;\n fontStyle?: string;\n color?: string;\n}\n\n/**\n * Theme Editor Syntax Highlighting\n */\nexport interface ThemeEditorSyntaxHighlighting {\n __typename: string;\n tags: Array<ThemeSyntaxHighlightingTag>;\n values: ThemeSyntaxHighlightingModifier;\n}\n\n/**\n * Editor Theme Values, an array of ThemeEditorSyntaxHighlighting\n */\nexport interface ThemeValuesEditor {\n syntaxHighlighting: Array<ThemeEditorSyntaxHighlighting>;\n}\n\n/**\n * Both global and editor theme values\n */\nexport interface ThemeValues {\n __typename?: string;\n editor: ThemeValuesEditor;\n global: ThemeValuesGlobal;\n}\n\n/**\n * Theme Version GraphQL type\n */\nexport interface ThemeVersion {\n __typename?: string;\n id: number;\n hue: number;\n lightness: number;\n saturation: number;\n timeUpdated?: string;\n description?: string;\n customTheme?: CustomTheme;\n values?: ThemeValues;\n}\n\n/**\n * Fires with the new theme values when the current theme changes\n */\nexport type OnThemeChangeValuesListener = (values: ThemeValuesGlobal) => void;\n\n/**\n * Fires with the new theme data when the current theme changes\n */\nexport type OnThemeChangeListener = (theme: ThemeVersion) => void;\n","/**\n * A Replit user\n */\nexport interface User {\n id: number;\n username: string;\n image: string;\n bio?: string;\n\n // SocialUserData fragment\n url?: string;\n socials?: Array<UserSocial>;\n firstName?: string;\n lastName?: string;\n displayName?: string;\n fullName?: string;\n followCount?: number;\n followerCount?: number;\n\n // PlanUserData fragment\n isUserHacker?: boolean;\n isUserPro?: boolean;\n\n // RolesUserData fragment\n roles?: Array<UserRole>;\n}\n\n/**\n * Extended values for the current user\n */\nexport interface CurrentUser extends User {}\n\n/**\n * A user social media link\n */\nexport interface UserSocial {\n id: number;\n url: string;\n type: UserSocialType;\n}\n\n/**\n * An enumerated type of social media links\n */\nexport enum UserSocialType {\n twitter = \"twitter\",\n github = \"github\",\n linkedin = \"linkedin\",\n website = \"website\",\n youtube = \"youtube\",\n twitch = \"twitch\",\n facebook = \"facebook\",\n discord = \"discord\",\n}\n\n/**\n * A user role\n */\nexport interface UserRole {\n id: number;\n name: string;\n key: string;\n tagline: string;\n}\n\n/**\n * A Repl\n */\nexport interface Repl {\n id: string;\n url: string;\n title: string;\n description: string;\n timeCreated: string;\n slug: string;\n isPrivate: boolean;\n\n // SocialReplData fragment\n likeCount?: number;\n publicForkCount?: number;\n runCount?: number;\n commentCount?: number;\n tags?: Array<Tag>;\n iconUrl?: string;\n imageUrl?: string;\n\n // CommentsReplData fragment\n comments?: ReplCommentConnection;\n\n // OwnerData fragment\n owner?: ReplOwner;\n\n // MultiplayersData fragment\n multiplayers?: Array<User>;\n}\n\n/**\n * A Repl Owner, can be either a User or a Team\n */\nexport interface ReplOwner {\n id: number;\n username: string;\n image: string;\n __typename: string;\n description?: string;\n}\n\n/**\n * A Repl tag\n */\nexport interface Tag {\n id: string;\n isOfficial: boolean;\n}\n\n/**\n * A Repl Comment\n */\nexport interface ReplComment {\n id: number;\n body: string;\n user: User;\n}\n\n/**\n * An array of ReplComments as items\n */\nexport interface ReplCommentConnection {\n items: Array<ReplComment>;\n}\n\n/**\n * Editor Preferences\n */\nexport interface EditorPreferences {\n __typename: string;\n fontSize: number;\n indentIsSpaces: boolean;\n indentSize: number;\n keyboardHandler: string;\n wrapping: boolean;\n codeIntelligence: boolean;\n codeSuggestion: boolean;\n multiselectModifierKey: string;\n minimapDisplay: string;\n}\n\n/**\n * Options for user queries\n */\nexport interface UserDataInclusion {\n includeSocialData?: boolean;\n includeRoles?: boolean;\n includePlan?: boolean;\n}\n\n/**\n * Options for the currentUser query\n */\nexport interface CurrentUserDataInclusion {\n includeSocialData?: boolean;\n includeRoles?: boolean;\n includePlan?: boolean;\n}\n\n/**\n * Options for repl queries\n */\nexport interface ReplDataInclusion {\n includeSocialData?: boolean;\n includeComments?: boolean;\n includeOwner?: boolean;\n includeMultiplayers?: boolean;\n}\n\n/**\n * A graphql response\n */\nexport type GraphResponse<T> = Promise<T | never>;\n\n/**\n * A graphql response for the repl query\n */\nexport type ReplQueryOutput = GraphResponse<{ repl: Repl }>;\n\n/**\n * A graphql response for the userByUsername query\n */\nexport type UserByUsernameQueryOutput = GraphResponse<{ userByUsername: User }>;\n\n/**\n * A graphql response for the user query\n */\nexport type UserQueryOutput = GraphResponse<{ user: User }>;\n\n/**\n * A graphql response for the currentUser query\n */\nexport type CurrentUserQueryOutput = GraphResponse<{ user: CurrentUser }>;\n","import {\n DirectoryChildNode,\n WatchFileListeners,\n WatchTextFileListeners,\n WatchDirListeners,\n} from \"./fs\";\nimport {\n UserDataInclusion,\n UserQueryOutput,\n UserByUsernameQueryOutput,\n ReplDataInclusion,\n ReplQueryOutput,\n CurrentUserQueryOutput,\n CurrentUserDataInclusion,\n EditorPreferences,\n} from \"./data\";\nimport {\n ThemeValuesGlobal,\n ThemeVersion,\n OnThemeChangeValuesListener,\n OnThemeChangeListener,\n} from \"./themes\";\nimport { OnActiveFileChangeListener } from \"./session\";\nimport Comlink from \"comlink\";\nimport { Data } from \"../api/debug\";\nimport { CommandFnArgs, CommandProxy, CreateCommand } from \"../commands\";\n\nexport * from \"./fs\";\nexport * from \"./themes\";\nexport * from \"./data\";\nexport * from \"./session\";\nexport * from \"./exec\";\nexport * from \"./auth\";\n\n/**\n * An enumerated set of values for the Handshake between the workspace and an extension\n */\nexport enum HandshakeStatus {\n Ready = \"ready\",\n Error = \"error\",\n Loading = \"loading\",\n}\n\n/**\n * The Replit init() function arguments\n */\nexport interface ReplitInitArgs {\n timeout?: number;\n}\n\n/**\n * The output of the Replit init() function\n */\nexport interface ReplitInitOutput {\n dispose: () => void;\n status: HandshakeStatus;\n}\n\n/**\n * A cleanup/disposer function (void)\n */\nexport type DisposerFunction = () => void;\n\n/**\n * The Extension Port\n */\nexport type ExtensionPortAPI = {\n handshake: (handshakeArgs: { clientName: string; clientVersion: string }) => {\n success: true;\n };\n\n // fs Module\n readFile: (\n path: string,\n encoding: \"utf8\" | \"binary\" | null\n ) => Promise<\n | { content: string }\n | {\n error: string;\n }\n >;\n writeFile: (\n path: string,\n content: string | Blob\n ) => Promise<\n | { success: boolean }\n | {\n error: string;\n }\n >;\n readDir: (path: string) => Promise<{\n children: Array<DirectoryChildNode>;\n error: string;\n }>;\n createDir: (path: string) => Promise<{\n success: boolean;\n error: string | null;\n }>;\n deleteFile: (path: string) => Promise<\n | {}\n | {\n error: string;\n }\n >;\n deleteDir: (path: string) => Promise<\n | {}\n | {\n error: string;\n }\n >;\n move: (\n path: string,\n to: string\n ) => Promise<{\n success: boolean;\n error: string | null;\n }>;\n copyFile: (\n path: string,\n to: string\n ) => Promise<{\n success: boolean;\n error: string | null;\n }>;\n watchFile: (\n path: string,\n watcher: WatchFileListeners,\n encoding: \"utf8\" | \"binary\" | null\n ) => DisposerFunction;\n watchTextFile: (path: string, watcher: WatchTextFileListeners) => () => void;\n watchDir: (path: string, watcher: WatchDirListeners) => DisposerFunction;\n\n // replDb Module\n setReplDbValue: (key: string, value: string) => Promise<void>;\n getReplDbValue: (key: string) =>\n | {\n error: string | null;\n }\n | string;\n listReplDbKeys: (prefix: string) => Promise<\n | { keys: string[] }\n | {\n error: string;\n }\n >;\n deleteReplDbKey: (key: string) => Promise<void>;\n\n activatePane: () => Promise<void>;\n\n // theme\n getCurrentThemeValues: () => Promise<ThemeValuesGlobal>;\n onThemeChangeValues: (\n callback: OnThemeChangeValuesListener\n ) => Promise<DisposerFunction>;\n getCurrentTheme: () => Promise<ThemeVersion>;\n onThemeChange: (callback: OnThemeChangeListener) => Promise<DisposerFunction>;\n\n filePath: string;\n\n // messages Module\n showConfirm: (text: string, length?: number) => string;\n showError: (text: string, length?: number) => string;\n showNotice: (text: string, length?: number) => string;\n showWarning: (text: string, length?: number) => string;\n hideMessage: (id: string) => void;\n hideAllMessages: () => void;\n\n // data Module\n currentUser: (args: CurrentUserDataInclusion) => CurrentUserQueryOutput;\n userById: (args: { id: number } & UserDataInclusion) => UserQueryOutput;\n userByUsername: (\n args: { username: string } & UserDataInclusion\n ) => UserByUsernameQueryOutput;\n currentRepl: (args: ReplDataInclusion) => ReplQueryOutput;\n replById: (args: { id: string } & ReplDataInclusion) => ReplQueryOutput;\n replByUrl: (args: { url: string } & ReplDataInclusion) => ReplQueryOutput;\n\n // session Module\n watchActiveFile: (callback: OnActiveFileChangeListener) => DisposerFunction;\n getActiveFile: () => Promise<string | null>;\n\n commands: {\n registerCommand: (command: CommandProxy) => void;\n registerCreateCommand: (\n data: {\n commandId: string;\n contributions: Array<string>;\n },\n create: (createArgs: CommandFnArgs) => Promise<CommandProxy | null>\n ) => void;\n };\n\n experimental: ExperimentalAPI;\n internal: InternalAPI;\n debug: DebugAPI;\n\n exec: (args: {\n splitStderr?: boolean;\n args: Array<string>;\n env?: {\n [key: string]: string;\n };\n onOutput: (output: string) => void;\n onStdErr: (stderr: string) => void;\n onError: (error: string) => void;\n }) => Promise<{\n dispose: () => void;\n promise: Promise<{\n exitCode: number;\n error: string | null;\n }>;\n }>;\n};\n\nexport type ExperimentalAPI = {\n editor: {\n getPreferences: () => Promise<EditorPreferences>;\n };\n\n auth: {\n getAuthToken: () => Promise<string>;\n };\n};\n\nexport type DebugAPI = {\n info: (message: string, data?: Data) => Promise<void>;\n warn: (message: string, data?: Data) => Promise<void>;\n error: (message: string, data?: Data) => Promise<void>;\n};\n\nexport type InternalAPI = {};\n\nexport type Promisify<T> = T extends Promise<unknown> ? T : Promise<T>;\n\nexport type RemoteProperty<T> = T extends Function | Comlink.ProxyMarked\n ? Comlink.Remote<T>\n : T extends object\n ? T\n : Promisify<T>; // We don't want to promisify objects, but we do want to promisify all other primitives\n\nexport type RemoteObject<T> = {\n [P in keyof T]: RemoteProperty<T[P]>;\n};\n\nexport type ExtensionPort = RemoteObject<ExtensionPortAPI>;\n","import * as Comlink from \"comlink\";\nimport { ExtensionPort } from \"../types\";\n\nexport const extensionPort = (() =>\n typeof window !== \"undefined\"\n ? (Comlink.wrap(\n Comlink.windowEndpoint(self.parent, self, \"*\")\n ) as any as ExtensionPort)\n : null)() as ExtensionPort;\n\nexport const proxy = Comlink.proxy;\nexport const releaseProxy = Comlink.releaseProxy;\n","import { HandshakeStatus } from \"../types\";\n\nlet handshakeStatus: HandshakeStatus = HandshakeStatus.Loading;\n\nexport const setHandshakeStatus = (status: HandshakeStatus) => {\n handshakeStatus = status;\n};\n\nexport const getHandshakeStatus = () => handshakeStatus;\n","import { extensionPort, proxy } from \"../..//util/comlink\";\nimport {\n WatchDirListeners,\n WatchFileListeners,\n WatchTextFileListeners,\n} from \"../../types\";\nimport { fileWatcherManager } from \"./watching\";\n\n/**\n * Reads the file specified at `path` and returns an object containing the contents, or an object containing an error if there was one. Required [permissions](/extensions/api/manifest#scopetype): `read`.\n */\nexport async function readFile(\n path: string,\n encoding: \"utf8\" | \"binary\" | null = \"utf8\"\n) {\n return extensionPort.readFile(path, encoding);\n}\n\n/**\n * Writes the file specified at `path` with the contents `content`. Required [permissions](/extensions/api/manifest#scopetype): `read`, `write-exec`.\n */\nexport async function writeFile(path: string, content: string | Blob) {\n return extensionPort.writeFile(path, content);\n}\n\n/**\n * Reads the directory specified at `path` and returns an object containing the contents, or an object containing an error if there was one. Required [permissions](/extensions/api/manifest#scopetype): `read`.\n */\nexport async function readDir(path: string) {\n return extensionPort.readDir(path);\n}\n\n/**\n * Creates a directory at the specified path. Required [permissions](/extensions/api/manifest#scopetype): `read`, `write-exec`.\n */\nexport async function createDir(path: string) {\n return extensionPort.createDir(path);\n}\n\n/**\n * Deletes the file at the specified path. Required [permissions](/extensions/api/manifest#scopetype): `read`, `write-exec`.\n */\nexport async function deleteFile(path: string) {\n return extensionPort.deleteFile(path);\n}\n\n/**\n * Deletes the directory at the specified path. Required [permissions](/extensions/api/manifest#scopetype): `read`, `write-exec`.\n */\nexport async function deleteDir(path: string) {\n return extensionPort.deleteDir(path);\n}\n\n/**\n * Moves the file or directory at `from` to `to`. Required [permissions](/extensions/api/manifest#scopetype): `read`, `write-exec`.\n */\nexport async function move(path: string, to: string) {\n return extensionPort.move(path, to);\n}\n\n/**\n * Copies the file at `from` to `to`. Required [permissions](/extensions/api/manifest#scopetype): `read`, `write-exec`.\n */\nexport async function copyFile(path: string, to: string) {\n return extensionPort.copyFile(path, to);\n}\n\n/**\n * Watches the file at `path` for changes with the provided `listeners`. Returns a dispose method which cleans up the listeners. Required [permissions](/extensions/api/manifest#scopetype): `read`.\n */\nexport async function watchFile(\n path: string,\n listeners: WatchFileListeners,\n encoding: \"utf8\" | \"binary\" = \"binary\"\n) {\n // Note: comlink does not let us test for functions being present, so we provide default functions for all callbacks in case the user does not pass those, to keep the API flexible\n return extensionPort.watchFile(\n path,\n proxy({\n onMoveOrDelete: () => {},\n onError: () => {},\n ...listeners,\n }),\n encoding\n );\n}\n\n/**\n * Watches file events (move, create, delete) in the specified directory at the given `path`. Returns a dispose method which cleans up the listeners. Required [permissions](/extensions/api/manifest#scopetype): `read`.\n */\nexport async function watchDir(path: string, listeners: WatchDirListeners) {\n return extensionPort.watchDir(\n path,\n proxy({\n onMoveOrDelete: () => {},\n ...listeners,\n })\n );\n}\n\n/**\n * Watches a text file at `path` for changes with the provided `listeners`. Returns a dispose method which cleans up the listeners.\n *\n * Use this for watching text files, and receive changes as versioned operational transform (OT) operations annotated with their source.\n *\n * Required [permissions](/extensions/api/manifest#scopetype): `read`.\n */\nexport function watchTextFile(path: string, listeners: WatchTextFileListeners) {\n return fileWatcherManager.watch(path, listeners);\n}\n","import { ChangeSet, ChangeSpec, Text } from \"@codemirror/state\";\nimport { extensionPort, proxy } from \"../../util/comlink\";\nimport { TextChange, WatchTextFileListeners } from \"../../types\";\n\n/**\n * A helper to change a ChangeSet into a simpler serializable & human readable format\n */\nfunction changeSetToSimpleTextChange(changes: ChangeSet): Array<TextChange> {\n const simpleChanges: Array<TextChange> = [];\n\n changes.iterChanges((fromA, toA, _fromB, _toB, text) => {\n const change: TextChange = { from: fromA };\n\n if (toA > fromA) {\n change.to = toA;\n }\n\n if (text.length) {\n change.insert = text.sliceString(0);\n }\n\n simpleChanges.push(change);\n });\n\n return simpleChanges;\n}\n\n/**\n * watches a file via comlink, notifies listeners about changes.\n * it handles synchronization between local and remote text states.\n * properly disposes resources when no longer needed.\n */\nclass TextFileWatcher {\n /*\n * TODO: what do we do with out of order messages, postMessage has no guarantees of order\n * TODO: we need versioning to guarantee correctness. Related to above, using async/await doesn't guarantee that our change got applied before the next incoming change and vice versa\n */\n private state: {\n localText: Text;\n remoteText: Text;\n unconfirmedChanges: Set<{ changes: ChangeSet }>;\n requestWriteChange: (changes: ChangeSpec) => Promise<void>;\n } | null;\n private isDisposed: boolean;\n public dispose: () => void;\n\n constructor(\n private path: string,\n private listeners: {\n onReady: () => void;\n onChange: NonNullable<WatchTextFileListeners[\"onChange\"]>;\n onMoveOrDelete: NonNullable<WatchTextFileListeners[\"onMoveOrDelete\"]>;\n onError: NonNullable<WatchTextFileListeners[\"onError\"]>;\n }\n ) {\n this.state = null;\n this.isDisposed = false;\n this.dispose = () => {\n this.isDisposed = true;\n };\n\n if (!extensionPort) {\n throw new Error(\"Expected extensionPort\");\n }\n\n extensionPort\n .watchTextFile(\n this.path,\n proxy({\n onReady: this.handleReady.bind(this) as any, // wrongly typed at extensionPort\n onChange: this.handleChange.bind(this),\n onMoveOrDelete: (event) => {\n listeners.onMoveOrDelete(event);\n },\n onError: (error) => {\n listeners.onError(error);\n },\n })\n )\n .then((portDispose) => {\n if (this.isDisposed) {\n portDispose();\n\n return;\n }\n\n this.dispose = () => {\n this.isDisposed = true;\n portDispose();\n };\n });\n }\n\n public writeChange(changes: ChangeSpec) {\n if (this.isDisposed) {\n throw new Error(\"Wrote change on a disposed TextFileWatcher\");\n }\n\n if (!this.state) {\n throw new Error(\"Tried to write changes before ready\");\n }\n\n const changeSet = ChangeSet.of(changes, this.state.localText.length);\n this.state.localText = changeSet.apply(this.state.localText);\n\n this.enqueueChangeSet(changeSet);\n }\n\n public getLatestContent() {\n if (this.isDisposed) {\n throw new Error(\"Cannot get content of a disposed TextFileWatcher\");\n }\n\n if (!this.state) {\n throw new Error(\"Called getLatestContent on an unready TextFileWatcher\");\n }\n\n return this.state.localText.sliceString(0);\n }\n\n public getIsReady() {\n if (this.isDisposed) {\n throw new Error(\"Cannot get isReady of a disposed TextFileWatcher\");\n }\n\n return Boolean(this.state);\n }\n\n private async handleReady({\n writeChange,\n initialContent,\n }: {\n writeChange: (changes: ChangeSpec) => Promise<void>;\n initialContent: Promise<string>;\n }) {\n if (this.isDisposed) {\n return;\n }\n\n const content = Text.of((await initialContent).split(\"\\n\"));\n this.state = {\n requestWriteChange: writeChange,\n localText: content,\n remoteText: content,\n unconfirmedChanges: new Set(),\n };\n\n this.listeners.onReady();\n }\n\n private handleChange({ changes: changeJSON }: { changes: any }) {\n if (this.isDisposed) {\n return;\n }\n\n if (!this.state) {\n throw new Error(\"unexpected handleOnChange called before handleOnReady\");\n }\n\n let changes = ChangeSet.fromJSON(changeJSON);\n\n this.state.remoteText = changes.apply(this.state.remoteText);\n\n for (const unconfirmed of this.state.unconfirmedChanges) {\n const unconfirmedUpdated = unconfirmed.changes.map(changes);\n changes = changes.map(unconfirmed.changes, true);\n unconfirmed.changes = unconfirmedUpdated;\n }\n\n this.state.localText = changes.apply(this.state.localText);\n\n this.listeners.onChange({\n changes: changeSetToSimpleTextChange(changes),\n latestContent: this.getLatestContent(),\n });\n }\n\n private async enqueueChangeSet(changes: ChangeSet) {\n if (this.isDisposed) {\n throw new Error(\"Wrote change on a disposed TextFileWatcher\");\n }\n\n if (!this.state) {\n throw new Error(\"Tried to write changes before ready\");\n }\n\n // Store in a ref since the ChangeSet is immutable, and it will change when fastfowarded\n const ref = { changes };\n this.state.unconfirmedChanges.add(ref);\n await this.state.requestWriteChange(\n changeSetToSimpleTextChange(ref.changes)\n );\n\n this.state.unconfirmedChanges.delete(ref);\n this.state.remoteText = ref.changes.apply(this.state.remoteText);\n }\n}\n\n/**\n * A class that manages multiple `TextFileWatcher` instances\n * ensuring that there's only one watcher per file to make sure\n * we are handling synchronization properly, having multiple watchers\n * will cause issues with the `TextFileWatcher` implementation.\n * Notifies listeners when a file is ready or when there are changes.\n * Automatically disposes watchers when there are no more listeners.\n * This should be a singleton, but it's not enforced for testability.\n */\nclass FileWatcherManager {\n private files: Map<\n string,\n {\n listeners: Set<WatchTextFileListeners>;\n watcher: TextFileWatcher;\n }\n >;\n\n constructor() {\n this.files = new Map();\n }\n\n public watch(path: string, listeners: WatchTextFileListeners) {\n if (this.files.has(path)) {\n this.watchExisting(path, listeners);\n } else {\n this.watchNew(path, listeners);\n }\n\n return () => {\n const file = this.files.get(path);\n\n if (!file) {\n return;\n }\n\n file.listeners.delete(listeners);\n if (file.listeners.size === 0) {\n this.dispose(path);\n }\n };\n }\n\n private dispose(path: string) {\n const file = this.files.get(path);\n\n if (!file) {\n return;\n }\n\n file.watcher.dispose();\n this.files.delete(path);\n }\n\n private watchNew(path: string, listeners: WatchTextFileListeners) {\n const watcher = new TextFileWatcher(path, {\n onReady: () => {\n this.handleReady(path);\n },\n onChange: (changeEvent) => {\n this.handleChange(path, changeEvent);\n },\n onMoveOrDelete: (event) => {\n this.handleMoveOrDelete(path, event);\n },\n onError: (error) => {\n this.handleError(path, error);\n },\n });\n\n this.files.set(path, {\n listeners: new Set([listeners]),\n watcher,\n });\n }\n\n private watchExisting(path: string, listeners: WatchTextFileListeners) {\n const file = this.files.get(path);\n\n if (!file) {\n throw new Error(\"file is not watched\");\n }\n\n file.listeners.add(listeners);\n }\n\n private handleChange(\n path: string,\n changeEvent: Parameters<NonNullable<WatchTextFileListeners[\"onChange\"]>>[0]\n ) {\n const file = this.files.get(path);\n\n if (!file) {\n throw new Error(\"Unexpected change on a non-watched file\");\n }\n\n if (!file.watcher.getIsReady()) {\n throw new Error(\"Unexpected change on a non-ready file\");\n }\n\n for (const { onChange } of file.listeners) {\n if (!onChange) {\n continue;\n }\n\n onChange(changeEvent);\n }\n }\n\n private handleReady(path: string) {\n const file = this.files.get(path);\n\n if (!file) {\n throw new Error(\"Unexpected change on a non-watched file\");\n }\n\n if (!file.watcher.getIsReady()) {\n throw new Error(\"Got ready on a non-ready file :/\");\n }\n\n const initialContent = file.watcher.getLatestContent();\n for (const { onReady, onChange } of file.listeners) {\n onReady({\n initialContent,\n getLatestContent: () => file.watcher.getLatestContent(),\n writeChange: (changes: TextChange | Array<TextChange>) => {\n file.watcher.writeChange(changes);\n\n for (const { onChange: otherOnChange } of file.listeners) {\n if (onChange === otherOnChange) {\n // we don't want to notify the originator, they already know about the change\n continue;\n }\n\n if (!otherOnChange) {\n continue;\n }\n\n otherOnChange({\n changes: Array.isArray(changes) ? changes : [changes],\n latestContent: file.watcher.getLatestContent(),\n });\n }\n },\n });\n }\n }\n\n private handleError(path: string, error: string) {\n const file = this.files.get(path);\n\n if (!file) {\n throw new Error(\"Unexpected error on a non-watched file\");\n }\n\n for (const { onError } of file.listeners) {\n if (!onError) {\n continue;\n }\n\n onError(error);\n }\n\n this.dispose(path);\n }\n\n private handleMoveOrDelete(\n path: string,\n event: Parameters<NonNullable<WatchTextFileListeners[\"onMoveOrDelete\"]>>[0]\n ) {\n const file = this.files.get(path);\n\n if (!file) {\n throw new Error(\"Unexpected move or delete event on a non-watched file\");\n }\n\n for (const { onMoveOrDelete } of file.listeners) {\n if (!onMoveOrDelete) {\n continue;\n }\n\n onMoveOrDelete(event);\n }\n\n this.dispose(path);\n }\n}\n\nexport const fileWatcherManager = new FileWatcherManager();\n","import { extensionPort } from \"../util/comlink\";\n\n/**\n * Sets the value for a given key. Required [permissions](/extensions/api/manifest#scopetype): `repldb:read`, `repldb:write`.\n */\nexport async function set(args: { key: string; value: any }) {\n return extensionPort.setReplDbValue(args.key, args.value);\n}\n\n/**\n * Returns a value associated with the given key. Required [permissions](/extensions/api/manifest#scopetype): `repldb:read`.\n */\nexport async function get(args: { key: string }) {\n return extensionPort.getReplDbValue(args.key);\n}\n\n/**\n * Lists keys in the replDb. Accepts an optional `prefix`, which filters for keys beginning with the given prefix. Required [permissions](/extensions/api/manifest#scopetype): `repldb:read`.\n */\nexport async function list(args: { prefix?: string } = {}) {\n return extensionPort.listReplDbKeys(args?.prefix || \"\");\n}\n\n/**\n * Deletes a key in the replDb. Required [permissions](/extensions/api/manifest#scopetype): `repldb:read`, `repldb:write`.\n */\nexport async function del(args: { key: string }) {\n return extensionPort.deleteReplDbKey(args.key);\n}\n","import { extensionPort } from \"../util/comlink\";\n\n/**\n * Returns the path to the current file the extension is opened with, if it is a [File Handler](/extensions/basics/key-concepts#file-handler).\n */\nexport function filePath() {\n return extensionPort.filePath;\n}\n","import { proxy } from \"comlink\";\nimport {\n DisposerFunction,\n OnThemeChangeListener,\n OnThemeChangeValuesListener,\n ThemeValuesGlobal,\n} from \"../types\";\nimport { extensionPort } from \"../util/comlink\";\n\n/**\n * Returns all metadata on the current theme including syntax highlighting, description, HSL, token values, and more.\n */\nexport async function getCurrentTheme() {\n return await extensionPort.getCurrentTheme();\n}\n\n/**\n * Returns the current theme's global token values.\n */\nexport async function getCurrentThemeValues(): Promise<ThemeValuesGlobal> {\n return await extensionPort.getCurrentThemeValues();\n}\n\n/**\n * Fires the `callback` parameter function with the updated theme when the user's theme changes.\n */\nexport async function onThemeChange(\n callback: OnThemeChangeListener\n): Promise<DisposerFunction> {\n return await extensionPort.onThemeChange(proxy(callback));\n}\n\n/**\n * Fires the `callback` parameter function with the updated theme values when the user's theme changes.\n */\nexport async function onThemeChangeValues(\n callback: OnThemeChangeValuesListener\n): Promise<DisposerFunction> {\n return await extensionPort.onThemeChangeValues(proxy(callback));\n}\n","import { extensionPort } from \"../util/comlink\";\n\n/**\n * Shows a confirmation toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID\n */\nexport const showConfirm = async (str: string, length: number = 4000) => {\n if (typeof str !== \"string\") {\n throw new Error(\"Messages must be strings\");\n }\n\n return extensionPort.showConfirm(str, length);\n};\n\n/**\n * Shows an error toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID\n */\nexport const showError = async (str: string, length: number = 4000) => {\n if (typeof str !== \"string\") {\n throw new Error(\"Messages must be strings\");\n }\n\n return extensionPort.showError(str, length);\n};\n\n/**\n * Shows a notice toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID\n */\nexport const showNotice = async (str: string, length: number = 4000) => {\n if (typeof str !== \"string\") {\n throw new Error(\"Messages must be strings\");\n }\n\n return extensionPort.showNotice(str, length);\n};\n\n/**\n * Shows a warning toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID\n */\nexport const showWarning = async (str: string, length: number = 4000) => {\n if (typeof str !== \"string\") {\n throw new Error(\"Messages must be strings\");\n }\n\n return extensionPort.showWarning(str, length);\n};\n\n/**\n * Hides a message by its IDs\n */\nexport const hideMessage = async (id: string) => {\n return extensionPort.hideMessage(id);\n};\n\n/**\n * Hides all toast messages visible on the screens\n */\nexport const hideAllMessages = async () => {\n return extensionPort.hideAllMessages();\n};\n","import {\n ReplDataInclusion,\n UserDataInclusion,\n CurrentUserDataInclusion,\n} from \"../types\";\nimport { extensionPort } from \"../util/comlink\";\n\n/**\n * Fetches the current user via graphql\n */\nexport async function currentUser(args: CurrentUserDataInclusion = {}) {\n return await extensionPort.currentUser(args);\n}\n\n/**\n * Fetches a user by their id via graphql\n */\nexport async function userById(args: { id: number } & UserDataInclusion) {\n if (typeof args.id !== \"number\") {\n throw new Error(\n `Query parameter \"id\" must be a number. Found type ${typeof args.id} instead.`\n );\n }\n\n return await extensionPort.userById(args);\n}\n\n/**\n * Fetches a user by their username via graphql\n */\nexport async function userByUsername(\n args: { username: string } & UserDataInclusion\n) {\n if (typeof args.username !== \"string\") {\n throw new Error(\n `Query parameter \"username\" must be a string. Found type ${typeof args.username} instead.`\n );\n }\n\n return await extensionPort.userByUsername(args);\n}\n\n/**\n * Fetches the current Repl via graphql\n */\nexport async function currentRepl(args: ReplDataInclusion = {}) {\n return await extensionPort.currentRepl(args);\n}\n\n/**\n * Fetches a Repl by its ID via graphql\n */\nexport async function replById(args: { id: string } & ReplDataInclusion) {\n if (typeof args.id !== \"string\") {\n throw new Error(\n `Query parameter \"id\" must be a string. Found type ${typeof args.id} instead.`\n );\n }\n\n return await extensionPort.replById(args);\n}\n\n/**\n * Fetches a Repl by its URL via graphql\n */\nexport async function replByUrl(args: { url: string } & ReplDataInclusion) {\n if (typeof args.url !== \"string\") {\n throw new Error(\n `Query parameter \"url\" must be a string. Found type ${typeof args.url} instead.`\n );\n }\n\n return await extensionPort.replByUrl(args);\n}\n","import { DisposerFunction, OnActiveFileChangeListener } from \"../types\";\nimport { extensionPort, proxy } from \"../util/comlink\";\n\n/**\n * Sets up a listener to handle when the active file is changed\n */\nexport function onActiveFileChange(\n listener: OnActiveFileChangeListener\n): DisposerFunction {\n let dispose = () => {\n console.log(\"disposing existing watcher\");\n };\n\n extensionPort.watchActiveFile(proxy(listener)).then((d: () => void) => {\n dispose = d;\n });\n\n return () => {\n dispose();\n };\n}\n\n/**\n * Returns the current file the user is focusing\n */\nexport async function getActiveFile() {\n return await extensionPort.getActiveFile();\n}\n","export * as auth from \"./auth\";\nexport * as editor from \"./editor\";\n\n// deprecated from the experimental namespace\nexport * as commands from \"../commands\";\n","import { extensionPort } from \"../../util/comlink\";\nimport { AuthenticateResult } from \"../../types\";\nimport { verifyJWTAndDecode, decodeProtectedHeader } from \"../../auth/verify\";\n\n/**\n * Returns a unique JWT token that can be used to verify that an extension has been loaded on Replit by a particular user\n */\nexport async function getAuthToken() {\n return extensionPort.experimental.auth.getAuthToken();\n}\n\n/**\n * Verifies a provided JWT token and returns the decoded token.\n */\nexport async function verifyAuthToken(\n token: string\n): Promise<{ payload: any; protectedHeader: any }> {\n const tokenHeaders = decodeProtectedHeader(token);\n\n if (tokenHeaders.typ !== \"JWT\") {\n throw new Error(\"Expected typ: JWT\");\n }\n\n if (tokenHeaders.alg !== \"EdDSA\") {\n throw new Error(\"Expected alg: EdDSA\");\n }\n\n if (!tokenHeaders.kid) {\n throw new Error(\"Expected `kid` to be defined\");\n }\n\n const res = await fetch(\n `https://replit.com/data/extensions/publicKey/${tokenHeaders.kid}`\n );\n\n const { ok, value: publicKey } = await res.json();\n\n if (!ok) {\n throw new Error(\"Extension Auth: Failed to fetch public key\");\n }\n\n try {\n const decodedToken = await verifyJWTAndDecode(token, publicKey);\n\n return decodedToken;\n } catch (e) {\n throw new Error(\"Extension Auth: Failed to verify token\");\n }\n}\n\n/**\n * Performs authentication and returns the user and installation information\n */\nexport async function authenticate(): Promise<AuthenticateResult> {\n const token = await getAuthToken();\n const decodedToken = await verifyAuthToken(token);\n\n if (\n typeof decodedToken.payload.userId !== \"number\" ||\n typeof decodedToken.payload.installationId !== \"string\" ||\n typeof decodedToken.payload.extensionId !== \"string\"\n ) {\n throw new Error(\"Failed to authenticate\");\n }\n\n return {\n user: {\n id: decodedToken.payload.userId,\n },\n installation: {\n id: decodedToken.payload.installationId,\n extensionId: decodedToken.payload.extensionId,\n },\n };\n}\n","/**\n * This is a polyfill for ed25519 support in the browser, which is currently not available as part of the\n * WebCrypto API. The polyfill code is vendored from https://www.npmjs.com/package/@yoursunny/webcrypto-ed25519,\n * and modified to use a modern and more secure version of @noble/curves.\n */\n\nimport { ed25519 as ed } from \"@noble/curves/ed25519\";\nimport { bytesToHex } from \"@noble/curves/abstract/utils\";\nimport * as asn1 from \"@root/asn1\";\n// @ts-expect-error no typing\nimport { toBase64Url as b64encode, toBuffer as b64decode } from \"b64u-lite\";\n\nexport const C = {\n wicgAlgorithm: \"Ed25519\",\n nodeAlgorithm: \"NODE-ED25519\",\n nodeNamedCurve: \"NODE-ED25519\",\n kty: \"OKP\",\n crv: \"Ed25519\",\n oid: \"2B6570\".toLowerCase(),\n};\n\nexport function isEd25519Algorithm(a: any) {\n return (\n a === C.wicgAlgorithm ||\n a === C.nodeAlgorithm ||\n a.name === C.wicgAlgorithm ||\n (a.name === C.nodeAlgorithm && a.namedCurve === C.nodeNamedCurve)\n );\n}\n\nexport const Ed25519Algorithm: KeyAlgorithm = {\n name: C.wicgAlgorithm,\n};\n\nfunction asUint8Array(b: BufferSource): Uint8Array {\n if (b instanceof Uint8Array) {\n return b;\n }\n if (b instanceof ArrayBuffer) {\n return new Uint8Array(b);\n }\n return new Uint8Array(b.buffer, b.byteOffset, b.byteLength);\n}\n\nfunction asArrayBuffer(b: Uint8Array): ArrayBuffer {\n if (b.byteLength === b.buffer.byteLength) {\n return b.buffer;\n }\n return b.buffer.slice(b.byteOffset, b.byteLength);\n}\n\nconst slot = \"8d9df0f7-1363-4d2c-8152-ce4ed78f27d8\";\n\ninterface Ed25519CryptoKey extends CryptoKey {\n [slot]: Uint8Array;\n}\n\nclass Ponyfill implements Record<keyof SubtleCrypto, Function> {\n constructor(private readonly super_: SubtleCrypto) {\n this.orig_ = {} as any;\n for (const method of [\n \"generateKey\",\n \"exportKey\",\n \"importKey\",\n \"encrypt\",\n \"decrypt\",\n \"wrapKey\",\n \"unwrapKey\",\n \"deriveBits\",\n \"deriveKey\",\n \"sign\",\n \"verify\",\n \"digest\",\n ] as const) {\n if (this[method]) {\n this.orig_[method] = super_[method];\n } else {\n this[method] = super_[method].bind(super_) as any;\n }\n }\n }\n\n private readonly orig_: Record<keyof SubtleCrypto, Function>;\n\n public async generateKey(\n algorithm: KeyAlgorithm,\n extractable: boolean,\n keyUsages: Iterable<KeyUsage>\n ): Promise<CryptoKeyPair> {\n if (isEd25519Algorithm(algorithm)) {\n const pvt = ed.utils.randomPrivateKey();\n const pub = await ed.getPublicKey(pvt);\n\n const usages = Array.from(keyUsages);\n const privateKey: Ed25519CryptoKey = {\n algorithm,\n extractable,\n type: \"private\",\n usages,\n [slot]: pvt,\n };\n const publicKey: Ed25519CryptoKey = {\n algorithm,\n extractable: true,\n type: \"public\",\n usages,\n [slot]: pub,\n };\n return { privateKey, publicKey };\n }\n return this.orig_.generateKey.apply(this.super_, arguments);\n }\n\n public async exportKey(\n format: KeyFormat,\n key: CryptoKey\n ): Promise<JsonWebKey | ArrayBuffer> {\n if (isEd25519Algorithm(key.algorithm) && key.extractable) {\n const raw = (key as Ed25519CryptoKey)[slot];\n switch (for