Skip to content

Commit 9775720

Browse files
authored
feat: SDK update for version 22.2.0 (#330)
* chore: update Command Line SDK to 22.2.0 * chore: update Command Line SDK to 22.2.0 * chore: update Command Line SDK to 22.2.0 * chore: update Command Line SDK to 22.2.0
1 parent 2f0063b commit 9775720

19 files changed

Lines changed: 527 additions & 128 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Change Log
22

3+
## 22.2.0
4+
5+
* Added: OAuth refresh tokens now stored in the OS keychain via `@napi-rs/keyring`, falling back to config
6+
* Added: `--type` option to `functions list-specifications` and `sites list-specifications`
7+
* Updated: Bumped `@appwrite.io/console` dependency to `^15.1.1`
8+
* Updated: Cleaner account selection prompt for `logout` with a `(current)` marker
9+
* Fixed: OAuth login now clears the stale legacy session cookie
10+
* Fixed: Browser launch on Windows now uses `rundll32` for OAuth flows
11+
312
## 22.1.3
413

514
* Added: `--resource` option to `oauth2 authorize`, `create-device-authorization`, and `create-token` for RFC 8707 resource indicators

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Once the installation is complete, you can verify the install using
2929

3030
```sh
3131
$ appwrite -v
32-
22.1.3
32+
22.2.0
3333
```
3434

3535
### Install using prebuilt binaries
@@ -83,7 +83,7 @@ $ scoop install https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/sc
8383
Once the installation completes, you can verify your install using
8484
```
8585
$ appwrite -v
86-
22.1.3
86+
22.2.0
8787
```
8888

8989
## Getting Started

bun.lock

Lines changed: 42 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

install.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# You can use "View source" of this page to see the full script.
1414

1515
# REPO
16-
$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.3/appwrite-cli-win-x64.exe"
17-
$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.1.3/appwrite-cli-win-arm64.exe"
16+
$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.2.0/appwrite-cli-win-x64.exe"
17+
$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/22.2.0/appwrite-cli-win-arm64.exe"
1818

1919
$APPWRITE_BINARY_NAME = "appwrite.exe"
2020

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ verifyMacOSCodeSignature() {
120120
downloadBinary() {
121121
echo "[2/5] Downloading executable for $OS ($ARCH) ..."
122122

123-
GITHUB_LATEST_VERSION="22.1.3"
123+
GITHUB_LATEST_VERSION="22.2.0"
124124
GITHUB_FILE="appwrite-cli-${OS}-${ARCH}"
125125
GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE"
126126

lib/auth/login.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
restoreCurrentSession,
3333
deleteServerSession,
3434
} from "./session.js";
35+
import { setStoredRefreshToken } from "./refresh-token.js";
3536

3637
const DEFAULT_ENDPOINT = "https://cloud.appwrite.io/v1";
3738

@@ -71,7 +72,7 @@ const startWaitingForApprovalSpinner = (): (() => void) => {
7172
};
7273
};
7374

74-
const listenForBrowserOpen = (
75+
export const listenForBrowserOpen = (
7576
url: string,
7677
onCancel: () => void,
7778
): (() => void) => {
@@ -90,17 +91,14 @@ const listenForBrowserOpen = (
9091
if (shouldRestoreRawMode) {
9192
stdin.setRawMode?.(true);
9293
}
93-
const shouldPause = stdin.isPaused();
9494
stdin.resume();
9595

9696
const cleanup = (): void => {
9797
stdin.off("data", onData);
9898
if (shouldRestoreRawMode) {
9999
stdin.setRawMode?.(false);
100100
}
101-
if (shouldPause) {
102-
stdin.pause();
103-
}
101+
stdin.pause();
104102
};
105103

106104
// Open the browser at most once; keep listening afterwards so Ctrl+C still
@@ -393,7 +391,7 @@ const loginWithOAuthDevice = async ({
393391

394392
const tokenExpiry = Date.now() + token.expires_in * 1000;
395393
globalConfig.setAccessToken(token.access_token);
396-
globalConfig.setRefreshToken(token.refresh_token || "");
394+
setStoredRefreshToken(id, token.refresh_token || "");
397395
globalConfig.setTokenExpiry(tokenExpiry);
398396

399397
let tokenEmail = "";
@@ -419,6 +417,7 @@ const loginWithOAuthDevice = async ({
419417
}
420418

421419
globalConfig.setEmail(account.email);
420+
globalConfig.removeCookie();
422421

423422
const { removed: removedLegacySessions, failed: failedLegacySessions } =
424423
await removeLegacySessionsExcept(id);

lib/auth/refresh-token.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Entry } from "@napi-rs/keyring";
2+
import { globalConfig } from "../config.js";
3+
import { EXECUTABLE_NAME } from "../constants.js";
4+
import type { SessionData } from "../types.js";
5+
6+
const REFRESH_TOKEN_SERVICE = `${EXECUTABLE_NAME}-oauth-refresh-token`;
7+
8+
interface KeyringEntry {
9+
setPassword(password: string): void;
10+
getPassword(): string | null;
11+
deletePassword(): boolean;
12+
}
13+
14+
type KeyringEntryFactory = (service: string, account: string) => KeyringEntry;
15+
16+
let keyringEntryFactory: KeyringEntryFactory = (service, account) =>
17+
new Entry(service, account);
18+
19+
const getSessionData = (sessionId: string): SessionData | undefined =>
20+
globalConfig.get(sessionId) as SessionData | undefined;
21+
22+
const setSessionData = (sessionId: string, data: SessionData): void => {
23+
globalConfig.addSession(sessionId, data);
24+
};
25+
26+
const setPrefsRefreshToken = (sessionId: string, refreshToken: string): void => {
27+
const session = getSessionData(sessionId);
28+
if (!session) return;
29+
30+
setSessionData(sessionId, {
31+
...session,
32+
refreshToken,
33+
});
34+
};
35+
36+
export const deletePrefsRefreshToken = (sessionId: string): void => {
37+
const session = getSessionData(sessionId);
38+
if (!session?.refreshToken) return;
39+
40+
const { refreshToken: _refreshToken, ...rest } = session;
41+
setSessionData(sessionId, rest);
42+
};
43+
44+
export const setRefreshTokenEntryFactoryForTests = (
45+
factory: KeyringEntryFactory,
46+
): (() => void) => {
47+
if (process.env.NODE_ENV !== "test") {
48+
throw new Error("setRefreshTokenEntryFactoryForTests is for tests only");
49+
}
50+
51+
const previousFactory = keyringEntryFactory;
52+
keyringEntryFactory = factory;
53+
return () => {
54+
keyringEntryFactory = previousFactory;
55+
};
56+
};
57+
58+
export const getStoredRefreshToken = (sessionId: string): string => {
59+
try {
60+
const refreshToken = keyringEntryFactory(
61+
REFRESH_TOKEN_SERVICE,
62+
sessionId,
63+
).getPassword();
64+
65+
if (refreshToken) {
66+
return refreshToken;
67+
}
68+
} catch (_error) {
69+
// Fall through to prefs fallback below.
70+
}
71+
72+
return getSessionData(sessionId)?.refreshToken ?? "";
73+
};
74+
75+
export const hasStoredRefreshToken = (sessionId: string): boolean =>
76+
getStoredRefreshToken(sessionId) !== "";
77+
78+
export const setStoredRefreshToken = (
79+
sessionId: string,
80+
refreshToken: string,
81+
): void => {
82+
if (!refreshToken) {
83+
deleteStoredRefreshToken(sessionId);
84+
return;
85+
}
86+
87+
try {
88+
keyringEntryFactory(REFRESH_TOKEN_SERVICE, sessionId).setPassword(
89+
refreshToken,
90+
);
91+
deletePrefsRefreshToken(sessionId);
92+
} catch (_error) {
93+
setPrefsRefreshToken(sessionId, refreshToken);
94+
}
95+
};
96+
97+
export const deleteStoredRefreshToken = (sessionId: string): void => {
98+
try {
99+
keyringEntryFactory(REFRESH_TOKEN_SERVICE, sessionId).deletePassword();
100+
} catch (_error) {
101+
// Missing or unavailable secure storage must not block local cleanup.
102+
}
103+
104+
deletePrefsRefreshToken(sessionId);
105+
};

lib/auth/session.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import type { SessionData } from "../types.js";
33
import ClientLegacy from "../client.js";
44
import { OAUTH2_CLIENT_ID } from "../constants.js";
55
import { revokeRefreshToken } from "./oauth.js";
6+
import {
7+
deleteStoredRefreshToken,
8+
getStoredRefreshToken,
9+
hasStoredRefreshToken,
10+
} from "./refresh-token.js";
611

712
/**
813
* Typed accessor for a stored session, avoiding repeated inline casts.
@@ -32,7 +37,7 @@ export const hasAuthSession = (): boolean =>
3237
*/
3338
export const isLocalOnlySession = (sessionId: string): boolean => {
3439
const session = getSession(sessionId);
35-
return Boolean(session && !session.refreshToken && !session.cookie);
40+
return Boolean(session && !hasStoredRefreshToken(sessionId) && !session.cookie);
3641
};
3742

3843
/**
@@ -72,6 +77,7 @@ export const restoreCurrentSessionFallback = (
7277
export const removeCurrentSession = (): void => {
7378
const current = globalConfig.getCurrentSession();
7479
globalConfig.setCurrentSession("");
80+
deleteStoredRefreshToken(current);
7581
globalConfig.removeSession(current);
7682
};
7783

@@ -89,10 +95,11 @@ export const deleteServerSession = async (
8995
}
9096

9197
try {
92-
if (session.refreshToken) {
98+
const refreshToken = getStoredRefreshToken(sessionId);
99+
if (refreshToken) {
93100
await revokeRefreshToken(
94101
session.endpoint,
95-
session.refreshToken,
102+
refreshToken,
96103
session.clientId || OAUTH2_CLIENT_ID,
97104
);
98105
return { deleted: true };
@@ -136,13 +143,15 @@ export const logoutSessions = async (
136143

137144
for (const sessionId of sessionIds) {
138145
if (isLocalOnlySession(sessionId)) {
146+
deleteStoredRefreshToken(sessionId);
139147
globalConfig.removeSession(sessionId);
140148
continue;
141149
}
142150

143151
globalConfig.setCurrentSession(sessionId);
144152
const result = await deleteServerSession(sessionId);
145153
if (result.deleted) {
154+
deleteStoredRefreshToken(sessionId);
146155
globalConfig.removeSession(sessionId);
147156
} else {
148157
failed++;
@@ -169,6 +178,7 @@ export const removeLegacySessionsExcept = async (
169178

170179
const result = await deleteServerSession(sessionId);
171180
if (result.deleted) {
181+
deleteStoredRefreshToken(sessionId);
172182
globalConfig.removeSession(sessionId);
173183
removed++;
174184
} else {

lib/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
SDK_TITLE,
2020
EXECUTABLE_NAME,
2121
} from "./constants.js";
22+
import { deleteStoredRefreshToken } from "./auth/refresh-token.js";
2223

2324
class Client {
2425
private endpoint: string;
@@ -250,6 +251,7 @@ class Client {
250251

251252
const current = globalConfig.getCurrentSession();
252253
globalConfig.setCurrentSession("");
254+
deleteStoredRefreshToken(current);
253255
globalConfig.removeSession(current);
254256
}
255257

lib/commands/services/functions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,11 @@ const functionsListRuntimesCommand = functions
123123
const functionsListSpecificationsCommand = functions
124124
.command(`list-specifications`)
125125
.description(`List allowed function specifications for this instance.`)
126+
.option(`--type <type>`, `Specification type to list. Can be one of: runtimes, builds.`)
126127
.action(
127128
actionRunner(
128-
async () => parse(await (await getFunctionsClient()).listSpecifications()),
129+
async ({ type }) =>
130+
parse(await (await getFunctionsClient()).listSpecifications(type)),
129131
),
130132
);
131133

0 commit comments

Comments
 (0)