@fogo/sessions-sdk 0.0.11 → 0.0.13

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/esm/index.js CHANGED
@@ -1,28 +1,32 @@
1
1
  import { AnchorProvider, BorshAccountsCoder } from "@coral-xyz/anchor";
2
- import { DomainRegistryIdl, SessionManagerProgram, SessionManagerIdl, IntentTransferProgram, } from "@fogo/sessions-idls";
2
+ import { DomainRegistryIdl, IntentTransferProgram, SessionManagerIdl, SessionManagerProgram, } from "@fogo/sessions-idls";
3
3
  import { findMetadataPda, safeFetchMetadata, } from "@metaplex-foundation/mpl-token-metadata";
4
4
  import { publicKey as metaplexPublicKey } from "@metaplex-foundation/umi";
5
5
  import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
6
6
  import { sha256 } from "@noble/hashes/sha2";
7
7
  import { fromLegacyPublicKey } from "@solana/compat";
8
- import { generateKeyPair, getAddressFromPublicKey, getProgramDerivedAddress, } from "@solana/kit";
8
+ import { generateKeyPair, getAddressFromPublicKey, getProgramDerivedAddress, verifySignature, } from "@solana/kit";
9
9
  import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, getMint, } from "@solana/spl-token";
10
10
  import { Ed25519Program, PublicKey } from "@solana/web3.js";
11
11
  import BN from "bn.js";
12
+ import bs58 from "bs58";
12
13
  import { z } from "zod";
13
14
  import { TransactionResultType } from "./adapter.js";
14
- export { TransactionResultType, createSolanaWalletAdapter, } from "./adapter.js";
15
+ import { importKey, signMessageWithKey, verifyMessageWithKey, } from "./crypto.js";
16
+ export { createSolanaWalletAdapter, TransactionResultType, } from "./adapter.js";
15
17
  const MESSAGE_HEADER = `Fogo Sessions:
16
18
  Signing this intent will allow this app to interact with your on-chain balances. Please make sure you trust this app and the domain in the message matches the domain of the current web application.
17
19
  `;
18
20
  const UNLIMITED_TOKEN_PERMISSIONS_VALUE = "this app may spend any amount of any token";
19
21
  const TOKENLESS_PERMISSIONS_VALUE = "this app may not spend any tokens";
20
22
  const CURRENT_MAJOR = "0";
21
- const CURRENT_MINOR = "1";
23
+ const CURRENT_MINOR = "2";
22
24
  const CURRENT_INTENT_TRANSFER_MAJOR = "0";
23
25
  const CURRENT_INTENT_TRANSFER_MINOR = "1";
