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";
66import * as Option from "effect/Option";
77
88import {
9 BuildScriptError,
910 createStageWorkspaceConfig,
1011 createStagePnpmConfig,
1112 createBuildConfig,
1213 DESKTOP_ASAR_UNPACK,
14 InvalidMacPasskeyRpDomainError,
15 InvalidMacPasskeyPublishableKeyError,
16 isMacPasskeySigningConfigurationError,
17 MissingMacPasskeyProvisioningProfileError,
1318 renderMacPasskeyEntitlements,
1419 resolveClerkPasskeyNativeArtifacts,
1520 resolveMacPasskeySigningConfiguration,
......@@ -214,23 +219,43 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
214219 });
215220
216221 it("rejects incomplete macOS passkey signing configuration", () => {
217 assert.throws(
218 () =>
219 resolveMacPasskeySigningConfiguration({
220 T3CODE_APPLE_TEAM_ID: "ABC1234567",
221 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "example.clerk.accounts.dev",
222 }),
223 /T3CODE_MACOS_PROVISIONING_PROFILE/u,
224 );
225 assert.throws(
226 () =>
227 resolveMacPasskeySigningConfiguration({
228 T3CODE_APPLE_TEAM_ID: "ABC1234567",
229 T3CODE_MACOS_PROVISIONING_PROFILE: "/tmp/t3code.provisionprofile",
230 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "https://example.clerk.accounts.dev/path",
231 }),
232 /Invalid passkey RP domain/u,
222 const captureError = (env: Readonly<Record<string, string | undefined>>) => {
223 try {
224 resolveMacPasskeySigningConfiguration(env);
225 } catch (error) {
226 return error;
227 }
228 return assert.fail("Expected passkey signing configuration to fail.");
229 };
230
231 const missingProfileError = captureError({
232 T3CODE_APPLE_TEAM_ID: "ABC1234567",
233 T3CODE_CLERK_PASSKEY_RP_DOMAINS: "example.clerk.accounts.dev",
234 });
235 assert.instanceOf(missingProfileError, MissingMacPasskeyProvisioningProfileError);
236 assert.equal(
237 missingProfileError.message,
238 "T3CODE_MACOS_PROVISIONING_PROFILE must point to an Associated Domains provisioning profile.",
233239 );
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");
234259 assert.throws(
235260 () =>
236261 resolveMacPasskeySigningConfiguration({
......@@ -240,6 +265,38 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
240265 }),
241266 /Invalid passkey RP domain/u,
242267 );
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);
243300 });
244301
245302 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
126126 return yield* getDefaultBuildArch(platform, config);
127127});
128128
129class BuildScriptError extends Data.TaggedError("BuildScriptError")<{
129export class BuildScriptError extends Data.TaggedError("BuildScriptError")<{
130130 readonly message: string;
131131 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
134145const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
135146 stream.pipe(
......@@ -306,26 +317,128 @@ export interface MacPasskeySigningConfiguration {
306317 readonly provisioningProfilePath: string;
307318}
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
309406function normalizePasskeyRpDomain(value: string): string {
310407 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
311419 let parsed: URL;
312420 try {
313421 parsed = new URL(`https://${normalized}`);
314 } catch {
315 throw new Error(`Invalid passkey RP domain: ${value}`);
422 } catch (cause) {
423 throw new InvalidMacPasskeyRpDomainError({ reason: "parse-failed", inputLength, cause });
316424 }
317425
318 if (
319 normalized.length === 0 ||
320 parsed.host !== normalized ||
321 parsed.username.length > 0 ||
322 parsed.password.length > 0 ||
323 parsed.port.length > 0 ||
324 parsed.pathname !== "/" ||
325 parsed.search.length > 0 ||
326 parsed.hash.length > 0
327 ) {
328 throw new Error(`Invalid passkey RP domain: ${value}`);
426 let reason: InvalidMacPasskeyRpDomainReason | undefined;
427 if (parsed.username.length > 0 || parsed.password.length > 0) {
428 reason = "credentials-not-allowed";
429 } else if (parsed.port.length > 0) {
430 reason = "port-not-allowed";
431 } else if (parsed.pathname !== "/") {
432 reason = "path-not-allowed";
433 } else if (parsed.search.length > 0) {
434 reason = "query-not-allowed";
435 } else if (parsed.hash.length > 0) {
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 });
329442 }
330443
331444 return parsed.hostname;
......@@ -336,14 +449,12 @@ export function resolveMacPasskeySigningConfiguration(
336449): MacPasskeySigningConfiguration {
337450 const teamId = env.T3CODE_APPLE_TEAM_ID?.trim().toUpperCase() ?? "";
338451 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 });
340453 }
341454
342455 const provisioningProfilePath = env.T3CODE_MACOS_PROVISIONING_PROFILE?.trim() ?? "";
343456 if (provisioningProfilePath.length === 0) {
344 throw new Error(
345 "T3CODE_MACOS_PROVISIONING_PROFILE must point to an Associated Domains provisioning profile.",
346 );
457 throw new MissingMacPasskeyProvisioningProfileError();
347458 }
348459
349460 const configuredRpDomains = env.T3CODE_CLERK_PASSKEY_RP_DOMAINS?.trim();
......@@ -353,18 +464,20 @@ export function resolveMacPasskeySigningConfiguration(
353464 } else {
354465 const publishableKey = env.T3CODE_CLERK_PUBLISHABLE_KEY?.trim();
355466 if (!publishableKey) {
356 throw new Error(
357 "T3CODE_CLERK_PUBLISHABLE_KEY or T3CODE_CLERK_PASSKEY_RP_DOMAINS is required for signed macOS passkey builds.",
358 );
467 throw new MissingMacPasskeyDomainConfigurationError();
359468 }
360 rpDomains = [
361 normalizePasskeyRpDomain(clerkFrontendApiHostnameFromPublishableKey(publishableKey)),
362 ];
469 let hostname: string;
470 try {
471 hostname = clerkFrontendApiHostnameFromPublishableKey(publishableKey);
472 } catch (cause) {
473 throw new InvalidMacPasskeyPublishableKeyError({ cause });
474 }
475 rpDomains = [normalizePasskeyRpDomain(hostname)];
363476 }
364477
365478 const uniqueRpDomains = [...new Set(rpDomains)];
366479 if (uniqueRpDomains.length === 0) {
367 throw new Error("At least one Clerk passkey RP domain is required.");
480 throw new MissingMacPasskeyRpDomainError();
368481 }
369482
370483 return {
......@@ -1150,11 +1263,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
11501263 options.platform === "mac" && options.signed
11511264 ? yield* Effect.try({
11521265 try: () => resolveMacPasskeySigningConfiguration(loadRepoEnv({ repoRoot })),
1153 catch: (cause) =>
1154 new BuildScriptError({
1155 message: cause instanceof Error ? cause.message : String(cause),
1156 cause,
1157 }),
1266 catch: BuildScriptError.fromMacPasskeySigningConfiguration,
11581267 })
11591268 : undefined;
11601269 const macPasskeySigning = configuredMacPasskeySigning