| ... | ... | @@ -126,10 +126,21 @@ const getDefaultArch = Effect.fn("getDefaultArch")(function* (platform: typeof B |
| 126 | 126 | return yield* getDefaultBuildArch(platform, config); |
| 127 | 127 | }); |
| 128 | 128 | |
| 129 | | class BuildScriptError extends Data.TaggedError("BuildScriptError")<{ |
| 129 | export class BuildScriptError extends Data.TaggedError("BuildScriptError")<{ |
| 130 | 130 | readonly message: string; |
| 131 | 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 | } |
| 133 | 144 | |
| 134 | 145 | const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => |
| 135 | 146 | stream.pipe( |
| ... | ... | @@ -306,26 +317,128 @@ export interface MacPasskeySigningConfiguration { |
| 306 | 317 | readonly provisioningProfilePath: string; |
| 307 | 318 | } |
| 308 | 319 | |
| 320 | export 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 | ]); |
| 331 | export type InvalidMacPasskeyRpDomainReason = typeof InvalidMacPasskeyRpDomainReason.Type; |
| 332 | |
| 333 | export 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 | |
| 346 | export 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 | |
| 357 | export 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 | |
| 366 | export 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 | |
| 375 | export 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 | |
| 386 | export 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 | |
| 395 | export const MacPasskeySigningConfigurationError = Schema.Union([ |
| 396 | InvalidMacPasskeyRpDomainError, |
| 397 | InvalidAppleTeamIdError, |
| 398 | MissingMacPasskeyProvisioningProfileError, |
| 399 | MissingMacPasskeyDomainConfigurationError, |
| 400 | InvalidMacPasskeyPublishableKeyError, |
| 401 | MissingMacPasskeyRpDomainError, |
| 402 | ]); |
| 403 | export type MacPasskeySigningConfigurationError = typeof MacPasskeySigningConfigurationError.Type; |
| 404 | export const isMacPasskeySigningConfigurationError = Schema.is(MacPasskeySigningConfigurationError); |
| 405 | |
| 309 | 406 | function normalizePasskeyRpDomain(value: string): string { |
| 310 | 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 | 419 | let parsed: URL; |
| 312 | 420 | try { |
| 313 | 421 | 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 }); |
| 316 | 424 | } |
| 317 | 425 | |
| 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 }); |
| 329 | 442 | } |
| 330 | 443 | |
| 331 | 444 | return parsed.hostname; |
| ... | ... | @@ -336,14 +449,12 @@ export function resolveMacPasskeySigningConfiguration( |
| 336 | 449 | ): MacPasskeySigningConfiguration { |
| 337 | 450 | const teamId = env.T3CODE_APPLE_TEAM_ID?.trim().toUpperCase() ?? ""; |
| 338 | 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 | } |
| 341 | 454 | |
| 342 | 455 | const provisioningProfilePath = env.T3CODE_MACOS_PROVISIONING_PROFILE?.trim() ?? ""; |
| 343 | 456 | 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(); |
| 347 | 458 | } |
| 348 | 459 | |
| 349 | 460 | const configuredRpDomains = env.T3CODE_CLERK_PASSKEY_RP_DOMAINS?.trim(); |
| ... | ... | @@ -353,18 +464,20 @@ export function resolveMacPasskeySigningConfiguration( |
| 353 | 464 | } else { |
| 354 | 465 | const publishableKey = env.T3CODE_CLERK_PUBLISHABLE_KEY?.trim(); |
| 355 | 466 | 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(); |
| 359 | 468 | } |
| 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)]; |
| 363 | 476 | } |
| 364 | 477 | |
| 365 | 478 | const uniqueRpDomains = [...new Set(rpDomains)]; |
| 366 | 479 | if (uniqueRpDomains.length === 0) { |
| 367 | | throw new Error("At least one Clerk passkey RP domain is required."); |
| 480 | throw new MissingMacPasskeyRpDomainError(); |
| 368 | 481 | } |
| 369 | 482 | |
| 370 | 483 | return { |
| ... | ... | @@ -1150,11 +1263,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( |
| 1150 | 1263 | options.platform === "mac" && options.signed |
| 1151 | 1264 | ? yield* Effect.try({ |
| 1152 | 1265 | 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, |
| 1158 | 1267 | }) |
| 1159 | 1268 | : undefined; |
| 1160 | 1269 | const macPasskeySigning = configuredMacPasskeySigning |