24
26
  export const establishSession = async (options) => {
25
- const sessionKey = await generateKeyPair();
27
+ const sessionKey = options.createUnsafeExtractableSessionKey
28
+ ? await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"])
29
+ : await generateKeyPair();
26
30
  if (options.unlimited) {
27
31
  return sendSessionEstablishTransaction(options, sessionKey, await Promise.all([
28
32
  buildIntentInstruction(options, sessionKey),
@@ -63,6 +67,23 @@ export const replaceSession = async (options) => establishSession({
63
67
  ...options,
64
68
  walletPublicKey: options.session.walletPublicKey,
65
69
  });
70
+ export const revokeSession = async (options) => {
71
+ if (options.session.sessionInfo.minor >= 2) {
72
+ const instruction = await new SessionManagerProgram(new AnchorProvider(options.adapter.connection, {}, {})).methods
73
+ .revokeSession()
74
+ .accounts({
75
+ sponsor: options.session.sessionInfo.sponsor,
76
+ session: options.session.sessionPublicKey,
77
+ })
78
+ .instruction();
79
+ return options.adapter.sendTransaction(options.session.sessionKey, [
80
+ instruction,
81
+ ]);
82
+ }
83
+ else {
84
+ return;
85
+ }
86
+ };
66
87
  export const reestablishSession = async (adapter, walletPublicKey, sessionKey) => createSession(adapter, walletPublicKey, sessionKey);
67
88
  export const getSessionAccount = async (connection, sessionPublicKey) => {
68
89
  const result = await connection.getAccountInfo(sessionPublicKey);
@@ -86,49 +107,108 @@ const createSession = async (adapter, walletPublicKey, sessionKey) => {
86
107
  };
87
108
  const sessionInfoSchema = z
88
109
  .object({
89
- session_info: z.object({
90
- authorized_programs: z.union([
91
- z.object({
92
- Specific: z.object({
93
- 0: z.array(z.object({
94
- program_id: z.instanceof(PublicKey),
95
- signer_pda: z.instanceof(PublicKey),
96
- })),
110
+ session_info: z.union([
111
+ z.object({
112
+ V1: z.object({
113
+ "0": z.object({
114
+ authorized_programs: z.union([
115
+ z.object({
116
+ Specific: z.object({
117
+ 0: z.array(z.object({
118
+ program_id: z.instanceof(PublicKey),
119
+ signer_pda: z.instanceof(PublicKey),
120
+ })),
121
+ }),
122
+ }),
123
+ z.object({
124
+ All: z.object({}),
125
+ }),
126
+ ]),
127
+ authorized_tokens: z.union([
128
+ z.object({ Specific: z.object({}) }),
129
+ z.object({ All: z.object({}) }),
130
+ ]),
131
+ expiration: z.instanceof(BN),
132
+ extra: z.object({
133
+ 0: z.unknown(),
134
+ }),
135
+ user: z.instanceof(PublicKey),
97
136
  }),
98
137
  }),
99
- z.object({
100
- All: z.object({}),
138
+ }),
139
+ z.object({
140
+ V2: z.object({
141
+ "0": z.union([
142
+ z.object({
143
+ Revoked: z.instanceof(BN),
144
+ }),
145
+ z.object({
146
+ Active: z.object({
147
+ "0": z.object({
148
+ authorized_programs: z.union([
149
+ z.object({
150
+ Specific: z.object({
151
+ 0: z.array(z.object({
152
+ program_id: z.instanceof(PublicKey),
153
+ signer_pda: z.instanceof(PublicKey),
154
+ })),
155
+ }),
156
+ }),
157
+ z.object({
158
+ All: z.object({}),
159
+ }),
160
+ ]),
161
+ authorized_tokens: z.union([
162
+ z.object({ Specific: z.object({}) }),
163
+ z.object({ All: z.object({}) }),
164
+ ]),
165
+ expiration: z.instanceof(BN),
166
+ extra: z.object({
167
+ 0: z.unknown(),
168
+ }),
169
+ user: z.instanceof(PublicKey),
170
+ }),
171
+ }),
172
+ }),
173
+ ]),
101
174
  }),
102
- ]),
103
- authorized_tokens: z.union([
104
- z.object({ Specific: z.object({}) }),
105
- z.object({ All: z.object({}) }),
106
- ]),
107
- expiration: z.instanceof(BN),
108
- extra: z.object({
109
- 0: z.unknown(),
110
175
  }),
111
- major: z.number(),
112
- minor: z.number(),
113
- user: z.instanceof(PublicKey),
114
- }),
176
+ ]),
177
+ major: z.number(),
178
+ sponsor: z.instanceof(PublicKey),
115
179
  })
116
- .transform(({ session_info }) => ({
117
- authorizedPrograms: "All" in session_info.authorized_programs
118
- ? AuthorizedPrograms.All()
119
- : AuthorizedPrograms.Specific(session_info.authorized_programs.Specific[0].map(({ program_id, signer_pda }) => ({
120
- programId: program_id,
121
- signerPda: signer_pda,
122
- }))),
123
- authorizedTokens: "All" in session_info.authorized_tokens
124
- ? AuthorizedTokens.All
125
- : AuthorizedTokens.Specific,
126
- expiration: new Date(Number(session_info.expiration) * 1000),
127
- extra: session_info.extra[0],
128
- major: session_info.major,
129
- minor: session_info.minor,
130
- user: session_info.user,
131
- }));
180
+ .transform(({ session_info, major, sponsor }) => {
181
+ let activeSessionInfo;
182
+ let minor;
183
+ if ("V1" in session_info) {
184
+ activeSessionInfo = session_info.V1["0"];
185
+ minor = 1;
186
+ }
187
+ else if ("Active" in session_info.V2["0"]) {
188
+ activeSessionInfo = session_info.V2["0"].Active["0"];
189
+ minor = 2;
190
+ }
191
+ else {
192
+ return;
193
+ }
194
+ return {
195
+ authorizedPrograms: "All" in activeSessionInfo.authorized_programs
196
+ ? AuthorizedPrograms.All()
197
+ : AuthorizedPrograms.Specific(activeSessionInfo.authorized_programs.Specific[0].map(({ program_id, signer_pda }) => ({
198
+ programId: program_id,
199
+ signerPda: signer_pda,
200
+ }))),
201
+ authorizedTokens: "All" in activeSessionInfo.authorized_tokens
202
+ ? AuthorizedTokens.All
203
+ : AuthorizedTokens.Specific,
204
+ expiration: new Date(Number(activeSessionInfo.expiration) * 1000),
205
+ extra: activeSessionInfo.extra[0],
206
+ major: major,
207
+ minor: minor,
208
+ user: activeSessionInfo.user,
209
+ sponsor,
210
+ };
211
+ });
132
212
  export var AuthorizedProgramsType;
133
213
  (function (AuthorizedProgramsType) {
134
214
  AuthorizedProgramsType[AuthorizedProgramsType["All"] = 0] = "All";
@@ -181,6 +261,36 @@ const getTokenInfo = async (adapter, limits) => {
181
261
  };
182
262
  }));
183
263
  };
264
+ const serializeU16LE = (value) => {
265
+ const result = new ArrayBuffer(2);
266
+ new DataView(result).setUint16(0, value, true); // littleEndian = true
267
+ return new Uint8Array(result);
268
+ };
269
+ // Some wallets add a prefix to the messag before signing, for example Ledger through Phantom
270
+ const addOffchainMessagePrefixToMessageIfNeeded = async (walletPublicKey, signature, message) => {
271
+ const publicKey = await crypto.subtle.importKey("raw", walletPublicKey.toBytes(), { name: "Ed25519" }, true, ["verify"]);
272
+ if (await verifySignature(publicKey, signature, message)) {
273
+ return message;
274
+ }
275
+ else {
276
+ // Source: https://github.com/anza-xyz/solana-sdk/blob/master/offchain-message/src/lib.rs#L162
277
+ const messageWithOffchainMessagePrefix = Uint8Array.from([
278
+ // eslint-disable-next-line unicorn/number-literal-case
279
+ 0xff,
280
+ ...new TextEncoder().encode("solana offchain"),
281
+ 0,
282
+ 1,
283
+ ...serializeU16LE(message.length),
284
+ ...message,
285
+ ]);
286
+ if (await verifySignature(publicKey, signature, messageWithOffchainMessagePrefix)) {
287
+ return messageWithOffchainMessagePrefix;
288
+ }
289
+ else {
290
+ throw new Error("The signature provided by the browser wallet is not valid");
291
+ }
292
+ }
293
+ };
184
294
  const buildIntentInstruction = async (options, sessionKey, tokens) => {
185
295
  const message = await buildMessage({
186
296
  chainId: options.adapter.chainId,
@@ -194,7 +304,7 @@ const buildIntentInstruction = async (options, sessionKey, tokens) => {
194
304
  return Ed25519Program.createInstructionWithPublicKey({
195
305
  publicKey: options.walletPublicKey.toBytes(),
196
306
  signature: intentSignature,
197
- message: message,
307
+ message: await addOffchainMessagePrefixToMessageIfNeeded(options.walletPublicKey, intentSignature, message),
198
308
  });
199
309
  };
200
310
  const buildMessage = async (body) => new TextEncoder().encode([
@@ -342,7 +452,7 @@ const buildTransferIntentInstruction = async (program, options, symbol) => {
342
452
  return Ed25519Program.createInstructionWithPublicKey({
343
453
  publicKey: options.walletPublicKey.toBytes(),
344
454
  signature: intentSignature,
345
- message: message,
455
+ message: await addOffchainMessagePrefixToMessageIfNeeded(options.walletPublicKey, intentSignature, message),
346
456
  });
347
457
  };
348
458
  const getNonce = async (program, walletPublicKey) => {
@@ -352,3 +462,44 @@ const getNonce = async (program, walletPublicKey) => {
352
462
  });
353
463
  return program.account.nonce.fetchNullable(noncePda);
354
464
  };
465
+ const loginTokenPayloadSchema = z.object({
466
+ iat: z.number(),
467
+ sessionPublicKey: z.string(),
468
+ });
469
+ /**
470
+ * Create a login token signed with the session key
471
+ * @param session - The session to create a login token for
472
+ * @returns The login token
473
+ */
474
+ export const createLogInToken = async (session) => {
475
+ const payload = {
476
+ // ...we can pass any arbitrary data we want to sign here...
477
+ iat: Date.now(),
478
+ sessionPublicKey: session.sessionPublicKey.toBase58(),
479
+ };
480
+ const message = JSON.stringify(payload);
481
+ // Sign the payload with the session private key
482
+ const signature = await signMessageWithKey(session.sessionKey, message);
483
+ // Return base58(message) + base58(signature)
484
+ return `${bs58.encode(new TextEncoder().encode(message))}.${signature}`;
485
+ };
486
+ /**
487
+ * Verify a login token
488
+ * @param token - The login token to verify against the session public key
489
+ * @param connection - The connection to use to get the session account
490
+ * @returns The session account if the token is valid, otherwise undefined
491
+ */
492
+ export const verifyLogInToken = async (token, connection) => {
493
+ const [rawMessage, signature] = token.split(".");
494
+ if (!rawMessage || !signature)
495
+ return;
496
+ // Decode + parse payload
497
+ const messageStr = new TextDecoder().decode(bs58.decode(rawMessage));
498
+ const payload = loginTokenPayloadSchema.parse(JSON.parse(messageStr));
499
+ // Verify signature with sessionPublicKey
500
+ const sessionCryptoKey = await importKey(payload.sessionPublicKey);
501
+ const isValid = await verifyMessageWithKey(sessionCryptoKey, messageStr, signature);
502
+ if (!isValid)
503
+ return;
504
+ return getSessionAccount(connection, new PublicKey(payload.sessionPublicKey));
505
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fogo/sessions-sdk",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "A set of utilities for integrating with Fogo sessions",
5
5
  "keywords": [
6
6
  "fogo",
@@ -24,16 +24,6 @@
24
24
  "types": "./cjs/index.d.ts",
25
25
  "default": "./cjs/index.js"
26
26
  }
27
- },
28
- "./paymaster": {
29
- "import": {
30
- "types": "./esm/paymaster.d.ts",
31
- "default": "./esm/paymaster.js"
32
- },
33
- "require": {
34
- "types": "./cjs/paymaster.d.ts",
35
- "default": "./cjs/paymaster.js"
36
- }
37
27
  }
38
28
  },
39
29
  "dependencies": {
@@ -48,6 +38,6 @@
48
38
  "bn.js": "^5.1.2",
49
39
  "bs58": "^6.0.0",
50
40
  "zod": "^3.25.62",
51
- "@fogo/sessions-idls": "^0.0.4"
41
+ "@fogo/sessions-idls": "^0.0.5"
52
42
  }
53
43
  }
@@ -1,7 +0,0 @@
1
- import type { Transaction, Rpc, SolanaRpcApi } from "@solana/kit";
2
- import { Keypair } from "@solana/web3.js";
3
- export declare const sponsorAndSend: (rpc: Rpc<SolanaRpcApi>, sponsor: CryptoKeyPair, transaction: Transaction) => Promise<import("@solana/kit").Signature>;
4
- export declare const createPaymasterEndpoint: (options: {
5
- rpc: string;
6
- sponsor: Keypair;
7
- }) => Promise<(req: Request) => Promise<Response>>;
package/cjs/paymaster.js DELETED
@@ -1,51 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createPaymasterEndpoint = exports.sponsorAndSend = void 0;
4
- const compat_1 = require("@solana/compat");
5
- const kit_1 = require("@solana/kit");
6
- const zod_1 = require("zod");
7
- const sponsorAndSend = async (rpc, sponsor, transaction) => rpc
8
- .sendTransaction((0, kit_1.getBase64EncodedWireTransaction)(await (0, kit_1.signTransaction)([sponsor], transaction)), {
9
- encoding: "base64",
10
- skipPreflight: true,
11
- })
12
- .send();
13
- exports.sponsorAndSend = sponsorAndSend;
14
- const createPaymasterEndpoint = async (options) => {
15
- const rpc = (0, kit_1.createSolanaRpc)(options.rpc);
16
- const sponsor = await (0, compat_1.fromLegacyKeypair)(options.sponsor);
17
- return async (req) => {
18
- const data = postBodySchema.parse(await req.json());
19
- try {
20
- const transaction = (0, kit_1.getTransactionDecoder)().decode((0, kit_1.getBase64Encoder)().encode(data.transaction));
21
- try {
22
- return new Response(await (0, exports.sponsorAndSend)(rpc, sponsor, transaction));
23
- }
24
- catch (error) {
25
- // eslint-disable-next-line no-console
26
- console.error(error);
27
- return new Response(`Failed to sponsor and send: ${serializeError(error)}`, { status: 500 });
28
- }
29
- }
30
- catch (error) {
31
- // eslint-disable-next-line no-console
32
- console.error(error);
33
- return new Response("Failed to deserialize transaction", { status: 400 });
34
- }
35
- };
36
- };
37
- exports.createPaymasterEndpoint = createPaymasterEndpoint;
38
- const postBodySchema = zod_1.z.strictObject({
39
- transaction: zod_1.z.string(),
40
- });
41
- const serializeError = (error) => {
42
- if (error instanceof Error) {
43
- return error.message;
44
- }
45
- else if (typeof error === "string") {
46
- return error.toString();
47
- }
48
- else {
49
- return "Unknown Error";
50
- }
51
- };
@@ -1,7 +0,0 @@
1
- import type { Transaction, Rpc, SolanaRpcApi } from "@solana/kit";
2
- import { Keypair } from "@solana/web3.js";
3
- export declare const sponsorAndSend: (rpc: Rpc<SolanaRpcApi>, sponsor: CryptoKeyPair, transaction: Transaction) => Promise<import("@solana/kit").Signature>;
4
- export declare const createPaymasterEndpoint: (options: {
5
- rpc: string;
6
- sponsor: Keypair;
7
- }) => Promise<(req: Request) => Promise<Response>>;
package/esm/paymaster.js DELETED
@@ -1,47 +0,0 @@
1
- import { fromLegacyKeypair } from "@solana/compat";
2
- import { signTransaction, createSolanaRpc, getBase64EncodedWireTransaction, getTransactionDecoder, getBase64Encoder, } from "@solana/kit";
3
- import { Keypair } from "@solana/web3.js";
4
- import { z } from "zod";
5
- export const sponsorAndSend = async (rpc, sponsor, transaction) => rpc
6
- .sendTransaction(getBase64EncodedWireTransaction(await signTransaction([sponsor], transaction)), {
7
- encoding: "base64",
8
- skipPreflight: true,
9
- })
10
- .send();
11
- export const createPaymasterEndpoint = async (options) => {
12
- const rpc = createSolanaRpc(options.rpc);
13
- const sponsor = await fromLegacyKeypair(options.sponsor);
14
- return async (req) => {
15
- const data = postBodySchema.parse(await req.json());
16
- try {
17
- const transaction = getTransactionDecoder().decode(getBase64Encoder().encode(data.transaction));
18
- try {
19
- return new Response(await sponsorAndSend(rpc, sponsor, transaction));
20
- }
21
- catch (error) {
22
- // eslint-disable-next-line no-console
23
- console.error(error);
24
- return new Response(`Failed to sponsor and send: ${serializeError(error)}`, { status: 500 });
25
- }
26
- }
27
- catch (error) {
28
- // eslint-disable-next-line no-console
29
- console.error(error);
30
- return new Response("Failed to deserialize transaction", { status: 400 });
31
- }
32
- };
33
- };
34
- const postBodySchema = z.strictObject({
35
- transaction: z.string(),
36
- });
37
- const serializeError = (error) => {
38
- if (error instanceof Error) {
39
- return error.message;
40
- }
41
- else if (typeof error === "string") {
42
- return error.toString();
43
- }
44
- else {
45
- return "Unknown Error";
46
- }
47
- };