Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions client/src/client_builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Core, InnerClient } from "./core.js";
import { Core, InnerClient, SharedCore } from "./core.js";
import { ClientConfiguration, clientAuthConfig } from "./configuration.js";
import { Client } from "./client.js";
import { SharedLibCore } from "./shared_lib_core.js";

const finalizationRegistry = new FinalizationRegistry(
(heldClient: InnerClient) => {
Expand All @@ -14,9 +15,12 @@ const finalizationRegistry = new FinalizationRegistry(
*/
export const createClientWithCore = async (
config: ClientConfiguration,
core: Core,
core: SharedCore,
): Promise<Client> => {
const authConfig = clientAuthConfig(config);
if (authConfig.accountName) {
core.setInner(new SharedLibCore(authConfig.accountName));
}
const clientId = await core.initClient(authConfig);
const inner: InnerClient = {
id: parseInt(clientId, 10),
Expand Down
24 changes: 22 additions & 2 deletions client/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ export interface ClientConfiguration {
}

// Sets the authentication method. Use a token as a `string` to authenticate with a service account token.
type Auth = string;
type Auth = string | DesktopAuth;

/**
* Setting that specifies that a client should use the desktop app to authenticate.
*/
export class DesktopAuth {
public accountName: string;

public constructor(accountName: string) {
this.accountName = accountName;
}
}
/**
* Creates a default client configuration.
* @returns The client configuration to instantiate the client with.
Expand All @@ -25,8 +35,18 @@ export const clientAuthConfig = (
): ClientAuthConfig => {
// TODO: Add logic for computing the correct sanitized version value for each platform
const defaultOsVersion = "0.0.0";

let serviceAccountToken: string | undefined;
let accountName: string | undefined;

if (typeof userConfig.auth === "string") {
serviceAccountToken = userConfig.auth;
} else if (userConfig.auth instanceof DesktopAuth) {
accountName = userConfig.auth.accountName;
}
return {
serviceAccountToken: userConfig.auth ?? "",
serviceAccountToken: serviceAccountToken ?? "",
accountName,
programmingLanguage: LANGUAGE,
sdkVersion: VERSION,
integrationName: userConfig.integrationName,
Expand Down
60 changes: 47 additions & 13 deletions client/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,21 @@ export interface Core {
/**
* Allocates a new authenticated client and returns its id.
*/
initClient(config: ClientAuthConfig): Promise<string>;
initClient(config: string): Promise<string>;
/**
* Calls async business logic from a given client and returns the result.
*/
invoke(config: InvokeConfig): Promise<string>;
/**
* Calls sync business logic from a given client and returns the result.
*/
invoke_sync(config: InvokeConfig): string;
invoke(config: string): Promise<string>;
/**
* Deallocates memory held by the given client in the SDK core when it goes out of scope.
*/
releaseClient(clientId: number): void;
releaseClient(clientId: string): void;
}

/**
* Wraps configuration information needed to allocate and authenticate a client instance and sends it to the SDK core.
*/
export interface ClientAuthConfig {
serviceAccountToken: string;
programmingLanguage: string;
sdkVersion: string;
integrationName: string;
Expand All @@ -49,6 +44,9 @@ export interface ClientAuthConfig {
os: string;
osVersion: string;
architecture: string;

serviceAccountToken?: string; // only used when service account token auth is selected
accountName?: string; // only used when desktop auth is selected
}

/**
Expand Down Expand Up @@ -83,14 +81,50 @@ export interface Parameters {
parameters: { [key: string]: unknown };
}

export class WasmCore implements Core {
public async initClient(config: string): Promise<string> {
try {
return await init_client(config);
} catch (e) {
throwError(e as string);
}
}

public async invoke(config: string): Promise<string> {
try {
return await invoke(config);
} catch (e) {
throwError(e as string);
}
}

public releaseClient(clientId: string): void {
try {
release_client(clientId);
} catch (e) {
console.warn("failed to release client:", e);
}
}
}

/**
* An implementation of the `Core` interface that shares resources across all clients.
*/
export class SharedCore implements Core {
export class SharedCore {
private inner: Core;

public constructor() {
this.inner = new WasmCore();
}

public setInner(core: Core) {
this.inner = core;
}

public async initClient(config: ClientAuthConfig): Promise<string> {
const serializedConfig = JSON.stringify(config);
try {
return await init_client(serializedConfig);
return await this.inner.initClient(serializedConfig);
} catch (e) {
throwError(e as string);
}
Expand All @@ -106,7 +140,7 @@ export class SharedCore implements Core {
);
}
try {
return await invoke(serializedConfig);
return await this.inner.invoke(serializedConfig);
} catch (e) {
throwError(e as string);
}
Expand All @@ -130,7 +164,7 @@ export class SharedCore implements Core {

public releaseClient(clientId: number): void {
const serializedId = JSON.stringify(clientId);
release_client(serializedId);
this.inner.releaseClient(serializedId);
}
}

Expand All @@ -139,5 +173,5 @@ export class SharedCore implements Core {
*/
export interface InnerClient {
id: number;
core: Core;
core: SharedCore;
}
1 change: 1 addition & 0 deletions client/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { Secrets } from "./secrets.js";
export * from "./client.js";
export * from "./errors.js";
export * from "./types.js";
export { DesktopAuth } from "./configuration.js";

/**
* Creates a default 1Password SDK client.
Expand Down
187 changes: 187 additions & 0 deletions client/src/shared_lib_core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

import { Core } from "./core";

/**
* Find the 1Password shared lib path by asking an the wasm core synchronously.
*/
const find1PasswordLibPath = (): string => {
const platform: NodeJS.Platform = os.platform();
const appRoot: string = path.dirname(process.execPath);
let searchPaths: string[] = [];

// Define lists of possible locations for each platform.
switch (platform) {
case "darwin": // macOS
searchPaths = [
"/Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib",
path.join(
os.homedir(),
"/Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib",
),
];
break;

case "win32": // Windows
searchPaths = [
"C:/Program Files/1Password/op_sdk_ipc_client.dll",
"C:/Program Files (x86)/1Password/op_sdk_ipc_client.dll",
path.join(
os.homedir(),
"/AppData/Local/1Password/op_sdk_ipc_client.dll",
),
];
break;

case "linux": // Linux
searchPaths = [
"/usr/bin/1password/libop_sdk_ipc_client.so",
"/opt/1password/libop_sdk_ipc_client.so",
"/snap/bin/1password/libop_sdk_ipc_client.so",
];
break;

default:
throw new Error(`Unsupported platform: ${platform}`);
}

// Iterate through the possible paths and return the first one that exists.
for (const addonPath of searchPaths) {
if (fs.existsSync(addonPath)) {
return addonPath;
}
}

// If the loop completes without finding the file, throw an error.
throw new Error("1Password desktop application not found");
};

interface DesktopIPCClient {
sendMessage(msg: Buffer): Promise<Uint8Array>;
}

type SharedLibRequest = {
kind: string;
account_name: string;
payload: string;
};

interface SharedLibResponse {
success: boolean;
payload: number[];
}

/**
* SharedLibCore: wrapper around the dynamically loaded shared library
*/
export class SharedLibCore implements Core {
private lib: DesktopIPCClient | null = null;
private acccountName: string;

public constructor(accountName: string) {
try {
const libPath = find1PasswordLibPath();
const moduleStub = { exports: {} };
process.dlopen(moduleStub, libPath);

// Safely check the structure of the loaded module before casting.
if (
typeof moduleStub === "object" &&
moduleStub !== null &&
typeof moduleStub.exports === "object" &&
moduleStub.exports !== null &&
"sendMessage" in moduleStub.exports &&
typeof (moduleStub.exports as { sendMessage: unknown }).sendMessage ===
"function"
) {
this.lib = moduleStub.exports as DesktopIPCClient;
} else {
throw new Error(
"Failed to initialize native library: sendMessage function not found on module.",
);
}
} catch (e) {
console.error(
"A critical error occurred while loading the native addon:",
e,
);
this.lib = null;
}

this.acccountName = accountName;
}

/**
* callSharedLibrary - send string to native function, receive string back.
*/
private async callSharedLibrary(
input: string,
operation_type: string,
): Promise<string> {
if (!this.lib) {
throw new Error("Native library is not available.");
}

if (!input || input.length === 0) {
throw new Error("internal: empty input");
}

const inputEncoded = Buffer.from(input, "utf8").toString("base64");

const req: SharedLibRequest = {
account_name: this.acccountName,
kind: operation_type,
payload: inputEncoded,
};

const inputBuf = Buffer.from(JSON.stringify(req), "utf8");

try {
const nativeResponse = await this.lib.sendMessage(inputBuf);

if (!(nativeResponse instanceof Uint8Array)) {
throw new Error(
`Native function returned an unexpected type. Expected Uint8Array, got ${typeof nativeResponse}`,
);
}

const respString = new TextDecoder().decode(nativeResponse);
const response = JSON.parse(respString) as SharedLibResponse;

if (response.success) {
const decodedPayload = Buffer.from(response.payload).toString("utf8");
// On success, the payload is the actual result string
return decodedPayload;
} else {
// On failure, convert the error payload to a readable string and throw
const errorMessage = Array.isArray(response.payload)
? String.fromCharCode(...response.payload)
: JSON.stringify(response.payload);

throw new Error(`Native library returned an error: ${errorMessage}`);
}
} catch (e) {
// Catch errors from the native call or from JSON parsing
console.error("An error occurred during the native library call:", e);
throw e;
}
}

// Core interface implementation

public async initClient(config: string): Promise<string> {
return this.callSharedLibrary(config, "init_client");
}

public async invoke(invokeConfigBytes: string): Promise<string> {
return this.callSharedLibrary(invokeConfigBytes, "invoke");
}

public releaseClient(clientId: string): void {
this.callSharedLibrary(clientId, "release_client").catch((err) => {
console.warn("failed to release client:", err);
});
}
}
Loading