@stacksjs/browser-extension 0.70.119 → 0.70.120
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/README.md +1 -0
- package/dist/app-store-connect.d.ts +50 -0
- package/dist/app-store-connect.js +92 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/safari.d.ts +2 -0
- package/dist/safari.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Safari with MAIN-world content scripts + `match_about_blank`).
|
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
buddy extension:safari:init # scaffold the Xcode container app into safari/
|
|
31
|
+
buddy extension:safari:provision # register both Bundle IDs + check the app record
|
|
31
32
|
buddy extension:safari:app # build + sync into the appex + xcodebuild
|
|
32
33
|
buddy extension:safari:publish # signed archive + App Store Connect upload
|
|
33
34
|
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AppStoreConnectAuth } from './safari';
|
|
2
|
+
import type { ExtensionConfig } from './types';
|
|
3
|
+
/** Generate the short-lived ES256 team token required by App Store Connect. */
|
|
4
|
+
export declare function appStoreConnectToken(auth: Required<AppStoreConnectAuth>, now?: unknown): string;
|
|
5
|
+
/**
|
|
6
|
+
* Register the explicit container and extension Bundle IDs required by a
|
|
7
|
+
* Safari Web Extension, then check for the manually-created App Store Connect
|
|
8
|
+
* app record. Apple does not expose an official API for creating that record.
|
|
9
|
+
*/
|
|
10
|
+
export declare function provisionSafariApp(config: ExtensionConfig, options?: SafariProvisionOptions): Promise<SafariProvisionResult>;
|
|
11
|
+
export declare interface AppStoreConnectResource<T extends Record<string, unknown>> {
|
|
12
|
+
type: string
|
|
13
|
+
id: string
|
|
14
|
+
attributes: T
|
|
15
|
+
}
|
|
16
|
+
export declare interface BundleIdAttributes extends Record<string, unknown> {
|
|
17
|
+
identifier: string
|
|
18
|
+
name: string
|
|
19
|
+
platform: BundleIdPlatform
|
|
20
|
+
seedId?: string
|
|
21
|
+
}
|
|
22
|
+
export declare interface AppAttributes extends Record<string, unknown> {
|
|
23
|
+
bundleId: string
|
|
24
|
+
name: string
|
|
25
|
+
primaryLocale: string
|
|
26
|
+
sku: string
|
|
27
|
+
}
|
|
28
|
+
export declare interface AppStoreConnectClientOptions extends AppStoreConnectAuth {
|
|
29
|
+
baseUrl?: string
|
|
30
|
+
fetch?: typeof globalThis.fetch
|
|
31
|
+
now?: () => number
|
|
32
|
+
}
|
|
33
|
+
export declare interface SafariProvisionOptions extends AppStoreConnectClientOptions {
|
|
34
|
+
checkOnly?: boolean
|
|
35
|
+
platform?: BundleIdPlatform
|
|
36
|
+
}
|
|
37
|
+
export declare interface SafariProvisionResult {
|
|
38
|
+
container: { identifier: string, exists: boolean, created: boolean }
|
|
39
|
+
extension: { identifier: string, exists: boolean, created: boolean }
|
|
40
|
+
appRecord: { exists: boolean, id?: string }
|
|
41
|
+
}
|
|
42
|
+
export type BundleIdPlatform = 'IOS' | 'MAC_OS' | 'UNIVERSAL';
|
|
43
|
+
/** Minimal official App Store Connect client for Safari provisioning checks. */
|
|
44
|
+
export declare class AppStoreConnectClient {
|
|
45
|
+
constructor(options?: AppStoreConnectClientOptions);
|
|
46
|
+
findBundleId(identifier: string): Promise<AppStoreConnectResource<BundleIdAttributes> | undefined>;
|
|
47
|
+
registerBundleId(identifier: string, name: string, platform?: BundleIdPlatform): Promise<AppStoreConnectResource<BundleIdAttributes>>;
|
|
48
|
+
ensureBundleId(identifier: string, name: string, options?: { checkOnly?: boolean, platform?: BundleIdPlatform }): Promise<{ bundleId?: AppStoreConnectResource<BundleIdAttributes>, created: boolean }>;
|
|
49
|
+
findApp(bundleId: string): Promise<AppStoreConnectResource<AppAttributes> | undefined>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createPrivateKey, sign } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolveAppStoreConnectAuth } from "./safari";
|
|
4
|
+
const appStoreConnectBaseUrl = "https://api.appstoreconnect.apple.com/v1";
|
|
5
|
+
function base64urlJson(value) {
|
|
6
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
7
|
+
}
|
|
8
|
+
export function appStoreConnectToken(auth, now = Math.floor(Date.now() / 1000)) {
|
|
9
|
+
const header = { alg: "ES256", kid: auth.keyId, typ: "JWT" }, payload = {
|
|
10
|
+
iss: auth.issuerId,
|
|
11
|
+
iat: now,
|
|
12
|
+
exp: now + 120,
|
|
13
|
+
aud: "appstoreconnect-v1"
|
|
14
|
+
}, input = `${base64urlJson(header)}.${base64urlJson(payload)}`;
|
|
15
|
+
let key;
|
|
16
|
+
try {
|
|
17
|
+
key = createPrivateKey(readFileSync(auth.keyPath, "utf8"));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw Error(`[browser-extension] App Store Connect API key could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
}
|
|
21
|
+
const signature = sign("sha256", Buffer.from(input), { key, dsaEncoding: "ieee-p1363" });
|
|
22
|
+
return `${input}.${signature.toString("base64url")}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AppStoreConnectClient {
|
|
26
|
+
auth;
|
|
27
|
+
baseUrl;
|
|
28
|
+
fetcher;
|
|
29
|
+
now;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.auth = resolveAppStoreConnectAuth(options);
|
|
32
|
+
this.baseUrl = (options.baseUrl ?? appStoreConnectBaseUrl).replace(/\/$/, "");
|
|
33
|
+
this.fetcher = options.fetch ?? globalThis.fetch;
|
|
34
|
+
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
35
|
+
}
|
|
36
|
+
async request(path, init = {}) {
|
|
37
|
+
const response = await this.fetcher(`${this.baseUrl}${path}`, {
|
|
38
|
+
...init,
|
|
39
|
+
headers: {
|
|
40
|
+
Accept: "application/json",
|
|
41
|
+
Authorization: `Bearer ${appStoreConnectToken(this.auth, this.now())}`,
|
|
42
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
43
|
+
...init.headers
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const details = (await response.json().catch(() => ({}))).errors?.map((error) => error.detail ?? error.title ?? error.code).filter(Boolean).join("; ");
|
|
48
|
+
throw Error(`[browser-extension] App Store Connect ${init.method ?? "GET"} ${path} failed (${response.status})${details ? `: ${details}` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
return await response.json();
|
|
51
|
+
}
|
|
52
|
+
async findBundleId(identifier) {
|
|
53
|
+
const query = new URLSearchParams({ "filter[identifier]": identifier });
|
|
54
|
+
return (await this.request(`/bundleIds?${query}`)).data.find((bundleId) => bundleId.attributes.identifier === identifier);
|
|
55
|
+
}
|
|
56
|
+
async registerBundleId(identifier, name, platform = "MAC_OS") {
|
|
57
|
+
return (await this.request("/bundleIds", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
data: {
|
|
61
|
+
type: "bundleIds",
|
|
62
|
+
attributes: { identifier, name, platform }
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})).data;
|
|
66
|
+
}
|
|
67
|
+
async ensureBundleId(identifier, name, options = {}) {
|
|
68
|
+
const existing = await this.findBundleId(identifier);
|
|
69
|
+
if (existing)
|
|
70
|
+
return { bundleId: existing, created: !1 };
|
|
71
|
+
if (options.checkOnly)
|
|
72
|
+
return { created: !1 };
|
|
73
|
+
return {
|
|
74
|
+
bundleId: await this.registerBundleId(identifier, name, options.platform),
|
|
75
|
+
created: !0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async findApp(bundleId) {
|
|
79
|
+
const query = new URLSearchParams({ "filter[bundleId]": bundleId });
|
|
80
|
+
return (await this.request(`/apps?${query}`)).data.find((app) => app.attributes.bundleId === bundleId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function provisionSafariApp(config, options = {}) {
|
|
84
|
+
if (!config.safariBundleId)
|
|
85
|
+
throw Error("[browser-extension] Safari provisioning needs safariBundleId in config/extension.ts");
|
|
86
|
+
const client = new AppStoreConnectClient(options), identifier = config.safariBundleId, extensionIdentifier = `${identifier}.Extension`, container = await client.ensureBundleId(identifier, config.name, options), extension = await client.ensureBundleId(extensionIdentifier, `${config.name} Safari Extension`, options), app = await client.findApp(identifier);
|
|
87
|
+
return {
|
|
88
|
+
container: { identifier, exists: Boolean(container.bundleId), created: container.created },
|
|
89
|
+
extension: { identifier: extensionIdentifier, exists: Boolean(extension.bundleId), created: extension.created },
|
|
90
|
+
appRecord: { exists: Boolean(app), id: app?.id }
|
|
91
|
+
};
|
|
92
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/safari.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare function syncSafariResources(config: ExtensionConfig, options?: S
|
|
|
38
38
|
* built `.app` path (undefined when xcodebuild was skipped/unavailable).
|
|
39
39
|
*/
|
|
40
40
|
export declare function buildSafariApp(config: ExtensionConfig, options?: SafariAppBuildOptions): Promise<{ appPath?: string, resources: string }>;
|
|
41
|
+
/** Resolve and validate App Store Connect credentials from options or environment variables. */
|
|
42
|
+
export declare function resolveAppStoreConnectAuth(options: AppStoreConnectAuth): Required<AppStoreConnectAuth>;
|
|
41
43
|
/**
|
|
42
44
|
* Create a signed Release archive and either validate it or upload it to App
|
|
43
45
|
* Store Connect. Xcode owns certificate/profile creation and the upload so the
|
package/dist/safari.js
CHANGED
|
@@ -132,7 +132,7 @@ export async function buildSafariApp(config, options = {}) {
|
|
|
132
132
|
await Bun.$`xcodebuild -project ${join(dir, `${appName}.xcodeproj`)} -scheme ${appName} -configuration ${configuration} -derivedDataPath ${derivedData} ${signing} build`;
|
|
133
133
|
return { appPath: join(derivedData, "Build", "Products", configuration, `${appName}.app`), resources };
|
|
134
134
|
}
|
|
135
|
-
function
|
|
135
|
+
export function resolveAppStoreConnectAuth(options) {
|
|
136
136
|
const keyId = options.keyId ?? process.env.APP_STORE_CONNECT_API_KEY_ID, issuerId = options.issuerId ?? process.env.APP_STORE_CONNECT_API_ISSUER_ID, keyPath = options.keyPath ?? process.env.APP_STORE_CONNECT_API_KEY_PATH, missing = [
|
|
137
137
|
!keyId && "APP_STORE_CONNECT_API_KEY_ID",
|
|
138
138
|
!issuerId && "APP_STORE_CONNECT_API_ISSUER_ID",
|
|
@@ -180,7 +180,7 @@ export async function publishSafariApp(config, options) {
|
|
|
180
180
|
const cwd = options.cwd ?? process.cwd(), teamId = options.teamId ?? config.safariTeamId;
|
|
181
181
|
if (!teamId)
|
|
182
182
|
throw Error("[browser-extension] Safari publishing needs safariTeamId in config/extension.ts or --team-id");
|
|
183
|
-
const auth =
|
|
183
|
+
const auth = resolveAppStoreConnectAuth(options), buildNumber = options.buildNumber ?? process.env.GITHUB_RUN_NUMBER ?? String(Math.floor(Date.now() / 1000));
|
|
184
184
|
if (!/^\d+(?:\.\d+){0,2}$/.test(buildNumber))
|
|
185
185
|
throw Error(`[browser-extension] invalid Safari build number ${buildNumber}; use one to three dot-separated integers`);
|
|
186
186
|
if (options.build !== !1)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/browser-extension",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.120",
|
|
5
5
|
"description": "Build MV3 browser extensions (Chrome, Firefox, Safari) the Stacks way — manifest, content/background scripts, DNR rules, packaging, Safari container app, all config-driven.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|