Skip to content

Commit

Permalink
Prepare for OAuth: Added handleUri, platform.openWindow, and papi.dat…
Browse files Browse the repository at this point in the history
…aProtection
  • Loading branch information
tjcouch-sil committed Jan 24, 2025
1 parent dda5fbd commit 18ab61c
Show file tree
Hide file tree
Showing 21 changed files with 828 additions and 17 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ sudo apt install libfuse2

Then simply [execute/run](https://github.com/AppImage/AppImageKit/wiki) the `.AppImage` file, which you can download from [Releases](https://github.com/paranext/paranext-core/releases).

Some users may find that not everything works properly in Linux without some additional setup. Please see [How to set up Platform.Bible on Linux](https://github.com/paranext/paranext/wiki/How-to-set-up-Platform.Bible-on-Linux) for more information.

### Mac Users

If you download and run the ARM release of Platform.Bible from [a computer running Apple Silicon](https://support.apple.com/en-us/116943), you will likely encounter a warning from Apple's Gatekeeper stating that "Platform.Bible is damaged and can't be opened. You should move it to the Trash." or something very similar:
Expand Down
4 changes: 4 additions & 0 deletions electron-builder.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
productName: 'Platform.Bible',
appId: 'org.paranext.PlatformBible',
copyright: 'Copyright © 2017-2025 SIL Global and United Bible Societies',
protocols: {
name: 'platform-bible',
schemes: ['platform-bible'],
},
asar: true,
asarUnpack: '**\\*.{node,dll}',
files: ['dist', 'node_modules', 'package.json'],
Expand Down
3 changes: 2 additions & 1 deletion extensions/src/hello-world/manifest.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "helloWorld",
"version": "0.0.1",
"publisher": "platform",
"displayData": "assets/displayData.json",
"author": "Paranext",
"license": "MIT",
"main": "src/main.ts",
"extensionDependencies": {},
"elevatedPrivileges": [],
"elevatedPrivileges": ["handleUri"],
"types": "src/types/hello-world.d.ts",
"menus": "contributions/menus.json",
"settings": "contributions/settings.json",
Expand Down
45 changes: 45 additions & 0 deletions extensions/src/hello-world/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,51 @@ function helloException(message: string) {
export async function activate(context: ExecutionActivationContext): Promise<void> {
logger.info('Hello world is activating!');

if (!context.elevatedPrivileges.handleUri) {
logger.warn(
'Hello World could not get handleUri. Maybe need to add handleUri in elevatedPrivileges',
);
} else {
context.registrations.add(
context.elevatedPrivileges.handleUri.registerUriHandler(async (uri) => {
const url = new URL(uri);
switch (url?.pathname) {
case '/greet':
logger.info(`Hello, ${url.searchParams.get('name')}!`);
break;
case '/greetAndOpen': {
const avatarUrl = `https://ui-avatars.com/api/?background=random&${url.searchParams}`;
logger.info(
`Hello, ${url.searchParams.get('name')}! Pulling up a generated avatar for you at ${avatarUrl}`,
);
await papi.commands.sendCommand('platform.openWindow', avatarUrl);
break;
}
default:
logger.info(`Hello World extension received a uri at an unknown path! ${uri}`);
break;
}
}),
);
logger.info(
`Hello world is listening to URIs. You can navigate to ${context.elevatedPrivileges.handleUri.redirectUri}/greet?name=your_name to say hello`,
);
}

// test data protection
try {
const data = 'Hello, world!';
const isEncryptionAvailable = await papi.dataProtection.isEncryptionAvailable();
const dataEncrypted = await papi.dataProtection.encryptString(data);
const dataDecrypted = await papi.dataProtection.decryptString(dataEncrypted);
if (!isEncryptionAvailable || data === dataEncrypted || data !== dataDecrypted)
logger.warn(
`Hello World Data Protection test failed! data = '${data}'. dataEncrypted = '${dataEncrypted}'. dataDecrypted = '${dataDecrypted}'. isEncryptionAvailable = ${isEncryptionAvailable}`,
);
} catch (e) {
logger.warn(`Hello World Data Protection test failed! ${e}`);
}

async function readRawDataForAllProjects(): Promise<string> {
try {
return await papi.storage.readUserData(context.executionToken, allProjectDataStorageKey);
Expand Down
216 changes: 216 additions & 0 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3096,6 +3096,11 @@ declare module 'papi-shared-types' {
'platform.restart': () => Promise<void>;
/** Open a browser to the platform's OpenRPC documentation */
'platform.openDeveloperDocumentationUrl': () => Promise<void>;
/**
* Open a link in a new browser window. Like `window.open` in the frontend with
* `target='_blank'`
*/
'platform.openWindow': (url: string) => Promise<void>;
/** @deprecated 3 December 2024. Renamed to `platform.openSettings` */
'platform.openProjectSettings': (webViewId: string) => Promise<void>;
/** @deprecated 3 December 2024. Renamed to `platform.openSettings` */
Expand Down Expand Up @@ -5464,20 +5469,80 @@ declare module 'shared/models/manage-extensions-privilege.model' {
getInstalledExtensions: GetInstalledExtensionsFunction;
};
}
declare module 'shared/models/handle-uri-privilege.model' {
import { Unsubscriber } from 'platform-bible-utils';
/** Function that is called when the system navigates to a URI that this handler is set up to handle. */
export type UriHandler = (uri: string) => Promise<void> | void;
/**
* Function that registers a {@link UriHandler} to be called when the system navigates to a URI that
* matches the handler's scope
*/
export type RegisterUriHandler = (uriHandler: UriHandler) => Unsubscriber;
/**
* Functions and properties related to listening for when the system navigates to a URI built for an
* extension
*/
export type HandleUri = {
/**
* Register a handler function to listen for when the system navigates to a URI built for this
* extension. Each extension can only register one uri handler at a time.
*
* Each extension has its own exclusive URI that it can handle. Extensions cannot handle each
* others' URIs. The URIs this extension's handler will receive will have the following
* structure:
*
* `<redirect-uri><additional-data>`;
*
* - `<redirect-uri>` is {@link HandleUri.redirectUri}.
* - `<additional-data>` is anything else that is on the URI as the application receives it. This
* could include path, query (aka parameters), and fragment (aka anchor).
*
* Handling URIs is useful for authentication workflows and other interactions with this extension
* from outside the application.
*
* Note: There is currently no check in place to guarantee that a call to this handler will only
* come from navigating to the uri; a process connecting over the PAPI WebSocket could fake a call
* to this handler. However, there is no expectation for this to happen.
*/
registerUriHandler: RegisterUriHandler;
/**
* The most basic URI this extension can handle with {@link HandleUri.registerUriHandler}. This
* `redirectUri` has the following structure:
*
* `<app-uri-scheme>://<extension-publisher>.<extension-name>`;
*
* - `<app-uri-scheme>` is the URI scheme this application supports. TODO: link name here
* - `<extension-publisher>` is the publisher id of this extension as specified in the extension
* manifest
* - `<extension-name>` is the name of this extension as specified in the extension manifest
*
* Additional data can be added to the end of the URI; this is just the scheme and authority. See
* {@link HandleUri.registerUriHandler} for more information.
*/
redirectUri: string;
};
}
declare module 'shared/models/elevated-privileges.model' {
import { CreateProcess } from 'shared/models/create-process-privilege.model';
import { ManageExtensions } from 'shared/models/manage-extensions-privilege.model';
import { HandleUri } from 'shared/models/handle-uri-privilege.model';
/** String constants that are listed in an extension's manifest.json to state needed privileges */
export enum ElevatedPrivilegeNames {
createProcess = 'createProcess',
manageExtensions = 'manageExtensions',
handleUri = 'handleUri',
}
/** Object that contains properties with special capabilities for extensions that required them */
export type ElevatedPrivileges = {
/** Functions that can be run to start new processes */
createProcess: CreateProcess | undefined;
/** Functions that can be run to manage what extensions are running */
manageExtensions: ManageExtensions | undefined;
/**
* Functions and properties related to listening for when the system navigates to a URI built for
* this extension
*/
handleUri: HandleUri | undefined;
};
}
declare module 'extension-host/extension-types/extension-activation-context.model' {
Expand Down Expand Up @@ -5785,6 +5850,23 @@ declare module 'shared/data/platform.data' {
* Platform.Bible core
*/
export const PLATFORM_NAMESPACE = 'platform';
/**
* Name of this application like `platform-bible`.
*
* Note: this is an identifier for the application, not this application's executable file name
*/
export const APP_NAME: string;
/** Version of this application in [semver](https://semver.org/) format. */
export const APP_VERSION: string;
/**
* URI scheme that this application handles. Navigating to a URI with this scheme will open this
* application. This application will handle the URI as it sees fit. For example, the URI may be
* handled by an extension - see {@link ElevatedPrivileges.registerUriHandler } for more
* information.
*
* This is the same as {@link APP_NAME}.
*/
export const APP_URI_SCHEME: string;
/** Query string passed to the renderer when starting if it should enable noisy dev mode */
export const DEV_MODE_RENDERER_INDICATOR = '?noisyDevMode';
}
Expand Down Expand Up @@ -6214,6 +6296,11 @@ declare module '@papi/core' {
InstalledExtensions,
ManageExtensions,
} from 'shared/models/manage-extensions-privilege.model';
export type {
HandleUri,
RegisterUriHandler,
UriHandler,
} from 'shared/models/handle-uri-privilege.model';
export type { DialogTypes } from 'renderer/components/dialogs/dialog-definition.model';
export type { UseDialogCallbackOptions } from 'renderer/hooks/papi-hooks/use-dialog-callback.hook';
export type {
Expand Down Expand Up @@ -6441,6 +6528,103 @@ declare module 'shared/services/project-settings.service' {
const projectSettingsService: IProjectSettingsService;
export default projectSettingsService;
}
declare module 'shared/models/data-protection.service-model' {
/**
*
* Provides functions related to encrypting and decrypting strings like user data, secrets, etc.
*
* Uses Electron's [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) API.
*
* Note that these encryption mechanisms are not transferrable between computers. We recommend using
* them with `papi.storage` methods to store data safely.
*
* WARNING: The primary purpose of this service is to enable extensions to encrypt and decrypt data
* to be stored securely in local files. It is not intended to protect data passed over a network
* connection. Please note that using this service passes the unencrypted string between local
* processes using the PAPI WebSocket.
*/
export interface IDataProtectionService {
/**
* Encrypts a string using Electron's
* [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) API. Transforms the
* returned buffer to a base64-encoded string using
* [`buffer.toString('base64')`](https://nodejs.org/api/buffer.html#buftostringencoding-start-end).
*
* This method throws if the encryption mechanism is not available such as on Linux without a
* supported package installed. See
* [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) for more information.
*
* Note that this encryption mechanism is not transferrable between computers. We recommend using
* it with `papi.storage` methods to store data safely.
*
* WARNING: The primary purpose of this service is to enable extensions to encrypt and decrypt
* data to be stored securely in local files. It is not intended to protect data passed over a
* network connection. Please note that using this service passes the unencrypted string between
* local processes using the PAPI WebSocket.
*
* @param text String to encrypt
* @returns Encrypted string. Use `papi.dataProtection.decryptString` to decrypt
*/
encryptString(text: string): Promise<string>;
/**
* Decrypts a string using Electron's
* [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) API. Transforms the
* input base64-encoded string to a buffer using [`Buffer.from(text,
* 'base64')`](https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding).
*
* This method throws if the decryption mechanism is not available such as on Linux without a
* supported package installed. See
* [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) for more information.
*
* Note that this encryption mechanism is not transferrable between computers. We recommend using
* it with `papi.storage` methods to store data safely.
*
* WARNING: The primary purpose of this service is to enable extensions to encrypt and decrypt
* data to be stored securely in local files. It is not intended to protect data passed over a
* network connection. Please note that using this service passes the unencrypted string between
* local processes using the PAPI WebSocket.
*
* @param encryptedText String to decrypt. This string should have been encrypted by
* `papi.dataProtection.encryptString`
* @returns Decrypted string
*/
decryptString(encryptedText: string): Promise<string>;
/**
* Returns `true` if encryption is currently available. Returns `false` if the decryption
* mechanism is not available such as on Linux without a supported package installed. See
* Electron's [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) API for
* more information.
*
* WARNING: The primary purpose of this service is to enable extensions to encrypt and decrypt
* data to be stored securely in local files. It is not intended to protect data passed over a
* network connection. Please note that using this service passes the unencrypted string between
* local processes using the PAPI WebSocket.
*
* @returns `true` if encryption is currently available; `false` otherwise
*/
isEncryptionAvailable(): Promise<boolean>;
}
export const dataProtectionServiceNetworkObjectName = 'DataProtectionService';
}
declare module 'shared/services/data-protection.service' {
import { IDataProtectionService } from 'shared/models/data-protection.service-model';
/**
*
* Provides functions related to encrypting and decrypting strings like user data, secrets, etc.
*
* Uses Electron's [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) API.
*
* Note that these encryption mechanisms are not transferrable between computers. We recommend using
* them with `papi.storage` methods to store data safely.
*
* WARNING: The primary purpose of this service is to enable extensions to encrypt and decrypt data
* to be stored securely in local files. It is not intended to protect data passed over a network
* connection. Please note that using this service passes the unencrypted string between local
* processes using the PAPI WebSocket.
*/
const dataProtectionService: IDataProtectionService;
export default dataProtectionService;
}
declare module '@papi/backend' {
/**
* Unified module for accessing API features in the extension host.
Expand Down Expand Up @@ -6554,6 +6738,21 @@ declare module '@papi/backend' {
* other services and extensions that have registered commands.
*/
commands: typeof commandService;
/**
*
* Provides functions related to encrypting and decrypting strings like user data, secrets, etc.
*
* Uses Electron's [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) API.
*
* Note that these encryption mechanisms are not transferrable between computers. We recommend using
* them with `papi.storage` methods to store data safely.
*
* WARNING: The primary purpose of this service is to enable extensions to encrypt and decrypt data
* to be stored securely in local files. It is not intended to protect data passed over a network
* connection. Please note that using this service passes the unencrypted string between local
* processes using the PAPI WebSocket.
*/
dataProtection: import('shared/models/data-protection.service-model').IDataProtectionService;
/**
*
* Service exposing various functions related to using webViews
Expand Down Expand Up @@ -6758,6 +6957,21 @@ declare module '@papi/backend' {
* other services and extensions that have registered commands.
*/
export const commands: typeof commandService;
/**
*
* Provides functions related to encrypting and decrypting strings like user data, secrets, etc.
*
* Uses Electron's [`safeStorage`](https://www.electronjs.org/docs/latest/api/safe-storage) API.
*
* Note that these encryption mechanisms are not transferrable between computers. We recommend using
* them with `papi.storage` methods to store data safely.
*
* WARNING: The primary purpose of this service is to enable extensions to encrypt and decrypt data
* to be stored securely in local files. It is not intended to protect data passed over a network
* connection. Please note that using this service passes the unencrypted string between local
* processes using the PAPI WebSocket.
*/
export const dataProtection: import('shared/models/data-protection.service-model').IDataProtectionService;
/**
*
* Service exposing various functions related to using webViews
Expand Down Expand Up @@ -6947,6 +7161,8 @@ declare module 'extension-host/extension-types/extension-manifest.model' {
* implemented.
*/
activationEvents: string[];
/** Id of publisher who published this extension on the extension marketplace */
publisher?: string;
};
}
declare module 'renderer/hooks/hook-generators/create-use-network-object-hook.util' {
Expand Down
4 changes: 2 additions & 2 deletions release/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion release/app/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "platform.bible",
"name": "platform-bible",
"version": "0.3.0",
"description": "Extensible Bible translation software",
"license": "MIT",
Expand Down
Loading

0 comments on commit 18ab61c

Please sign in to comment.