@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 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
@@ -8,6 +8,7 @@
8
8
  * app scaffold, appex resource sync, and xcodebuild pipeline.
9
9
  */
10
10
  export * from './build';
11
+ export * from './app-store-connect';
11
12
  export * from './config';
12
13
  export * from './manifest';
13
14
  export * from './package';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./build";
2
+ export * from "./app-store-connect";
2
3
  export * from "./config";
3
4
  export * from "./manifest";
4
5
  export * from "./package";
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 appStoreConnectAuth(options) {
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 = appStoreConnectAuth(options), buildNumber = options.buildNumber ?? process.env.GITHUB_RUN_NUMBER ?? String(Math.floor(Date.now() / 1000));
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.119",
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": [