authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 10:41:13-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 10:41:13-07:00
log20734d4a78e4acb1bc1fff8083df5f71165cc545
treed369f78211919442c21659f815000b4c243e3921
parent86db35ca6175c3340a18d151a486b77b82664ba7
signature Signed by PGP key B5690EEEBB952194

[codex] Structure macOS passkey signing failures (#3303)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 213 insertions(+), 47 deletions(-)

scripts/build-desktop-artifact.test.ts+73-16
...@@ -6,10 +6,15 @@ import * as Layer from "effect/Layer";...@@ -6,10 +6,15 @@ import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";6import * as Option from "effect/Option";
77
8import {8import {
9 BuildScriptError,
9 createStageWorkspaceConfig,10 createStageWorkspaceConfig,
10 createStagePnpmConfig,11 createStagePnpmConfig,
11 createBuildConfig,12 createBuildConfig,
12 DESKTOP_ASAR_UNPACK,13 DESKTOP_ASAR_UNPACK,
14 InvalidMacPasskeyRpDomainError,
15 InvalidMacPasskeyPublishableKeyError,
16 isMacPasskeySigningConfigurationError,
17 MissingMacPasskeyProvisioningProfileError,
13 renderMacPasskeyEntitlements,18 renderMacPasskeyEntitlements,
14 resolveClerkPasskeyNativeArtifacts,19 resolveClerkPasskeyNativeArtifacts,
15 resolveMacPasskeySigningConfiguration,20 resolveMacPasskeySigningConfiguration,
...@@ -214,23 +219,43 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -214,23 +219,43 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
214 });219 });
215220
216 it("rejects incomplete macOS passkey signing configuration", () => {221 it("rejects incomplete macOS passkey signing configuration", () => {
217 assert.throws(222 const captureError = (env: Readonly<Record<string, string | undefined>>) => {
218 () =>223 try {
219 resolveMacPasskeySigningConfiguration({224 resolveMacPasskeySigningConfiguration(env);
220 T3CODE_APPLE_TEAM_ID: "ABC1234567",225 } catch (error) {
221 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "example.clerk.accounts.dev",226 return error;
222 }),227 }
223 /T3CODE_MACOS_PROVISIONING_PROFILE/u,228 return assert.fail("Expected passkey signing configuration to fail.");
224 );229 };
225 assert.throws(230
226 () =>231 const missingProfileError = captureError({
227 resolveMacPasskeySigningConfiguration({232 T3CODE_APPLE_TEAM_ID: "ABC1234567",
228 T3CODE_APPLE_TEAM_ID: "ABC1234567",233 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "example.clerk.accounts.dev",
229 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",234 });
230 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "https://example.clerk.accounts.dev/path",235 assert.instanceOf(missingProfileError, MissingMacPasskeyProvisioningProfileError);
231 }),236 assert.equal(
232 /Invalid passkey RP domain/u,237 missingProfileError.message,
238 "T3CODE_MACOS_PROVISIONING_PROFILE must point to an Associated Domains provisioning profile.",
233 );239 );
240
241 const unsafeDomain =
242 "https://domain-user:domain-secret@example.clerk.accounts.dev/path?token=query-secret";
243 const invalidDomainError = captureError({
244 T3CODE_APPLE_TEAM_ID: "ABC1234567",
245 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
246 T3CODE_CLERK_PASSKEY_RP_DOMAINS: unsafeDomain,
247 });
248 assert.instanceOf(invalidDomainError, InvalidMacPasskeyRpDomainError);
249 assert.equal(invalidDomainError.reason, "scheme-not-allowed");
250 assert.equal(invalidDomainError.inputLength, unsafeDomain.length);
251 assert.equal(invalidDomainError.message, "Invalid passkey RP domain (scheme-not-allowed).");
252 assert.notProperty(invalidDomainError, "domain");
253 assert.notProperty(invalidDomainError, "cause");
254 const serializedInvalidDomainError = JSON.stringify(invalidDomainError);
255 assert.notInclude(serializedInvalidDomainError, unsafeDomain);
256 assert.notInclude(serializedInvalidDomainError, "domain-user");
257 assert.notInclude(serializedInvalidDomainError, "domain-secret");
258 assert.notInclude(serializedInvalidDomainError, "query-secret");
234 assert.throws(259 assert.throws(
235 () =>260 () =>
236 resolveMacPasskeySigningConfiguration({261 resolveMacPasskeySigningConfiguration({
...@@ -240,6 +265,38 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -240,6 +265,38 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
240 }),265 }),
241 /Invalid passkey RP domain/u,266 /Invalid passkey RP domain/u,
242 );267 );
268 const invalidPublishableKeyError = captureError({
269 T3CODE_APPLE_TEAM_ID: "ABC1234567",
270 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
271 T3CODE_CLERK_PUBLISHABLE_KEY: "pk_test_%",
272 });
273 assert.instanceOf(invalidPublishableKeyError, InvalidMacPasskeyPublishableKeyError);
274 assert.ok(invalidPublishableKeyError.cause);
275 assert.equal(invalidPublishableKeyError.message, "T3CODE_CLERK_PUBLISHABLE_KEY is invalid.");
276 assert.notProperty(invalidPublishableKeyError, "publishableKey");
277 assert.notInclude(invalidPublishableKeyError.message, "pk_test_%");
278 });
279
280 it("preserves known passkey signing configuration errors at the build boundary", () => {
281 const decodingCause = new Error("publishable-key-decode-failed");
282 const knownError = new InvalidMacPasskeyPublishableKeyError({ cause: decodingCause });
283 const error = BuildScriptError.fromMacPasskeySigningConfiguration(knownError);
284
285 assert.strictEqual(error, knownError);
286 assert.instanceOf(error, InvalidMacPasskeyPublishableKeyError);
287 assert.strictEqual(error.cause, decodingCause);
288 assert.isTrue(isMacPasskeySigningConfigurationError(error));
289 });
290
291 it("wraps unknown passkey signing configuration defects without copying cause text", () => {
292 const secret = "pk_test_do-not-retain";
293 const cause = new Error(secret);
294 const error = BuildScriptError.fromMacPasskeySigningConfiguration(cause);
295
296 assert.instanceOf(error, BuildScriptError);
297 assert.strictEqual(error.cause, cause);
298 assert.equal(error.message, "Failed to resolve macOS passkey signing configuration.");
299 assert.notInclude(error.message, secret);
243 });300 });
244301
245 it.effect("adds passkey entitlements and both renderer protocols to signed macOS builds", () =>302 it.effect("adds passkey entitlements and both renderer protocols to signed macOS builds", () =>
scripts/build-desktop-artifact.ts+140-31
...@@ -126,10 +126,21 @@ const getDefaultArch = Effect.fn("getDefaultArch")(function* (platform: typeof B...@@ -126,10 +126,21 @@ const getDefaultArch = Effect.fn("getDefaultArch")(function* (platform: typeof B
126 return yield* getDefaultBuildArch(platform, config);126 return yield* getDefaultBuildArch(platform, config);
127});127});
128128
129class BuildScriptError extends Data.TaggedError("BuildScriptError")<{129export class BuildScriptError extends Data.TaggedError("BuildScriptError")<{
130 readonly message: string;130 readonly message: string;
131 readonly cause?: unknown;131 readonly cause?: unknown;
132}> {}132}> {
133 static fromMacPasskeySigningConfiguration(
134 cause: unknown,
135 ): MacPasskeySigningConfigurationError | BuildScriptError {
136 return isMacPasskeySigningConfigurationError(cause)
137 ? cause
138 : new BuildScriptError({
139 message: "Failed to resolve macOS passkey signing configuration.",
140 cause,
141 });
142 }
143}
133144
134const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>145const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
135 stream.pipe(146 stream.pipe(
...@@ -306,26 +317,128 @@ export interface MacPasskeySigningConfiguration {...@@ -306,26 +317,128 @@ export interface MacPasskeySigningConfiguration {
306 readonly provisioningProfilePath: string;317 readonly provisioningProfilePath: string;
307}318}
308319
320export const InvalidMacPasskeyRpDomainReason = Schema.Literals([
321 "empty",
322 "scheme-not-allowed",
323 "parse-failed",
324 "credentials-not-allowed",
325 "port-not-allowed",
326 "path-not-allowed",
327 "query-not-allowed",
328 "fragment-not-allowed",
329 "hostname-mismatch",
330]);
331export type InvalidMacPasskeyRpDomainReason = typeof InvalidMacPasskeyRpDomainReason.Type;
332
333export class InvalidMacPasskeyRpDomainError extends Schema.TaggedErrorClass<InvalidMacPasskeyRpDomainError>()(
334 "InvalidMacPasskeyRpDomainError",
335 {
336 reason: InvalidMacPasskeyRpDomainReason,
337 inputLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
338 cause: Schema.optionalKey(Schema.Defect()),
339 },
340) {
341 override get message(): string {
342 return `Invalid passkey RP domain (${this.reason}).`;
343 }
344}
345
346export class InvalidAppleTeamIdError extends Schema.TaggedErrorClass<InvalidAppleTeamIdError>()(
347 "InvalidAppleTeamIdError",
348 {
349 teamId: Schema.String,
350 },
351) {
352 override get message(): string {
353 return `T3CODE_APPLE_TEAM_ID '${this.teamId}' must be a 10-character Apple Developer Team ID.`;
354 }
355}
356
357export class MissingMacPasskeyProvisioningProfileError extends Schema.TaggedErrorClass<MissingMacPasskeyProvisioningProfileError>()(
358 "MissingMacPasskeyProvisioningProfileError",
359 {},
360) {
361 override get message(): string {
362 return "T3CODE_MACOS_PROVISIONING_PROFILE must point to an Associated Domains provisioning profile.";
363 }
364}
365
366export class MissingMacPasskeyDomainConfigurationError extends Schema.TaggedErrorClass<MissingMacPasskeyDomainConfigurationError>()(
367 "MissingMacPasskeyDomainConfigurationError",
368 {},
369) {
370 override get message(): string {
371 return "T3CODE_CLERK_PUBLISHABLE_KEY or T3CODE_CLERK_PASSKEY_RP_DOMAINS is required for signed macOS passkey builds.";
372 }
373}
374
375export class InvalidMacPasskeyPublishableKeyError extends Schema.TaggedErrorClass<InvalidMacPasskeyPublishableKeyError>()(
376 "InvalidMacPasskeyPublishableKeyError",
377 {
378 cause: Schema.Defect(),
379 },
380) {
381 override get message(): string {
382 return "T3CODE_CLERK_PUBLISHABLE_KEY is invalid.";
383 }
384}
385
386export class MissingMacPasskeyRpDomainError extends Schema.TaggedErrorClass<MissingMacPasskeyRpDomainError>()(
387 "MissingMacPasskeyRpDomainError",
388 {},
389) {
390 override get message(): string {
391 return "At least one Clerk passkey RP domain is required.";
392 }
393}
394
395export const MacPasskeySigningConfigurationError = Schema.Union([
396 InvalidMacPasskeyRpDomainError,
397 InvalidAppleTeamIdError,
398 MissingMacPasskeyProvisioningProfileError,
399 MissingMacPasskeyDomainConfigurationError,
400 InvalidMacPasskeyPublishableKeyError,
401 MissingMacPasskeyRpDomainError,
402]);
403export type MacPasskeySigningConfigurationError = typeof MacPasskeySigningConfigurationError.Type;
404export const isMacPasskeySigningConfigurationError = Schema.is(MacPasskeySigningConfigurationError);
405
309function normalizePasskeyRpDomain(value: string): string {406function normalizePasskeyRpDomain(value: string): string {
310 const normalized = value.trim().toLowerCase();407 const normalized = value.trim().toLowerCase();
408 const inputLength = value.length;
409 if (normalized.length === 0) {
410 throw new InvalidMacPasskeyRpDomainError({ reason: "empty", inputLength });
411 }
412 if (/^[a-z][a-z\d+.-]*:\/\//u.test(normalized)) {
413 throw new InvalidMacPasskeyRpDomainError({
414 reason: "scheme-not-allowed",
415 inputLength,
416 });
417 }
418
311 let parsed: URL;419 let parsed: URL;
312 try {420 try {
313 parsed = new URL(`https://${normalized}`);421 parsed = new URL(`https://${normalized}`);
314 } catch {422 } catch (cause) {
315 throw new Error(`Invalid passkey RP domain: ${value}`);423 throw new InvalidMacPasskeyRpDomainError({ reason: "parse-failed", inputLength, cause });
316 }424 }
317425
318 if (426 let reason: InvalidMacPasskeyRpDomainReason | undefined;
319 normalized.length === 0 ||427 if (parsed.username.length > 0 || parsed.password.length > 0) {
320 parsed.host !== normalized ||428 reason = "credentials-not-allowed";
321 parsed.username.length > 0 ||429 } else if (parsed.port.length > 0) {
322 parsed.password.length > 0 ||430 reason = "port-not-allowed";
323 parsed.port.length > 0 ||431 } else if (parsed.pathname !== "/") {
324 parsed.pathname !== "/" ||432 reason = "path-not-allowed";
325 parsed.search.length > 0 ||433 } else if (parsed.search.length > 0) {
326 parsed.hash.length > 0434 reason = "query-not-allowed";
327 ) {435 } else if (parsed.hash.length > 0) {
328 throw new Error(`Invalid passkey RP domain: ${value}`);436 reason = "fragment-not-allowed";
437 } else if (parsed.host !== normalized) {
438 reason = "hostname-mismatch";
439 }
440 if (reason) {
441 throw new InvalidMacPasskeyRpDomainError({ reason, inputLength });
329 }442 }
330443
331 return parsed.hostname;444 return parsed.hostname;
...@@ -336,14 +449,12 @@ export function resolveMacPasskeySigningConfiguration(...@@ -336,14 +449,12 @@ export function resolveMacPasskeySigningConfiguration(
336): MacPasskeySigningConfiguration {449): MacPasskeySigningConfiguration {
337 const teamId = env.T3CODE_APPLE_TEAM_ID?.trim().toUpperCase() ?? "";450 const teamId = env.T3CODE_APPLE_TEAM_ID?.trim().toUpperCase() ?? "";
338 if (!APPLE_TEAM_ID_PATTERN.test(teamId)) {451 if (!APPLE_TEAM_ID_PATTERN.test(teamId)) {
339 throw new Error("T3CODE_APPLE_TEAM_ID must be a 10-character Apple Developer Team ID.");452 throw new InvalidAppleTeamIdError({ teamId });
340 }453 }
341454
342 const provisioningProfilePath = env.T3CODE_MACOS_PROVISIONING_PROFILE?.trim() ?? "";455 const provisioningProfilePath = env.T3CODE_MACOS_PROVISIONING_PROFILE?.trim() ?? "";
343 if (provisioningProfilePath.length === 0) {456 if (provisioningProfilePath.length === 0) {
344 throw new Error(457 throw new MissingMacPasskeyProvisioningProfileError();
345 "T3CODE_MACOS_PROVISIONING_PROFILE must point to an Associated Domains provisioning profile.",
346 );
347 }458 }
348459
349 const configuredRpDomains = env.T3CODE_CLERK_PASSKEY_RP_DOMAINS?.trim();460 const configuredRpDomains = env.T3CODE_CLERK_PASSKEY_RP_DOMAINS?.trim();
...@@ -353,18 +464,20 @@ export function resolveMacPasskeySigningConfiguration(...@@ -353,18 +464,20 @@ export function resolveMacPasskeySigningConfiguration(
353 } else {464 } else {
354 const publishableKey = env.T3CODE_CLERK_PUBLISHABLE_KEY?.trim();465 const publishableKey = env.T3CODE_CLERK_PUBLISHABLE_KEY?.trim();
355 if (!publishableKey) {466 if (!publishableKey) {
356 throw new Error(467 throw new MissingMacPasskeyDomainConfigurationError();
357 "T3CODE_CLERK_PUBLISHABLE_KEY or T3CODE_CLERK_PASSKEY_RP_DOMAINS is required for signed macOS passkey builds.",
358 );
359 }468 }
360 rpDomains = [469 let hostname: string;
361 normalizePasskeyRpDomain(clerkFrontendApiHostnameFromPublishableKey(publishableKey)),470 try {
362 ];471 hostname = clerkFrontendApiHostnameFromPublishableKey(publishableKey);
472 } catch (cause) {
473 throw new InvalidMacPasskeyPublishableKeyError({ cause });
474 }
475 rpDomains = [normalizePasskeyRpDomain(hostname)];
363 }476 }
364477
365 const uniqueRpDomains = [...new Set(rpDomains)];478 const uniqueRpDomains = [...new Set(rpDomains)];
366 if (uniqueRpDomains.length === 0) {479 if (uniqueRpDomains.length === 0) {
367 throw new Error("At least one Clerk passkey RP domain is required.");480 throw new MissingMacPasskeyRpDomainError();
368 }481 }
369482
370 return {483 return {
...@@ -1150,11 +1263,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (...@@ -1150,11 +1263,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
1150 options.platform === "mac" && options.signed1263 options.platform === "mac" && options.signed
1151 ? yield* Effect.try({1264 ? yield* Effect.try({
1152 try: () => resolveMacPasskeySigningConfiguration(loadRepoEnv({ repoRoot })),1265 try: () => resolveMacPasskeySigningConfiguration(loadRepoEnv({ repoRoot })),
1153 catch: (cause) =>1266 catch: BuildScriptError.fromMacPasskeySigningConfiguration,
1154 new BuildScriptError({
1155 message: cause instanceof Error ? cause.message : String(cause),
1156 cause,
1157 }),
1158 })1267 })
1159 : undefined;1268 : undefined;
1160 const macPasskeySigning = configuredMacPasskeySigning1269 const macPasskeySigning = configuredMacPasskeySigning