Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/app/commands/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe("makeCreateCommand", () => {
expiresIn: 3600,
secret: "secret",
issuedAt: 1000,
apiBaseUrl: "https://api.line.me",
});
vi.mocked(mockAddApp).mockResolvedValueOnce({ liffId: "12345" });

Expand Down Expand Up @@ -85,6 +86,7 @@ describe("makeCreateCommand", () => {
expiresIn: 3600,
secret: "secret",
issuedAt: 1000,
apiBaseUrl: "https://api.line.me",
});

const command = makeCreateCommand();
Expand Down
8 changes: 4 additions & 4 deletions src/app/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ type CreateAppOptions = {
};

export const createLiffApp = async (options: CreateAppOptions) => {
const accessToken = (await resolveChannel(options?.channelId))?.accessToken;
if (!accessToken) {
const channel = await resolveChannel(options?.channelId);
if (!channel?.accessToken) {
throw new Error(`Access token not found.
Please provide a valid channel ID or set the current channel first.
`);
}

const client = new LiffApiClient({
token: accessToken,
baseUrl: "https://api.line.me",
token: channel.accessToken,
baseUrl: channel.apiBaseUrl,
});
const { liffId } = await client.addApp({
view: {
Expand Down
2 changes: 2 additions & 0 deletions src/app/commands/delete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("makeDeleteCommand", () => {
expiresIn: 3600,
secret: "secret",
issuedAt: 1000,
apiBaseUrl: "https://api.line.me",
});
vi.mocked(inquire.prompt).mockResolvedValue({ confirmDelete: true });

Expand Down Expand Up @@ -105,6 +106,7 @@ describe("makeDeleteCommand", () => {
expiresIn: 3600,
secret: "secret",
issuedAt: 1000,
apiBaseUrl: "https://api.line.me",
});
vi.mocked(inquire.prompt).mockResolvedValue({ confirmDelete: false });

Expand Down
8 changes: 4 additions & 4 deletions src/app/commands/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ const deleteAction = async (options: {
channelId?: string;
liffId: string;
}) => {
const accessToken = (await resolveChannel(options?.channelId))?.accessToken;
if (!accessToken) {
const channel = await resolveChannel(options?.channelId);
if (!channel?.accessToken) {
throw new Error(`Access token not found.
Please provide a valid channel ID or set the current channel first.
`);
Expand All @@ -25,8 +25,8 @@ const deleteAction = async (options: {
if (!confirmDelete) return;

const client = new LiffApiClient({
token: accessToken,
baseUrl: "https://api.line.me",
token: channel.accessToken,
baseUrl: channel.apiBaseUrl,
});
console.info(`Deleting LIFF app...`);
await client.deleteApp(options.liffId);
Expand Down
1 change: 1 addition & 0 deletions src/app/commands/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe("makeListCommand", () => {
expiresIn: 3600,
secret: "secret",
issuedAt: 1000,
apiBaseUrl: "https://api.line.me",
});
vi.mocked(mockFetchApps).mockResolvedValueOnce({
apps: [
Expand Down
8 changes: 4 additions & 4 deletions src/app/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { LiffApiClient } from "../../api/liff.js";
import { resolveChannel } from "../../channel/resolveChannel.js";

const listAction = async (options: { channelId?: string }) => {
const accessToken = (await resolveChannel(options?.channelId))?.accessToken;
if (!accessToken) {
const channel = await resolveChannel(options?.channelId);
if (!channel?.accessToken) {
throw new Error(`Access token not found.
Please provide a valid channel ID or set the current channel first.
`);
}

const client = new LiffApiClient({
token: accessToken,
baseUrl: "https://api.line.me",
token: channel.accessToken,
baseUrl: channel.apiBaseUrl,
});
const { apps } = await client.fetchApps();

Expand Down
1 change: 1 addition & 0 deletions src/app/commands/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe("makeUpdateCommand", () => {
expiresIn: 3600,
secret: "secret",
issuedAt: 1000,
apiBaseUrl: "https://api.line.me",
});
vi.mocked(mockUpdateApp).mockResolvedValueOnce();

Expand Down
8 changes: 4 additions & 4 deletions src/app/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ const updateAction = async (options: {
endpointUrl?: string;
viewType?: string;
}) => {
const accessToken = (await resolveChannel(options?.channelId))?.accessToken;
if (!accessToken) {
const channel = await resolveChannel(options?.channelId);
if (!channel?.accessToken) {
throw new Error(`Access token not found.
Please provide a valid channel ID or set the current channel first.
`);
}

const client = new LiffApiClient({
token: accessToken,
baseUrl: "https://api.line.me",
token: channel.accessToken,
baseUrl: channel.apiBaseUrl,
});
await client.updateApp(options.liffId, {
view: {
Expand Down
74 changes: 72 additions & 2 deletions src/channel/commands/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import inquire from "inquirer";

import { makeAddCommand } from "./add.js";
import { upsertChannel } from "../stores/channels.js";
import { getChannel, upsertChannel } from "../stores/channels.js";
import { renewAccessToken } from "../renewAccessToken.js";

vi.mock("inquirer");
Expand Down Expand Up @@ -45,13 +45,83 @@ describe("add", () => {
accessToken: "access_token",
expiresIn: 3600,
issuedAt: now,
apiBaseUrl: "https://api.line.me",
});

const command = makeAddCommand();
await command.parseAsync(["_", "add", "123"]);

expect(renewAccessToken).toHaveBeenCalledWith("123", "secret", now);
expect(renewAccessToken).toHaveBeenCalledWith(
"123",
"secret",
now,
"https://api.line.me",
);
expect(console.info).toHaveBeenCalledWith("Channel 123 is now added.");
});

it("should use the specified API base URL", async () => {
const now = Date.now();
vi.setSystemTime(now);

vi.mocked(inquire.prompt).mockResolvedValue({ channelSecret: "secret" });
vi.mocked(renewAccessToken).mockResolvedValue({
secret: "secret",
accessToken: "access_token",
expiresIn: 3600,
issuedAt: now,
apiBaseUrl: "https://api.example.com",
});

const command = makeAddCommand();
await command.parseAsync([
"_",
"add",
"123",
"--api-base-url",
"https://api.example.com",
]);

expect(renewAccessToken).toHaveBeenCalledWith(
"123",
"secret",
now,
"https://api.example.com",
);
});

it("should keep the registered API base URL when the option is omitted", async () => {
const now = Date.now();
vi.setSystemTime(now);

vi.mocked(inquire.prompt).mockResolvedValue({ channelSecret: "secret" });
vi.mocked(getChannel).mockReturnValue({
secret: "secret",
accessToken: "access_token",
expiresIn: 3600,
issuedAt: now,
apiBaseUrl: "https://api.example.com",
});

const command = makeAddCommand();
await command.parseAsync(["_", "add", "123"]);

expect(renewAccessToken).toHaveBeenCalledWith(
"123",
"secret",
now,
"https://api.example.com",
);
});

it("should throw an error for an invalid API base URL", async () => {
vi.mocked(inquire.prompt).mockResolvedValue({ channelSecret: "secret" });

const command = makeAddCommand();
await expect(
command.parseAsync(["_", "add", "123", "--api-base-url", "not-a-url"]),
).rejects.toThrowError("Invalid API base URL: not-a-url");
expect(renewAccessToken).not.toHaveBeenCalled();
});
});
});
27 changes: 23 additions & 4 deletions src/channel/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@ import { createCommand } from "commander";
import inquirer from "inquirer";

import { renewAccessToken } from "../renewAccessToken.js";
import { DEFAULT_API_BASE_URL, getChannel } from "../stores/channels.js";

export const addAction: (channelId?: string) => Promise<void> = async (
channelId,
) => {
export const addAction: (
channelId: string | undefined,
options?: { apiBaseUrl?: string },
) => Promise<void> = async (channelId, options = {}) => {
if (!channelId) {
throw new Error("Channel ID is required.");
}

// When --api-base-url is omitted, keep the URL already registered for the
// channel so that re-running `channel add` doesn't silently reset it.
const apiBaseUrl =
options.apiBaseUrl ??
getChannel(channelId)?.apiBaseUrl ??
DEFAULT_API_BASE_URL;

try {
new URL(apiBaseUrl);
} catch {
throw new Error(`Invalid API base URL: ${apiBaseUrl}`);
}

const { channelSecret } = await inquirer.prompt<{ channelSecret: string }>([
{
type: "password",
Expand All @@ -19,7 +34,7 @@ export const addAction: (channelId?: string) => Promise<void> = async (
},
]);

await renewAccessToken(channelId, channelSecret, Date.now());
await renewAccessToken(channelId, channelSecret, Date.now(), apiBaseUrl);

console.info(`Channel ${channelId} is now added.`);
};
Expand All @@ -29,6 +44,10 @@ export const makeAddCommand = () => {
add
.description("Register a LIFF channel that you want to manage")
.argument("[channelId]", "The channel ID to add")
.option(
"--api-base-url <url>",
"The base URL of the LIFF and auth API. This option is intended for LINE internal use only.",
)
.action(addAction);
return add;
};
1 change: 1 addition & 0 deletions src/channel/commands/use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe("makeUseCommand", () => {
expiresIn: 3600,
secret: "secret",
issuedAt: 1000,
apiBaseUrl: "https://api.line.me",
});

const command = makeUseCommand();
Expand Down
14 changes: 12 additions & 2 deletions src/channel/renewAccessToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,29 @@ describe("renewAccessToken", () => {
const accessToken = "new_access_token";
const expiresIn = 3600;
const issuedAt = Date.now();
const apiBaseUrl = "https://api.example.com";

vi.mocked(mockFetchStatelessChannelAccessToken).mockResolvedValue({
token_type: "Bearer",
access_token: accessToken,
expires_in: expiresIn,
});
vi.mocked(upsertChannel).mockResolvedValue({
vi.mocked(upsertChannel).mockReturnValue({
secret: channelSecret,
accessToken,
expiresIn,
issuedAt,
apiBaseUrl,
});

const result = await renewAccessToken(channelId, channelSecret, issuedAt);
const result = await renewAccessToken(
channelId,
channelSecret,
issuedAt,
apiBaseUrl,
);

expect(AuthApiClient).toHaveBeenCalledWith({ baseUrl: apiBaseUrl });
expect(
vi.mocked(mockFetchStatelessChannelAccessToken),
).toHaveBeenCalledWith({
Expand All @@ -62,12 +70,14 @@ describe("renewAccessToken", () => {
accessToken,
expiresIn,
issuedAt,
apiBaseUrl,
);
expect(result).toStrictEqual({
secret: channelSecret,
accessToken,
expiresIn,
issuedAt,
apiBaseUrl,
});
});

Expand Down
10 changes: 8 additions & 2 deletions src/channel/renewAccessToken.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { AuthApiClient } from "../api/auth.js";
import { ChannelInfo, upsertChannel } from "./stores/channels.js";
import {
ChannelInfo,
DEFAULT_API_BASE_URL,
upsertChannel,
} from "./stores/channels.js";

export const renewAccessToken = async (
channelId: string,
channelSecret: string,
issuedAt: number,
apiBaseUrl: string = DEFAULT_API_BASE_URL,
): Promise<ChannelInfo | undefined> => {
const client = new AuthApiClient({
baseUrl: "https://api.line.me",
baseUrl: apiBaseUrl,
});
const res = await client.fetchStatelessChannelAccessToken({
channelId: channelId,
Expand All @@ -22,5 +27,6 @@ export const renewAccessToken = async (
res.access_token,
res.expires_in,
issuedAt,
apiBaseUrl,
);
};
Loading
Loading