sliccy 1.57.1 → 1.58.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/dist/node-server/publish-chrome-web-store.d.ts +51 -0
- package/dist/node-server/publish-chrome-web-store.js +373 -0
- package/dist/ui/assets/{index-DVIFVVQ-.js → index-CURp5qKe.js} +198 -198
- package/dist/ui/assets/lick-manager-D6_9f0OR.js +1 -0
- package/dist/ui/assets/secret-env-OKzGv9D2.js +1 -0
- package/dist/ui/assets/shell-B45M6r84.js +1 -0
- package/dist/ui/assets/sprinkle-renderer-Dnz_l3xR.js +1 -0
- package/dist/ui/index.html +1 -1
- package/dist/ui/packages/webapp/index.html +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/lick-manager-CYnp_g5P.js +0 -1
- package/dist/ui/assets/secret-env-B2go5yyt.js +0 -1
- package/dist/ui/assets/shell-D9rdl4qT.js +0 -1
- package/dist/ui/assets/sprinkle-renderer-iMqXngft.js +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
interface ServiceAccountCredentials {
|
|
2
|
+
client_email: string;
|
|
3
|
+
private_key: string;
|
|
4
|
+
token_uri?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface ChromeWebStoreConfig {
|
|
8
|
+
publisherId: string;
|
|
9
|
+
itemId: string;
|
|
10
|
+
publishType?: 'DEFAULT_PUBLISH' | 'STAGED_PUBLISH';
|
|
11
|
+
deployPercentage?: number;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
forceCancelPendingReview?: boolean;
|
|
14
|
+
skipReview?: boolean;
|
|
15
|
+
serviceAccount: ServiceAccountCredentials;
|
|
16
|
+
}
|
|
17
|
+
interface FetchStatusResponse {
|
|
18
|
+
name: string;
|
|
19
|
+
itemId: string;
|
|
20
|
+
publishedItemRevisionStatus?: ItemRevisionStatus;
|
|
21
|
+
submittedItemRevisionStatus?: ItemRevisionStatus;
|
|
22
|
+
lastAsyncUploadState?: string;
|
|
23
|
+
takenDown?: boolean;
|
|
24
|
+
warned?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface ItemRevisionStatus {
|
|
27
|
+
state?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ChromeWebStorePublishResult {
|
|
30
|
+
version: string;
|
|
31
|
+
itemId: string;
|
|
32
|
+
uploadState: string;
|
|
33
|
+
publishState: string;
|
|
34
|
+
}
|
|
35
|
+
export interface PublishChromeWebStoreOptions {
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
fetchImpl?: typeof fetch;
|
|
38
|
+
log?: Pick<Console, 'log' | 'warn'>;
|
|
39
|
+
manifestPath?: string;
|
|
40
|
+
projectRoot?: string;
|
|
41
|
+
nowSeconds?: () => number;
|
|
42
|
+
pollIntervalMs?: number;
|
|
43
|
+
maxUploadPollAttempts?: number;
|
|
44
|
+
waitMs?: (ms: number) => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare function parseServiceAccountCredentials(jsonValue: string | undefined, base64Value: string | undefined): ServiceAccountCredentials | undefined;
|
|
47
|
+
export declare function readChromeWebStoreConfig(env?: NodeJS.ProcessEnv): ChromeWebStoreConfig | null;
|
|
48
|
+
export declare function createServiceAccountAssertion(serviceAccount: ServiceAccountCredentials, nowSeconds: number): string;
|
|
49
|
+
export declare function waitForUploadCompletion(config: ChromeWebStoreConfig, accessToken: string, fetchImpl: typeof fetch, waitMs: (ms: number) => Promise<void>, pollIntervalMs: number, maxAttempts: number): Promise<FetchStatusResponse>;
|
|
50
|
+
export declare function publishChromeWebStoreRelease(options?: PublishChromeWebStoreOptions): Promise<ChromeWebStorePublishResult | null>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { createSign } from 'crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve, relative } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
6
|
+
const PROJECT_ROOT = resolve(__dirname, '..', '..');
|
|
7
|
+
const DEFAULT_RELEASE_MANIFEST_PATH = resolve(PROJECT_ROOT, 'artifacts', 'release', 'release-artifacts.json');
|
|
8
|
+
const CHROME_WEB_STORE_SCOPE = 'https://www.googleapis.com/auth/chromewebstore';
|
|
9
|
+
const DEFAULT_TOKEN_URI = 'https://oauth2.googleapis.com/token';
|
|
10
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
11
|
+
const DEFAULT_MAX_UPLOAD_POLL_ATTEMPTS = 30;
|
|
12
|
+
const SUCCESSFUL_PUBLISH_STATES = new Set([
|
|
13
|
+
'PENDING_REVIEW',
|
|
14
|
+
'STAGED',
|
|
15
|
+
'PUBLISHED',
|
|
16
|
+
'PUBLISHED_TO_TESTERS',
|
|
17
|
+
]);
|
|
18
|
+
function getEnvValue(env, name) {
|
|
19
|
+
const value = env[name]?.trim();
|
|
20
|
+
return value ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
function parseOptionalBoolean(value, envName) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (value === 'true' || value === '1')
|
|
26
|
+
return true;
|
|
27
|
+
if (value === 'false' || value === '0')
|
|
28
|
+
return false;
|
|
29
|
+
throw new Error(`${envName} must be one of true, false, 1, or 0.`);
|
|
30
|
+
}
|
|
31
|
+
function parseOptionalPercentage(value) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
if (!/^\d+$/.test(value)) {
|
|
35
|
+
throw new Error('CHROME_WEB_STORE_DEPLOY_PERCENTAGE must be an integer between 0 and 100.');
|
|
36
|
+
}
|
|
37
|
+
const parsed = Number.parseInt(value, 10);
|
|
38
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
|
|
39
|
+
throw new Error('CHROME_WEB_STORE_DEPLOY_PERCENTAGE must be an integer between 0 and 100.');
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
function parseOptionalPublishType(value) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (value === 'DEFAULT_PUBLISH' || value === 'STAGED_PUBLISH')
|
|
47
|
+
return value;
|
|
48
|
+
throw new Error('CHROME_WEB_STORE_PUBLISH_TYPE must be DEFAULT_PUBLISH or STAGED_PUBLISH.');
|
|
49
|
+
}
|
|
50
|
+
export function parseServiceAccountCredentials(jsonValue, base64Value) {
|
|
51
|
+
const raw = jsonValue ??
|
|
52
|
+
(base64Value ? Buffer.from(base64Value, 'base64').toString('utf8').trim() : undefined);
|
|
53
|
+
if (!raw)
|
|
54
|
+
return undefined;
|
|
55
|
+
const credentials = JSON.parse(raw);
|
|
56
|
+
if (typeof credentials.client_email !== 'string' || !credentials.client_email.trim()) {
|
|
57
|
+
throw new Error('Chrome Web Store service account credentials must include a non-empty client_email.');
|
|
58
|
+
}
|
|
59
|
+
if (typeof credentials.private_key !== 'string' || !credentials.private_key.trim()) {
|
|
60
|
+
throw new Error('Chrome Web Store service account credentials must include a non-empty private_key.');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...credentials,
|
|
64
|
+
client_email: credentials.client_email,
|
|
65
|
+
private_key: credentials.private_key,
|
|
66
|
+
token_uri: typeof credentials.token_uri === 'string' && credentials.token_uri.trim()
|
|
67
|
+
? credentials.token_uri
|
|
68
|
+
: DEFAULT_TOKEN_URI,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function readChromeWebStoreConfig(env = process.env) {
|
|
72
|
+
const publisherId = getEnvValue(env, 'CHROME_WEB_STORE_PUBLISHER_ID');
|
|
73
|
+
const itemId = getEnvValue(env, 'CHROME_WEB_STORE_ITEM_ID');
|
|
74
|
+
const jsonCredentials = getEnvValue(env, 'CHROME_WEB_STORE_SERVICE_ACCOUNT_JSON');
|
|
75
|
+
const base64Credentials = getEnvValue(env, 'CHROME_WEB_STORE_SERVICE_ACCOUNT_JSON_BASE64');
|
|
76
|
+
const publishType = getEnvValue(env, 'CHROME_WEB_STORE_PUBLISH_TYPE');
|
|
77
|
+
const deployPercentage = getEnvValue(env, 'CHROME_WEB_STORE_DEPLOY_PERCENTAGE');
|
|
78
|
+
const dryRun = getEnvValue(env, 'CHROME_WEB_STORE_DRY_RUN');
|
|
79
|
+
const forceCancelPendingReview = getEnvValue(env, 'CHROME_WEB_STORE_FORCE_CANCEL_PENDING');
|
|
80
|
+
const skipReview = getEnvValue(env, 'CHROME_WEB_STORE_SKIP_REVIEW');
|
|
81
|
+
const hasAnyChromeWebStoreSetting = [
|
|
82
|
+
publisherId,
|
|
83
|
+
itemId,
|
|
84
|
+
jsonCredentials,
|
|
85
|
+
base64Credentials,
|
|
86
|
+
publishType,
|
|
87
|
+
deployPercentage,
|
|
88
|
+
dryRun,
|
|
89
|
+
forceCancelPendingReview,
|
|
90
|
+
skipReview,
|
|
91
|
+
].some(Boolean);
|
|
92
|
+
if (!hasAnyChromeWebStoreSetting)
|
|
93
|
+
return null;
|
|
94
|
+
const missing = [];
|
|
95
|
+
if (!publisherId)
|
|
96
|
+
missing.push('CHROME_WEB_STORE_PUBLISHER_ID');
|
|
97
|
+
if (!itemId)
|
|
98
|
+
missing.push('CHROME_WEB_STORE_ITEM_ID');
|
|
99
|
+
if (!jsonCredentials && !base64Credentials) {
|
|
100
|
+
missing.push('CHROME_WEB_STORE_SERVICE_ACCOUNT_JSON or CHROME_WEB_STORE_SERVICE_ACCOUNT_JSON_BASE64');
|
|
101
|
+
}
|
|
102
|
+
if (missing.length > 0) {
|
|
103
|
+
throw new Error(`Chrome Web Store publishing is partially configured. Missing: ${missing.join(', ')}.`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
publisherId: publisherId,
|
|
107
|
+
itemId: itemId,
|
|
108
|
+
publishType: parseOptionalPublishType(publishType),
|
|
109
|
+
deployPercentage: parseOptionalPercentage(deployPercentage),
|
|
110
|
+
dryRun: parseOptionalBoolean(dryRun, 'CHROME_WEB_STORE_DRY_RUN'),
|
|
111
|
+
forceCancelPendingReview: parseOptionalBoolean(forceCancelPendingReview, 'CHROME_WEB_STORE_FORCE_CANCEL_PENDING'),
|
|
112
|
+
skipReview: parseOptionalBoolean(skipReview, 'CHROME_WEB_STORE_SKIP_REVIEW'),
|
|
113
|
+
serviceAccount: parseServiceAccountCredentials(jsonCredentials, base64Credentials),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function createServiceAccountAssertion(serviceAccount, nowSeconds) {
|
|
117
|
+
const header = { alg: 'RS256', typ: 'JWT' };
|
|
118
|
+
const claims = {
|
|
119
|
+
iss: serviceAccount.client_email,
|
|
120
|
+
scope: CHROME_WEB_STORE_SCOPE,
|
|
121
|
+
aud: serviceAccount.token_uri ?? DEFAULT_TOKEN_URI,
|
|
122
|
+
exp: nowSeconds + 3600,
|
|
123
|
+
iat: nowSeconds,
|
|
124
|
+
};
|
|
125
|
+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
126
|
+
const encodedClaims = Buffer.from(JSON.stringify(claims)).toString('base64url');
|
|
127
|
+
const unsignedToken = `${encodedHeader}.${encodedClaims}`;
|
|
128
|
+
const signer = createSign('RSA-SHA256');
|
|
129
|
+
signer.update(unsignedToken);
|
|
130
|
+
signer.end();
|
|
131
|
+
const signature = signer.sign(serviceAccount.private_key).toString('base64url');
|
|
132
|
+
return `${unsignedToken}.${signature}`;
|
|
133
|
+
}
|
|
134
|
+
function readReleaseManifest(manifestPath) {
|
|
135
|
+
if (!existsSync(manifestPath)) {
|
|
136
|
+
throw new Error(`Release manifest was not found at ${manifestPath}. Run npm run package:release first.`);
|
|
137
|
+
}
|
|
138
|
+
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
139
|
+
}
|
|
140
|
+
function resolveArtifactPath(projectRoot, projectRelativePath) {
|
|
141
|
+
return resolve(projectRoot, projectRelativePath);
|
|
142
|
+
}
|
|
143
|
+
function toProjectRelative(projectRoot, filePath) {
|
|
144
|
+
return relative(projectRoot, filePath).split('\\').join('/');
|
|
145
|
+
}
|
|
146
|
+
function createPublisherItemName(config) {
|
|
147
|
+
return `publishers/${config.publisherId}/items/${config.itemId}`;
|
|
148
|
+
}
|
|
149
|
+
async function parseJsonResponse(response) {
|
|
150
|
+
const text = await response.text();
|
|
151
|
+
if (!text)
|
|
152
|
+
return {};
|
|
153
|
+
return JSON.parse(text);
|
|
154
|
+
}
|
|
155
|
+
async function expectJsonResponse(response, context) {
|
|
156
|
+
const data = await parseJsonResponse(response);
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const serialized = data ? ` ${JSON.stringify(data)}` : '';
|
|
159
|
+
throw new Error(`${context} failed with ${response.status} ${response.statusText}.${serialized}`);
|
|
160
|
+
}
|
|
161
|
+
return data;
|
|
162
|
+
}
|
|
163
|
+
async function exchangeServiceAccountToken(serviceAccount, fetchImpl, nowSeconds) {
|
|
164
|
+
const assertion = createServiceAccountAssertion(serviceAccount, nowSeconds());
|
|
165
|
+
const body = new URLSearchParams({
|
|
166
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
167
|
+
assertion,
|
|
168
|
+
});
|
|
169
|
+
const response = await fetchImpl(serviceAccount.token_uri ?? DEFAULT_TOKEN_URI, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
173
|
+
},
|
|
174
|
+
body,
|
|
175
|
+
});
|
|
176
|
+
const tokenResponse = await expectJsonResponse(response, 'Chrome Web Store OAuth token exchange');
|
|
177
|
+
if (!tokenResponse.access_token) {
|
|
178
|
+
throw new Error('Chrome Web Store OAuth token exchange did not return an access_token.');
|
|
179
|
+
}
|
|
180
|
+
return tokenResponse.access_token;
|
|
181
|
+
}
|
|
182
|
+
async function uploadExtensionArchive(config, accessToken, archiveBytes, fetchImpl) {
|
|
183
|
+
const itemName = createPublisherItemName(config);
|
|
184
|
+
const response = await fetchImpl(`https://chromewebstore.googleapis.com/upload/v2/${itemName}:upload`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: {
|
|
187
|
+
authorization: `Bearer ${accessToken}`,
|
|
188
|
+
'content-type': 'application/zip',
|
|
189
|
+
},
|
|
190
|
+
body: archiveBytes,
|
|
191
|
+
});
|
|
192
|
+
return expectJsonResponse(response, 'Chrome Web Store upload');
|
|
193
|
+
}
|
|
194
|
+
async function fetchUploadStatus(config, accessToken, fetchImpl) {
|
|
195
|
+
const itemName = createPublisherItemName(config);
|
|
196
|
+
const response = await fetchImpl(`https://chromewebstore.googleapis.com/v2/${itemName}:fetchStatus`, {
|
|
197
|
+
method: 'GET',
|
|
198
|
+
headers: {
|
|
199
|
+
authorization: `Bearer ${accessToken}`,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return expectJsonResponse(response, 'Chrome Web Store upload status');
|
|
203
|
+
}
|
|
204
|
+
async function cancelPendingSubmission(config, accessToken, fetchImpl) {
|
|
205
|
+
const itemName = createPublisherItemName(config);
|
|
206
|
+
const response = await fetchImpl(`https://chromewebstore.googleapis.com/v2/${itemName}:cancelSubmission`, {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: {
|
|
209
|
+
authorization: `Bearer ${accessToken}`,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
const text = await response.text();
|
|
214
|
+
throw new Error(`Chrome Web Store cancel submission failed with ${response.status} ${response.statusText}.${text ? ` ${text}` : ''}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function waitForPendingReviewCancellation(config, accessToken, fetchImpl, waitMs, pollIntervalMs, maxAttempts) {
|
|
218
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
219
|
+
const status = await fetchUploadStatus(config, accessToken, fetchImpl);
|
|
220
|
+
if (status.submittedItemRevisionStatus?.state !== 'PENDING_REVIEW') {
|
|
221
|
+
return status;
|
|
222
|
+
}
|
|
223
|
+
if (attempt === maxAttempts) {
|
|
224
|
+
throw new Error(`Chrome Web Store pending review cancellation did not complete after ${maxAttempts} status checks.`);
|
|
225
|
+
}
|
|
226
|
+
await waitMs(pollIntervalMs);
|
|
227
|
+
}
|
|
228
|
+
throw new Error('Chrome Web Store pending review cancellation polling failed unexpectedly.');
|
|
229
|
+
}
|
|
230
|
+
export async function waitForUploadCompletion(config, accessToken, fetchImpl, waitMs, pollIntervalMs, maxAttempts) {
|
|
231
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
232
|
+
const status = await fetchUploadStatus(config, accessToken, fetchImpl);
|
|
233
|
+
const uploadState = status.lastAsyncUploadState;
|
|
234
|
+
if (uploadState === 'SUCCEEDED')
|
|
235
|
+
return status;
|
|
236
|
+
if (uploadState === 'FAILED' || uploadState === 'NOT_FOUND') {
|
|
237
|
+
throw new Error(`Chrome Web Store async upload finished in state ${uploadState}.`);
|
|
238
|
+
}
|
|
239
|
+
if (uploadState !== undefined && uploadState !== 'IN_PROGRESS') {
|
|
240
|
+
throw new Error(`Chrome Web Store async upload returned unknown state ${uploadState}.`);
|
|
241
|
+
}
|
|
242
|
+
if (attempt === maxAttempts) {
|
|
243
|
+
throw new Error(`Chrome Web Store async upload did not complete after ${maxAttempts} status checks.`);
|
|
244
|
+
}
|
|
245
|
+
await waitMs(pollIntervalMs);
|
|
246
|
+
}
|
|
247
|
+
throw new Error('Chrome Web Store async upload polling failed unexpectedly.');
|
|
248
|
+
}
|
|
249
|
+
async function ensureSubmissionCanProceed(config, accessToken, fetchImpl, log, waitMs, pollIntervalMs, maxAttempts) {
|
|
250
|
+
const status = await fetchUploadStatus(config, accessToken, fetchImpl);
|
|
251
|
+
if (status.submittedItemRevisionStatus?.state !== 'PENDING_REVIEW') {
|
|
252
|
+
return { skipped: false, status };
|
|
253
|
+
}
|
|
254
|
+
if (config.dryRun) {
|
|
255
|
+
const outcome = config.forceCancelPendingReview
|
|
256
|
+
? 'would cancel the pending review and resubmit'
|
|
257
|
+
: 'would skip Chrome publish';
|
|
258
|
+
log.warn(`Dry run: Chrome Web Store item ${config.itemId} already has a revision pending review, so the release ${outcome}.`);
|
|
259
|
+
return { skipped: true, status };
|
|
260
|
+
}
|
|
261
|
+
if (!config.forceCancelPendingReview) {
|
|
262
|
+
log.warn(`Skipping Chrome Web Store publish for item ${config.itemId} because a revision is already pending review. Set CHROME_WEB_STORE_FORCE_CANCEL_PENDING=true to cancel the pending review and resubmit automatically.`);
|
|
263
|
+
return { skipped: true, status };
|
|
264
|
+
}
|
|
265
|
+
log.warn(`Cancelling the pending Chrome Web Store review for item ${config.itemId} before uploading a new revision.`);
|
|
266
|
+
await cancelPendingSubmission(config, accessToken, fetchImpl);
|
|
267
|
+
const updatedStatus = await waitForPendingReviewCancellation(config, accessToken, fetchImpl, waitMs, pollIntervalMs, maxAttempts);
|
|
268
|
+
return { skipped: false, status: updatedStatus };
|
|
269
|
+
}
|
|
270
|
+
function logChromeWebStoreItemWarnings(config, status, log) {
|
|
271
|
+
if (status.warned) {
|
|
272
|
+
log.warn(`Chrome Web Store item ${config.itemId} is currently warned in the developer dashboard.`);
|
|
273
|
+
}
|
|
274
|
+
if (status.takenDown) {
|
|
275
|
+
log.warn(`Chrome Web Store item ${config.itemId} is currently taken down in the developer dashboard.`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function publishItem(config, accessToken, fetchImpl) {
|
|
279
|
+
const itemName = createPublisherItemName(config);
|
|
280
|
+
const deployInfos = config.deployPercentage === undefined
|
|
281
|
+
? undefined
|
|
282
|
+
: [
|
|
283
|
+
{
|
|
284
|
+
deployPercentage: config.deployPercentage,
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
const body = {
|
|
288
|
+
...(config.publishType ? { publishType: config.publishType } : {}),
|
|
289
|
+
...(deployInfos ? { deployInfos } : {}),
|
|
290
|
+
...(config.skipReview === undefined ? {} : { skipReview: config.skipReview }),
|
|
291
|
+
};
|
|
292
|
+
const response = await fetchImpl(`https://chromewebstore.googleapis.com/v2/${itemName}:publish`, {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: {
|
|
295
|
+
authorization: `Bearer ${accessToken}`,
|
|
296
|
+
'content-type': 'application/json',
|
|
297
|
+
},
|
|
298
|
+
body: JSON.stringify(body),
|
|
299
|
+
});
|
|
300
|
+
return expectJsonResponse(response, 'Chrome Web Store publish');
|
|
301
|
+
}
|
|
302
|
+
export async function publishChromeWebStoreRelease(options = {}) {
|
|
303
|
+
const env = options.env ?? process.env;
|
|
304
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
305
|
+
const log = options.log ?? console;
|
|
306
|
+
const projectRoot = options.projectRoot ?? PROJECT_ROOT;
|
|
307
|
+
const manifestPath = options.manifestPath ?? DEFAULT_RELEASE_MANIFEST_PATH;
|
|
308
|
+
const nowSeconds = options.nowSeconds ?? (() => Math.floor(Date.now() / 1000));
|
|
309
|
+
const waitMs = options.waitMs ?? ((ms) => new Promise((resolveWait) => setTimeout(resolveWait, ms)));
|
|
310
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
311
|
+
const maxUploadPollAttempts = options.maxUploadPollAttempts ?? DEFAULT_MAX_UPLOAD_POLL_ATTEMPTS;
|
|
312
|
+
const config = readChromeWebStoreConfig(env);
|
|
313
|
+
if (!config) {
|
|
314
|
+
log.log('Skipping Chrome Web Store publish because CHROME_WEB_STORE_* env vars are not set.');
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const releaseManifest = readReleaseManifest(manifestPath);
|
|
318
|
+
const archivePath = resolveArtifactPath(projectRoot, releaseManifest.extensionArchive);
|
|
319
|
+
if (!existsSync(archivePath)) {
|
|
320
|
+
throw new Error(`Chrome Web Store extension archive was not found at ${archivePath}. Run npm run package:release first.`);
|
|
321
|
+
}
|
|
322
|
+
const archiveBytes = readFileSync(archivePath);
|
|
323
|
+
const accessToken = await exchangeServiceAccountToken(config.serviceAccount, fetchImpl, nowSeconds);
|
|
324
|
+
const submissionGuard = await ensureSubmissionCanProceed(config, accessToken, fetchImpl, log, waitMs, pollIntervalMs, maxUploadPollAttempts);
|
|
325
|
+
if (submissionGuard.skipped) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
logChromeWebStoreItemWarnings(config, submissionGuard.status, log);
|
|
329
|
+
if (config.dryRun) {
|
|
330
|
+
log.log(`Dry run: verified Chrome Web Store access for item ${config.itemId}; would upload ${toProjectRelative(projectRoot, archivePath)} and publish version ${releaseManifest.version}.`);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const uploadResponse = await uploadExtensionArchive(config, accessToken, archiveBytes, fetchImpl);
|
|
334
|
+
if (uploadResponse.uploadState === 'FAILED' || uploadResponse.uploadState === 'NOT_FOUND') {
|
|
335
|
+
throw new Error(`Chrome Web Store upload finished immediately in state ${uploadResponse.uploadState}.`);
|
|
336
|
+
}
|
|
337
|
+
if (uploadResponse.uploadState !== 'SUCCEEDED' && uploadResponse.uploadState !== 'IN_PROGRESS') {
|
|
338
|
+
throw new Error(`Chrome Web Store upload returned unknown state ${uploadResponse.uploadState}.`);
|
|
339
|
+
}
|
|
340
|
+
if (uploadResponse.crxVersion && uploadResponse.crxVersion !== releaseManifest.version) {
|
|
341
|
+
throw new Error(`Chrome Web Store accepted version ${uploadResponse.crxVersion}, but release artifacts expect ${releaseManifest.version}.`);
|
|
342
|
+
}
|
|
343
|
+
const status = uploadResponse.uploadState === 'IN_PROGRESS'
|
|
344
|
+
? await waitForUploadCompletion(config, accessToken, fetchImpl, waitMs, pollIntervalMs, maxUploadPollAttempts)
|
|
345
|
+
: undefined;
|
|
346
|
+
if (status) {
|
|
347
|
+
logChromeWebStoreItemWarnings(config, status, log);
|
|
348
|
+
}
|
|
349
|
+
const publishResponse = await publishItem(config, accessToken, fetchImpl);
|
|
350
|
+
if (!SUCCESSFUL_PUBLISH_STATES.has(publishResponse.state)) {
|
|
351
|
+
throw new Error(`Chrome Web Store publish returned unexpected item state ${publishResponse.state}.`);
|
|
352
|
+
}
|
|
353
|
+
log.log(`Published ${toProjectRelative(projectRoot, archivePath)} to Chrome Web Store item ${config.itemId} (${publishResponse.state}).`);
|
|
354
|
+
return {
|
|
355
|
+
version: releaseManifest.version,
|
|
356
|
+
itemId: config.itemId,
|
|
357
|
+
uploadState: uploadResponse.uploadState === 'IN_PROGRESS' ? 'SUCCEEDED' : uploadResponse.uploadState,
|
|
358
|
+
publishState: publishResponse.state,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function main() {
|
|
362
|
+
const manifestPath = process.argv[2]
|
|
363
|
+
? resolve(PROJECT_ROOT, process.argv[2])
|
|
364
|
+
: DEFAULT_RELEASE_MANIFEST_PATH;
|
|
365
|
+
await publishChromeWebStoreRelease({ manifestPath });
|
|
366
|
+
}
|
|
367
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
368
|
+
void main().catch((error) => {
|
|
369
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
370
|
+
console.error(`[publish-chrome-web-store] ${message}`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
});
|
|
373
|
+
}
|