chrome-webstore-upload 4.0.1 → 4.0.2
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/distribution/errors.d.ts +5 -0
- package/distribution/errors.js +42 -0
- package/distribution/index.d.ts +22 -0
- package/distribution/index.js +133 -0
- package/distribution/types.d.ts +22 -0
- package/distribution/types.js +1 -0
- package/distribution/zip-dir.d.ts +1 -0
- package/distribution/zip-dir.js +17 -0
- package/package.json +13 -18
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class CWSError extends Error {
|
|
2
|
+
cause;
|
|
3
|
+
name = 'CWSError';
|
|
4
|
+
}
|
|
5
|
+
function parseErrorMessage(response) {
|
|
6
|
+
const errorResponse = response;
|
|
7
|
+
// Handle OAuth errors: { error: "invalid_grant", error_description: "Bad Request" }
|
|
8
|
+
if (typeof errorResponse.error === 'string') {
|
|
9
|
+
if (errorResponse.error === 'invalid_grant') {
|
|
10
|
+
return 'Invalid grant: The authentication keys are probably invalid or expired';
|
|
11
|
+
}
|
|
12
|
+
if (errorResponse.error === 'invalid_request') {
|
|
13
|
+
return `Invalid request: ${errorResponse.error_description ?? 'Missing required parameters'}`;
|
|
14
|
+
}
|
|
15
|
+
return errorResponse.error_description ?? errorResponse.error;
|
|
16
|
+
}
|
|
17
|
+
// Handle API errors: { error: { code: 400, message: "...", errors: [...] } }
|
|
18
|
+
if (errorResponse.error && typeof errorResponse.error === 'object') {
|
|
19
|
+
const { error } = errorResponse;
|
|
20
|
+
if (error.message) {
|
|
21
|
+
// Remove "Publish condition not met: " prefix if present
|
|
22
|
+
return error.message.replace(/^Publish condition not met: /, '');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Handle item errors in ItemResource: { itemError: [{ error_code: "...", error_detail: "..." }] }
|
|
26
|
+
const itemResource = response;
|
|
27
|
+
if (itemResource.itemError && Array.isArray(itemResource.itemError) && itemResource.itemError.length > 0) {
|
|
28
|
+
const errorDetails = itemResource.itemError.map(error => error.error_detail).join('; ');
|
|
29
|
+
return errorDetails;
|
|
30
|
+
}
|
|
31
|
+
return 'Unknown error';
|
|
32
|
+
}
|
|
33
|
+
export function throwIfNotOk(request, response) {
|
|
34
|
+
// Check for upload failure even on HTTP 200
|
|
35
|
+
const itemResource = response;
|
|
36
|
+
if (!request.ok || itemResource.uploadState === 'FAILURE') {
|
|
37
|
+
const message = parseErrorMessage(response);
|
|
38
|
+
const error = new CWSError(message);
|
|
39
|
+
error.cause = response;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReadStream } from 'node:fs';
|
|
2
|
+
import type { APIClientOptions, ItemResource, PublishResponse } from './types.js';
|
|
3
|
+
export declare const refreshTokenURI = "https://www.googleapis.com/oauth2/v4/token";
|
|
4
|
+
export type { APIClientOptions, ItemResource, PublishResponse } from './types.js';
|
|
5
|
+
export { CWSError } from './errors.js';
|
|
6
|
+
declare class APIClient {
|
|
7
|
+
extensionId: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
refreshToken: string;
|
|
10
|
+
clientSecret: string | undefined;
|
|
11
|
+
constructor(options: APIClientOptions);
|
|
12
|
+
uploadExisting(streamOrPath: ReadStream | ReadableStream | string, token?: string | Promise<string>, maxAwaitInProgressResponseSeconds?: number): Promise<ItemResource>;
|
|
13
|
+
publish(target?: string, token?: string | Promise<string>, deployPercentage?: number | undefined): Promise<PublishResponse>;
|
|
14
|
+
get(projection?: string, token?: string | Promise<string>): Promise<ItemResource>;
|
|
15
|
+
fetchToken(): Promise<string>;
|
|
16
|
+
_waitUploadSuccess(response: ItemResource, maxAwaitInProgressResponseSeconds: number): Promise<ItemResource>;
|
|
17
|
+
_headers(token: string): {
|
|
18
|
+
Authorization: string;
|
|
19
|
+
'x-goog-api-version': string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export default function chromeWebstoreUpload(options: APIClientOptions): APIClient;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// API documentation:
|
|
2
|
+
// https://developer.chrome.com/docs/webstore/api
|
|
3
|
+
// https://developer.chrome.com/docs/webstore/using-api
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { throwIfNotOk } from './errors.js';
|
|
6
|
+
import zipStreamFromDirectory from './zip-dir.js';
|
|
7
|
+
const rootURI = 'https://www.googleapis.com';
|
|
8
|
+
export const refreshTokenURI = 'https://www.googleapis.com/oauth2/v4/token';
|
|
9
|
+
const uploadExistingURI = (id) => `${rootURI}/upload/chromewebstore/v1.1/items/${id}`;
|
|
10
|
+
const publishURI = ({ extensionId, target = 'default', deployPercentage }) => {
|
|
11
|
+
const url = new URL(`${rootURI}/chromewebstore/v1.1/items/${extensionId}/publish`);
|
|
12
|
+
url.searchParams.set('publishTarget', target);
|
|
13
|
+
if (deployPercentage !== undefined) {
|
|
14
|
+
url.searchParams.set('deployPercentage', String(deployPercentage));
|
|
15
|
+
}
|
|
16
|
+
return url.href;
|
|
17
|
+
};
|
|
18
|
+
const getURI = (id, projection) => `${rootURI}/chromewebstore/v1.1/items/${id}?projection=${projection}`;
|
|
19
|
+
const requiredFields = ['extensionId', 'clientId', 'refreshToken'];
|
|
20
|
+
const retryIntervalSeconds = 2;
|
|
21
|
+
async function getStreamFromPath(filepath) {
|
|
22
|
+
const stats = await fs.promises.stat(filepath);
|
|
23
|
+
return stats.isFile()
|
|
24
|
+
? fs.createReadStream(filepath)
|
|
25
|
+
: zipStreamFromDirectory(filepath);
|
|
26
|
+
}
|
|
27
|
+
export { CWSError } from './errors.js';
|
|
28
|
+
class APIClient {
|
|
29
|
+
extensionId;
|
|
30
|
+
clientId;
|
|
31
|
+
refreshToken;
|
|
32
|
+
clientSecret;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
if (typeof fetch !== 'function') {
|
|
35
|
+
throw new TypeError('`chrome-webstore-upload` requires Node.js 18.17 or newer because it relies on the global `fetch` function.');
|
|
36
|
+
}
|
|
37
|
+
if (typeof options !== 'object') {
|
|
38
|
+
throw new TypeError('The options object is required');
|
|
39
|
+
}
|
|
40
|
+
for (const field of requiredFields) {
|
|
41
|
+
if (!options[field]) {
|
|
42
|
+
throw new Error(`Option "${field}" is required`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
this.extensionId = options.extensionId;
|
|
46
|
+
this.clientId = options.clientId;
|
|
47
|
+
this.refreshToken = options.refreshToken;
|
|
48
|
+
this.clientSecret = options.clientSecret;
|
|
49
|
+
}
|
|
50
|
+
async uploadExisting(streamOrPath, token = this.fetchToken(), maxAwaitInProgressResponseSeconds = 0) {
|
|
51
|
+
if (!streamOrPath) {
|
|
52
|
+
throw new Error('Read stream missing');
|
|
53
|
+
}
|
|
54
|
+
// Convert string path (file or directory) to stream
|
|
55
|
+
const readStream = typeof streamOrPath === 'string'
|
|
56
|
+
? await getStreamFromPath(streamOrPath)
|
|
57
|
+
: streamOrPath;
|
|
58
|
+
const { extensionId } = this;
|
|
59
|
+
const request = await fetch(uploadExistingURI(extensionId), {
|
|
60
|
+
method: 'PUT',
|
|
61
|
+
headers: this._headers(await token),
|
|
62
|
+
// @ts-expect-error Node extension? 🤷♂️ Required https://github.com/nodejs/node/issues/46221
|
|
63
|
+
duplex: 'half',
|
|
64
|
+
// Until they figure it out, this seems to work. Alternatively use https://stackoverflow.com/a/76780381/288906
|
|
65
|
+
body: readStream,
|
|
66
|
+
});
|
|
67
|
+
const response = await request.json();
|
|
68
|
+
throwIfNotOk(request, response);
|
|
69
|
+
return this._waitUploadSuccess(response, maxAwaitInProgressResponseSeconds);
|
|
70
|
+
}
|
|
71
|
+
async publish(target = 'default', token = this.fetchToken(), deployPercentage = undefined) {
|
|
72
|
+
const { extensionId } = this;
|
|
73
|
+
const request = await fetch(publishURI({ extensionId, target, deployPercentage }), {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: this._headers(await token),
|
|
76
|
+
});
|
|
77
|
+
const response = await request.json();
|
|
78
|
+
throwIfNotOk(request, response);
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
81
|
+
async get(projection = 'DRAFT', token = this.fetchToken()) {
|
|
82
|
+
const { extensionId } = this;
|
|
83
|
+
const request = await fetch(getURI(extensionId, projection), {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
headers: this._headers(await token),
|
|
86
|
+
});
|
|
87
|
+
const response = await request.json();
|
|
88
|
+
throwIfNotOk(request, response);
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
async fetchToken() {
|
|
92
|
+
const { clientId, clientSecret, refreshToken } = this;
|
|
93
|
+
const json = {
|
|
94
|
+
client_id: clientId,
|
|
95
|
+
refresh_token: refreshToken,
|
|
96
|
+
grant_type: 'refresh_token',
|
|
97
|
+
client_secret: clientSecret,
|
|
98
|
+
};
|
|
99
|
+
if (!clientSecret) {
|
|
100
|
+
delete json.client_secret;
|
|
101
|
+
}
|
|
102
|
+
const request = await fetch(refreshTokenURI, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
body: JSON.stringify(json),
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const response = await request.json();
|
|
110
|
+
throwIfNotOk(request, response);
|
|
111
|
+
return response.access_token;
|
|
112
|
+
}
|
|
113
|
+
async _waitUploadSuccess(response, maxAwaitInProgressResponseSeconds) {
|
|
114
|
+
if (response.uploadState !== 'IN_PROGRESS' || maxAwaitInProgressResponseSeconds < retryIntervalSeconds) {
|
|
115
|
+
return response;
|
|
116
|
+
}
|
|
117
|
+
// Wait before checking again
|
|
118
|
+
await new Promise(resolve => {
|
|
119
|
+
setTimeout(resolve, retryIntervalSeconds * 1000);
|
|
120
|
+
});
|
|
121
|
+
// Retry fetching the item resource
|
|
122
|
+
return this._waitUploadSuccess(await this.get('DRAFT'), maxAwaitInProgressResponseSeconds - retryIntervalSeconds);
|
|
123
|
+
}
|
|
124
|
+
_headers(token) {
|
|
125
|
+
return {
|
|
126
|
+
Authorization: `Bearer ${token}`,
|
|
127
|
+
'x-goog-api-version': '2',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export default function chromeWebstoreUpload(options) {
|
|
132
|
+
return new APIClient(options);
|
|
133
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type APIClientOptions = {
|
|
2
|
+
extensionId: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
clientSecret: string | undefined;
|
|
6
|
+
};
|
|
7
|
+
export type ItemResource = {
|
|
8
|
+
kind: 'chromewebstore#item';
|
|
9
|
+
id: string;
|
|
10
|
+
publicKey: string;
|
|
11
|
+
uploadState: 'FAILURE' | 'IN_PROGRESS' | 'NOT_FOUND' | 'SUCCESS';
|
|
12
|
+
itemError: Array<{
|
|
13
|
+
error_code: string;
|
|
14
|
+
error_detail: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export type PublishResponse = {
|
|
18
|
+
kind: 'chromewebstore#item';
|
|
19
|
+
item_id: string;
|
|
20
|
+
status: Array<'OK' | 'NOT_AUTHORIZED' | 'INVALID_DEVELOPER' | 'DEVELOPER_NO_OWNERSHIP' | 'DEVELOPER_SUSPENDED' | 'ITEM_NOT_FOUND' | 'ITEM_PENDING_REVIEW' | 'ITEM_TAKEN_DOWN' | 'PUBLISHER_SUSPENDED'>;
|
|
21
|
+
statusDetail: string[];
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function zipStreamFromDirectory(directory: string): Promise<NodeJS.ReadableStream>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { isNotJunk } from 'junk';
|
|
4
|
+
import yazl from 'yazl';
|
|
5
|
+
export default async function zipStreamFromDirectory(directory) {
|
|
6
|
+
const allFiles = await readdir(directory, { recursive: true });
|
|
7
|
+
const files = allFiles.filter(file => isNotJunk(basename(file)));
|
|
8
|
+
if (!files.includes('manifest.json')) {
|
|
9
|
+
throw new Error(`manifest.json was not found in ${directory}`);
|
|
10
|
+
}
|
|
11
|
+
const zip = new yazl.ZipFile();
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
zip.addFile(join(directory, file), file);
|
|
14
|
+
}
|
|
15
|
+
zip.end();
|
|
16
|
+
return zip.outputStream;
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-webstore-upload",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "Upload Chrome Extensions to the Chrome Web Store",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -42,34 +42,29 @@
|
|
|
42
42
|
"object-curly-spacing": [
|
|
43
43
|
"error",
|
|
44
44
|
"always"
|
|
45
|
+
],
|
|
46
|
+
"@typescript-eslint/naming-convention": "off",
|
|
47
|
+
"@stylistic/object-curly-spacing": [
|
|
48
|
+
"error",
|
|
49
|
+
"always"
|
|
50
|
+
],
|
|
51
|
+
"@stylistic/block-spacing": [
|
|
52
|
+
"error",
|
|
53
|
+
"always"
|
|
45
54
|
]
|
|
46
55
|
},
|
|
47
|
-
"overrides": [
|
|
48
|
-
{
|
|
49
|
-
"files": [
|
|
50
|
-
"**/*.ts"
|
|
51
|
-
],
|
|
52
|
-
"rules": {
|
|
53
|
-
"@typescript-eslint/naming-convention": "off",
|
|
54
|
-
"@typescript-eslint/object-curly-spacing": [
|
|
55
|
-
"error",
|
|
56
|
-
"always"
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
],
|
|
61
56
|
"space": 4
|
|
62
57
|
},
|
|
63
58
|
"devDependencies": {
|
|
64
|
-
"@sindresorhus/tsconfig": "^
|
|
59
|
+
"@sindresorhus/tsconfig": "^8.0.1",
|
|
65
60
|
"@types/yazl": "^3.3.0",
|
|
66
61
|
"dot-json": "^1.3.0",
|
|
67
62
|
"fetch-mock": "^9.11.0",
|
|
68
63
|
"node-fetch": "^2.7.0",
|
|
69
|
-
"typescript": "^5.
|
|
64
|
+
"typescript": "^5.9.3",
|
|
70
65
|
"utc-version": "^2.0.2",
|
|
71
66
|
"vitest": "^3.2.4",
|
|
72
|
-
"xo": "^
|
|
67
|
+
"xo": "^1.2.3"
|
|
73
68
|
},
|
|
74
69
|
"engines": {
|
|
75
70
|
"node": ">=20"
|