@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 +21 -0
- package/README.md +39 -2
- package/dist/core.d.ts +19 -0
- package/dist/core.js +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react-native.d.ts +55 -0
- package/dist/react-native.js +74 -0
- package/dist/web.d.ts +24 -0
- package/dist/web.js +13 -0
- package/package.json +52 -3
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
|
-
#
|
|
1
|
+
# @synqauth/solana-mobile
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|