@synqauth/solana-mobile 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aerosol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # @synqauth/solana-mobile
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sign in to Synq with a Solana wallet app on Android, through Mobile Wallet Adapter.
4
+
5
+ Synq is customer identity for businesses: an OpenID Connect issuer for each of your brands, with Solana wallet, social, email and passkey sign-in. The documentation is at [docs.synq.build](https://docs.synq.build).
6
+
7
+ This package is at 0.x and hasn't been published yet: its API can change in any release until Synq's first customer depends on it.
8
+
9
+ ## Usage
10
+
11
+ In a React Native app on Android, with Mobile Wallet Adapter's own `transact`:
12
+
13
+ ```ts
14
+ import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol'
15
+ import { mobileWalletSignInUrl } from '@synqauth/solana-mobile/react-native'
16
+
17
+ const url = await mobileWalletSignInUrl(synq.client, {
18
+ issuer,
19
+ clientId,
20
+ appOrigin: 'https://app.example.com', // your registered redirect URI's origin
21
+ identity: { name: 'Acme', uri: 'https://app.example.com' },
22
+ transact,
23
+ })
24
+ // open `url` in the system browser; it comes straight back with a code
25
+ ```
26
+
27
+ In a mobile browser, register Mobile Wallet Adapter as a wallet, and `@synqauth/solana` finds it:
28
+
29
+ ```ts
30
+ import { registerMwa } from '@solana-mobile/wallet-standard-mobile'
31
+ import { registerMobileWallet } from '@synqauth/solana-mobile/web'
32
+
33
+ registerMobileWallet({ registerMwa, appIdentity: { name: 'Acme', uri: location.origin } })
34
+ ```
35
+
36
+ The wallet app signs your brand's issuer's own request, and Synq checks it.
37
+
38
+ ## License
39
+
40
+ MIT
package/dist/core.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { SignInRequest } from "@synqauth/solana";
2
+ //#region src/core.d.ts
3
+ /** What MWA's `authorize` answers for a sign-in: every field base64. */
4
+ export interface MwaSignInResult {
5
+ readonly address: string;
6
+ readonly signed_message: string;
7
+ readonly signature: string;
8
+ readonly signature_type?: string;
9
+ }
10
+ /** A Solana address (32 bytes) in base58, as the issuer names wallets. */
11
+ export declare function base58Address(bytes: Uint8Array): string;
12
+ /**
13
+ * `synq_siws` for MWA's sign-in result for `request`: the address in
14
+ * base58, the signature as it came, and the request's nonce. Refuses a
15
+ * result whose signed message isn't the issuer's request for that address,
16
+ * or that isn't an Ed25519 signature.
17
+ */
18
+ export declare function siwsFromMwa(request: SignInRequest, result: MwaSignInResult): string;
19
+ //#endregion
package/dist/core.js ADDED
@@ -0,0 +1,52 @@
1
+ import { SynqError } from "@synqauth/js";
2
+ import { encodeSiws, signInMessage } from "@synqauth/solana";
3
+ //#region src/core.ts
4
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
5
+ function fromBase64(value) {
6
+ const binary = atob(value);
7
+ const bytes = new Uint8Array(binary.length);
8
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
9
+ return bytes;
10
+ }
11
+ /** A Solana address (32 bytes) in base58, as the issuer names wallets. */
12
+ function base58Address(bytes) {
13
+ if (bytes.length !== 32) throw new SynqError("invalid_response", "The wallet answered an address that is not 32 bytes");
14
+ let n = 0n;
15
+ for (const byte of bytes) n = n * 256n + BigInt(byte);
16
+ let out = "";
17
+ while (n > 0n) {
18
+ out = ALPHABET[Number(n % 58n)] + out;
19
+ n /= 58n;
20
+ }
21
+ for (const byte of bytes) {
22
+ if (byte !== 0) break;
23
+ out = `1${out}`;
24
+ }
25
+ return out;
26
+ }
27
+ /**
28
+ * `synq_siws` for MWA's sign-in result for `request`: the address in
29
+ * base58, the signature as it came, and the request's nonce. Refuses a
30
+ * result whose signed message isn't the issuer's request for that address,
31
+ * or that isn't an Ed25519 signature.
32
+ */
33
+ function siwsFromMwa(request, result) {
34
+ let address;
35
+ let message;
36
+ try {
37
+ address = base58Address(fromBase64(result.address));
38
+ message = new TextDecoder().decode(fromBase64(result.signed_message));
39
+ } catch (error) {
40
+ if (error instanceof SynqError) throw error;
41
+ throw new SynqError("invalid_response", "The wallet's sign-in result isn't base64", { cause: error });
42
+ }
43
+ if (result.signature_type && result.signature_type.toLowerCase() !== "ed25519") throw new SynqError("invalid_response", `The wallet signed with ${result.signature_type}, not Ed25519`);
44
+ if (message !== signInMessage(request, address)) throw new SynqError("invalid_response", "The wallet signed another message than the request makes");
45
+ return encodeSiws({
46
+ address,
47
+ signature: result.signature,
48
+ nonce: request.nonce
49
+ });
50
+ }
51
+ //#endregion
52
+ export { base58Address, siwsFromMwa };
@@ -0,0 +1,2 @@
1
+ import { MwaSignInResult, base58Address, siwsFromMwa } from "./core.js";
2
+ export { type MwaSignInResult, base58Address, siwsFromMwa };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { base58Address, siwsFromMwa } from "./core.js";
2
+ export { base58Address, siwsFromMwa };
@@ -0,0 +1,55 @@
1
+ import { MwaSignInResult, siwsFromMwa } from "./core.js";
2
+ import { AuthorizationUrlOptions, SynqClient } from "@synqauth/js";
3
+ import { SignInRequest } from "@synqauth/solana";
4
+ //#region src/react-native.d.ts
5
+ /** The part of an MWA wallet session this uses. */
6
+ export interface MwaWallet {
7
+ authorize(params: {
8
+ identity: {
9
+ name?: string;
10
+ uri?: string;
11
+ icon?: string;
12
+ };
13
+ chain?: string;
14
+ sign_in_payload?: SignInRequest;
15
+ }): Promise<{
16
+ sign_in_result?: MwaSignInResult;
17
+ }>;
18
+ }
19
+ /** MWA's `transact`: runs `callback` in a session with the wallet app the person picks. */
20
+ export type MwaTransact = <T>(callback: (wallet: MwaWallet) => Promise<T>) => Promise<T>;
21
+ /** What `mobileWalletSignInUrl` takes besides the client. */
22
+ export interface MobileWalletSignInOptions extends Omit<AuthorizationUrlOptions, 'pushed' | 'prompt' | 'method'> {
23
+ issuer: string | URL;
24
+ clientId: string;
25
+ /**
26
+ * The app's https identity: the origin of a redirect URI it registered
27
+ * and claims (an App Link through `assetlinks.json`, a universal link
28
+ * through `apple-app-site-association`). The issuer answers a wallet
29
+ * sign-in request only to one of the app's own origins, which a native
30
+ * app's requests carry as `Origin`. An app whose only redirect URI is a
31
+ * scheme of its own (`com.example.app:/callback`) can sign people in
32
+ * through the browser but not with a wallet: this is `invalid_options`.
33
+ */
34
+ appOrigin: string;
35
+ /** How the wallet app names the app asking: `uri`, when given, is https on `appOrigin`. */
36
+ identity: {
37
+ name?: string;
38
+ uri?: string;
39
+ icon?: string;
40
+ };
41
+ transact: MwaTransact;
42
+ fetch?: typeof fetch;
43
+ }
44
+ /**
45
+ * The issuer's request, signed by the wallet app through MWA, pushed to
46
+ * Synq: the URL to open in the system browser, which comes straight back to
47
+ * the app's redirect URI with a code. A person who declines in the wallet
48
+ * app is `wallet_rejected`. The app needs an https identity it claims as
49
+ * `appOrigin` (an App Link, a universal link): one whose only redirect URI is
50
+ * a scheme of its own signs in through the browser only, and is
51
+ * `invalid_options` here, before anyone is asked.
52
+ */
53
+ export declare function mobileWalletSignInUrl(client: SynqClient, options: MobileWalletSignInOptions): Promise<URL>;
54
+ //#endregion
55
+ export { type MwaSignInResult, siwsFromMwa };
@@ -0,0 +1,74 @@
1
+ import { siwsFromMwa } from "./core.js";
2
+ import { SynqError } from "@synqauth/js";
3
+ import { requestSignIn } from "@synqauth/solana";
4
+ //#region src/react-native.ts
5
+ /**
6
+ * The app's https origin, from `appOrigin`, or `invalid_options`. Wallet
7
+ * sign-in needs an https identity the app claims (an App Link through
8
+ * `assetlinks.json`, a universal link through `apple-app-site-association`):
9
+ * MWA wallets show the dapp's identity URI and check it against the app,
10
+ * the Sign In With Solana message names its domain, and the issuer answers
11
+ * only an origin of one of the app's redirect URIs. A scheme of the app's
12
+ * own (`com.example.app:/callback`) proves nothing about who owns the app:
13
+ * it is enough for signing in through the browser, not with a wallet.
14
+ */
15
+ function httpsIdentity(appOrigin, identityUri) {
16
+ const url = URL.canParse(appOrigin) ? new URL(appOrigin) : void 0;
17
+ if (url?.protocol !== "https:") throw new SynqError("invalid_options", `Signing in with a wallet app needs the app's https identity as appOrigin, the origin of an https redirect URI it claims (an App Link or universal link), not ${appOrigin}: a scheme of the app's own is for signing in through the browser only`);
18
+ if (identityUri !== void 0) {
19
+ const identity = URL.canParse(identityUri) ? new URL(identityUri) : void 0;
20
+ if (identity?.protocol !== "https:" || identity.origin !== url.origin) throw new SynqError("invalid_options", `identity.uri must be an https URL on the app's identity, ${url.origin}, not ${identityUri}`);
21
+ }
22
+ return url.origin;
23
+ }
24
+ /**
25
+ * The issuer's request, signed by the wallet app through MWA, pushed to
26
+ * Synq: the URL to open in the system browser, which comes straight back to
27
+ * the app's redirect URI with a code. A person who declines in the wallet
28
+ * app is `wallet_rejected`. The app needs an https identity it claims as
29
+ * `appOrigin` (an App Link, a universal link): one whose only redirect URI is
30
+ * a scheme of its own signs in through the browser only, and is
31
+ * `invalid_options` here, before anyone is asked.
32
+ */
33
+ async function mobileWalletSignInUrl(client, options) {
34
+ const { issuer, clientId, appOrigin, identity, transact, fetch: fetcher, params, ...rest } = options;
35
+ const origin = httpsIdentity(appOrigin, identity.uri);
36
+ const base = fetcher ?? fetch;
37
+ const withOrigin = (input, init) => {
38
+ const headers = new Headers(init?.headers);
39
+ headers.set("origin", origin);
40
+ return base(input, {
41
+ ...init,
42
+ headers
43
+ });
44
+ };
45
+ const request = await requestSignIn({
46
+ issuer,
47
+ clientId,
48
+ fetch: withOrigin
49
+ });
50
+ let result;
51
+ try {
52
+ result = await transact(async (wallet) => {
53
+ return (await wallet.authorize({
54
+ identity,
55
+ chain: request.chainId,
56
+ sign_in_payload: request
57
+ })).sign_in_result;
58
+ });
59
+ } catch (error) {
60
+ throw new SynqError("wallet_rejected", `The wallet app didn't sign: ${String(error)}`, { cause: error });
61
+ }
62
+ if (!result) throw new SynqError("wallet_rejected", "The wallet app answered no sign-in");
63
+ return client.authorizationUrl({
64
+ ...rest,
65
+ prompt: "login",
66
+ pushed: true,
67
+ params: {
68
+ ...params,
69
+ synq_siws: siwsFromMwa(request, result)
70
+ }
71
+ });
72
+ }
73
+ //#endregion
74
+ export { mobileWalletSignInUrl, siwsFromMwa };
package/dist/web.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ //#region src/web.d.ts
2
+ /** The options `registerMwa` takes that this sets; the rest pass through. */
3
+ export interface RegisterMwaOptions {
4
+ appIdentity: {
5
+ name?: string;
6
+ uri?: string;
7
+ icon?: string;
8
+ };
9
+ chains: readonly string[];
10
+ [option: string]: unknown;
11
+ }
12
+ /** What `registerMobileWallet` takes. */
13
+ export interface RegisterMobileWalletOptions {
14
+ /** `registerMwa` from @solana-mobile/wallet-standard-mobile. */
15
+ registerMwa: (options: RegisterMwaOptions) => void;
16
+ appIdentity: RegisterMwaOptions['appIdentity'];
17
+ /** The chains it signs for: Synq's issuers name their stage's (`solana:devnet` on preview, `solana:mainnet` in production). */
18
+ chains?: readonly string[];
19
+ /** Anything else `registerMwa` takes, such as `authorizationCache` or `onWalletNotFound`. */
20
+ options?: Record<string, unknown>;
21
+ }
22
+ /** Registers Mobile Wallet Adapter as a wallet the page's discovery finds; does nothing off a browser. */
23
+ export declare function registerMobileWallet(options: RegisterMobileWalletOptions): boolean;
24
+ //#endregion
package/dist/web.js ADDED
@@ -0,0 +1,13 @@
1
+ //#region src/web.ts
2
+ /** Registers Mobile Wallet Adapter as a wallet the page's discovery finds; does nothing off a browser. */
3
+ function registerMobileWallet(options) {
4
+ if (typeof globalThis.window !== "object") return false;
5
+ options.registerMwa({
6
+ ...options.options,
7
+ appIdentity: options.appIdentity,
8
+ chains: options.chains ?? ["solana:mainnet", "solana:devnet"]
9
+ });
10
+ return true;
11
+ }
12
+ //#endregion
13
+ export { registerMobileWallet };
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@synqauth/solana-mobile",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "Sign in to Synq with a Solana wallet app on Android, through Mobile Wallet Adapter.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/aerosol-build/synq.git",
9
+ "directory": "packages/solana-mobile"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "engines": {
14
+ "node": ">=22"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./core": {
22
+ "types": "./dist/core.d.ts",
23
+ "default": "./dist/core.js"
24
+ },
25
+ "./react-native": {
26
+ "types": "./dist/react-native.d.ts",
27
+ "default": "./dist/react-native.js"
28
+ },
29
+ "./web": {
30
+ "types": "./dist/web.d.ts",
31
+ "default": "./dist/web.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "@synqauth/js": "0.0.0",
43
+ "@synqauth/solana": "0.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@vitest/coverage-v8": "5.0.2",
47
+ "vitest": "5.0.2"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "lint": "eslint .",
52
+ "typecheck": "tsc",
53
+ "test": "vitest run --coverage"
54
+ }
6
55
  }