@better-update/cli 0.26.1 → 0.28.0
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/dist/index.mjs +1189 -185
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region package.json
|
|
37
|
-
var version = "0.
|
|
37
|
+
var version = "0.28.0";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/lib/interactive-mode.ts
|
|
@@ -67,6 +67,7 @@ const cookieSecurity = HttpApiSecurity.apiKey({
|
|
|
67
67
|
key: "__Secure-better-auth.session_token",
|
|
68
68
|
in: "cookie"
|
|
69
69
|
});
|
|
70
|
+
/** @effect-expect-leaking HttpServerRequest | ParsedSearchParams | RouteContext */
|
|
70
71
|
var Authentication = class extends HttpApiMiddleware.Tag()("api/Authentication", {
|
|
71
72
|
failure: Schema.Union(Unauthorized, Forbidden),
|
|
72
73
|
provides: AuthContext,
|
|
@@ -526,6 +527,49 @@ var AndroidUploadKeystoresGroup = class extends HttpApiGroup.make("androidUpload
|
|
|
526
527
|
description: "Manage Android signing keystores"
|
|
527
528
|
})) {};
|
|
528
529
|
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region ../../packages/api/src/domain/api-key.ts
|
|
532
|
+
var ApiKey = class extends Schema.Class("ApiKey")({
|
|
533
|
+
id: Id,
|
|
534
|
+
name: Schema.NullOr(Schema.String),
|
|
535
|
+
start: Schema.NullOr(Schema.String),
|
|
536
|
+
prefix: Schema.NullOr(Schema.String),
|
|
537
|
+
enabled: Schema.Boolean,
|
|
538
|
+
createdAt: DateTimeString,
|
|
539
|
+
expiresAt: Schema.NullOr(DateTimeString)
|
|
540
|
+
}) {};
|
|
541
|
+
var CreatedApiKey = class extends Schema.Class("CreatedApiKey")({
|
|
542
|
+
id: Id,
|
|
543
|
+
name: Schema.NullOr(Schema.String),
|
|
544
|
+
start: Schema.NullOr(Schema.String),
|
|
545
|
+
prefix: Schema.NullOr(Schema.String),
|
|
546
|
+
enabled: Schema.Boolean,
|
|
547
|
+
createdAt: DateTimeString,
|
|
548
|
+
expiresAt: Schema.NullOr(DateTimeString),
|
|
549
|
+
key: Schema.String
|
|
550
|
+
}) {};
|
|
551
|
+
const CreateApiKeyBody = Schema.Struct({
|
|
552
|
+
name: Name120,
|
|
553
|
+
expiresInDays: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive()))
|
|
554
|
+
});
|
|
555
|
+
const ApiKeyList = Schema.Struct({ items: Schema.Array(ApiKey) });
|
|
556
|
+
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region ../../packages/api/src/groups/api-keys.ts
|
|
559
|
+
var ApiKeysGroup = class extends HttpApiGroup.make("api-keys").add(HttpApiEndpoint.get("list", "/api/api-keys").addSuccess(ApiKeyList).annotateContext(OpenApi.annotations({
|
|
560
|
+
title: "List API keys",
|
|
561
|
+
description: "List the active organization's API keys (hashed secret never exposed; only the `start` prefix for identification)"
|
|
562
|
+
}))).add(HttpApiEndpoint.post("create", "/api/api-keys").setPayload(CreateApiKeyBody).addSuccess(CreatedApiKey, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
563
|
+
title: "Create API key",
|
|
564
|
+
description: "Mint a new API key for the active organization. The plaintext key is returned ONCE; only its hash is stored"
|
|
565
|
+
}))).add(HttpApiEndpoint.del("revoke")`/api/api-keys/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
566
|
+
title: "Revoke API key",
|
|
567
|
+
description: "Delete an API key by id (org-scoped; no cross-organization deletes)"
|
|
568
|
+
}))).addError(NotFound).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
569
|
+
title: "API Keys",
|
|
570
|
+
description: "IAM-gated organization API key mint / list / revoke"
|
|
571
|
+
})) {};
|
|
572
|
+
|
|
529
573
|
//#endregion
|
|
530
574
|
//#region ../../packages/api/src/domain/apple-team.ts
|
|
531
575
|
const AppleTeamType = Schema.Literal("IN_HOUSE", "COMPANY_ORGANIZATION", "INDIVIDUAL");
|
|
@@ -860,7 +904,7 @@ var AssetsGroup = class extends HttpApiGroup.make("assets").add(HttpApiEndpoint.
|
|
|
860
904
|
|
|
861
905
|
//#endregion
|
|
862
906
|
//#region ../../packages/api/src/domain/audit-log.ts
|
|
863
|
-
const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook", "iosAppMetadata", "submission", "vaultAccess");
|
|
907
|
+
const AuditLogResourceType = Schema.Literal("project", "branch", "channel", "update", "build", "appleCredential", "androidCredential", "iosBundleConfiguration", "envVar", "device", "webhook", "iosAppMetadata", "submission", "vaultAccess", "policy", "group", "policyAttachment", "apiKey", "invitation", "member", "organization");
|
|
864
908
|
const AuditLogSource = Schema.Literal("session", "api-key");
|
|
865
909
|
var AuditLog = class extends Schema.Class("AuditLog")({
|
|
866
910
|
id: Id,
|
|
@@ -1656,6 +1700,97 @@ var GoogleServiceAccountKeysGroup = class extends HttpApiGroup.make("googleServi
|
|
|
1656
1700
|
description: "Manage Google Play + FCM service account JSON keys"
|
|
1657
1701
|
})) {};
|
|
1658
1702
|
|
|
1703
|
+
//#endregion
|
|
1704
|
+
//#region ../../packages/api/src/domain/group.ts
|
|
1705
|
+
var Group = class extends Schema.Class("Group")({
|
|
1706
|
+
id: Id,
|
|
1707
|
+
organizationId: Id,
|
|
1708
|
+
name: Schema.NonEmptyString,
|
|
1709
|
+
description: Schema.NullOr(Schema.String),
|
|
1710
|
+
createdAt: DateTimeString,
|
|
1711
|
+
updatedAt: Schema.NullOr(DateTimeString)
|
|
1712
|
+
}) {};
|
|
1713
|
+
const CreateGroupBody = Schema.Struct({
|
|
1714
|
+
name: Schema.NonEmptyString,
|
|
1715
|
+
description: Schema.optional(Schema.String)
|
|
1716
|
+
});
|
|
1717
|
+
const UpdateGroupBody = Schema.Struct({
|
|
1718
|
+
name: Schema.optional(Schema.NonEmptyString),
|
|
1719
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
1720
|
+
});
|
|
1721
|
+
var GroupMember = class extends Schema.Class("GroupMember")({
|
|
1722
|
+
memberId: Id,
|
|
1723
|
+
createdAt: DateTimeString
|
|
1724
|
+
}) {};
|
|
1725
|
+
const AddGroupMemberBody = Schema.Struct({ memberId: Id });
|
|
1726
|
+
|
|
1727
|
+
//#endregion
|
|
1728
|
+
//#region ../../packages/api/src/groups/groups.ts
|
|
1729
|
+
/** `:memberId` path parameter — the `member.id` of a group member. */
|
|
1730
|
+
const memberIdParam = HttpApiSchema.param("memberId", Id);
|
|
1731
|
+
var GroupsGroup = class extends HttpApiGroup.make("groups").add(HttpApiEndpoint.get("list", "/api/groups").addSuccess(Schema.Struct({ items: Schema.Array(Group) })).annotateContext(OpenApi.annotations({
|
|
1732
|
+
title: "List groups",
|
|
1733
|
+
description: "List member groups in the active organization"
|
|
1734
|
+
}))).add(HttpApiEndpoint.post("create", "/api/groups").setPayload(CreateGroupBody).addSuccess(Group, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
1735
|
+
title: "Create group",
|
|
1736
|
+
description: "Create a member group for the active organization"
|
|
1737
|
+
}))).add(HttpApiEndpoint.get("get")`/api/groups/${idParam}`.addSuccess(Group).annotateContext(OpenApi.annotations({
|
|
1738
|
+
title: "Get group",
|
|
1739
|
+
description: "Fetch a single group by id"
|
|
1740
|
+
}))).add(HttpApiEndpoint.patch("update")`/api/groups/${idParam}`.setPayload(UpdateGroupBody).addSuccess(Group).annotateContext(OpenApi.annotations({
|
|
1741
|
+
title: "Update group",
|
|
1742
|
+
description: "Update a group's name or description"
|
|
1743
|
+
}))).add(HttpApiEndpoint.del("delete")`/api/groups/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
1744
|
+
title: "Delete group",
|
|
1745
|
+
description: "Delete a group and sweep its memberships and policy attachments"
|
|
1746
|
+
}))).add(HttpApiEndpoint.get("listMembers")`/api/groups/${idParam}/members`.addSuccess(Schema.Struct({ items: Schema.Array(GroupMember) })).annotateContext(OpenApi.annotations({
|
|
1747
|
+
title: "List group members",
|
|
1748
|
+
description: "List the members belonging to a group"
|
|
1749
|
+
}))).add(HttpApiEndpoint.post("addMember")`/api/groups/${idParam}/members`.setPayload(AddGroupMemberBody).addSuccess(GroupMember, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
1750
|
+
title: "Add group member",
|
|
1751
|
+
description: "Add an organization member to a group"
|
|
1752
|
+
}))).add(HttpApiEndpoint.del("removeMember")`/api/groups/${idParam}/members/${memberIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
1753
|
+
title: "Remove group member",
|
|
1754
|
+
description: "Remove a member from a group"
|
|
1755
|
+
}))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
1756
|
+
title: "Groups",
|
|
1757
|
+
description: "Member groups for collective policy attachment"
|
|
1758
|
+
})) {};
|
|
1759
|
+
|
|
1760
|
+
//#endregion
|
|
1761
|
+
//#region ../../packages/api/src/domain/invitation.ts
|
|
1762
|
+
var Invitation = class extends Schema.Class("Invitation")({
|
|
1763
|
+
id: Id,
|
|
1764
|
+
email: Schema.String,
|
|
1765
|
+
role: Schema.NullOr(Schema.String),
|
|
1766
|
+
status: Schema.String,
|
|
1767
|
+
expiresAt: DateTimeString,
|
|
1768
|
+
createdAt: DateTimeString
|
|
1769
|
+
}) {};
|
|
1770
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
|
|
1771
|
+
const InvitableRole = Schema.Literal("member");
|
|
1772
|
+
const CreateInvitationBody = Schema.Struct({
|
|
1773
|
+
email: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(320), Schema.pattern(EMAIL_PATTERN)),
|
|
1774
|
+
role: Schema.optional(InvitableRole)
|
|
1775
|
+
});
|
|
1776
|
+
const InvitationList = Schema.Struct({ items: Schema.Array(Invitation) });
|
|
1777
|
+
|
|
1778
|
+
//#endregion
|
|
1779
|
+
//#region ../../packages/api/src/groups/invitations.ts
|
|
1780
|
+
var InvitationsGroup = class extends HttpApiGroup.make("invitations").add(HttpApiEndpoint.get("list", "/api/invitations").addSuccess(InvitationList).annotateContext(OpenApi.annotations({
|
|
1781
|
+
title: "List invitations",
|
|
1782
|
+
description: "List the active organization's invitations (all statuses, newest first)"
|
|
1783
|
+
}))).add(HttpApiEndpoint.post("create", "/api/invitations").setPayload(CreateInvitationBody).addSuccess(Invitation, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
1784
|
+
title: "Create invitation",
|
|
1785
|
+
description: "Invite a member to the active organization. Writes a pending `invitation` row (better-auth's accept-invitation consumes it) and sends the invite email"
|
|
1786
|
+
}))).add(HttpApiEndpoint.del("cancel")`/api/invitations/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
1787
|
+
title: "Cancel invitation",
|
|
1788
|
+
description: "Cancel a pending invitation by id (org-scoped). A canceled invitation can no longer be accepted"
|
|
1789
|
+
}))).addError(NotFound).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
1790
|
+
title: "Invitations",
|
|
1791
|
+
description: "IAM-gated organization invitation create / list / cancel"
|
|
1792
|
+
})) {};
|
|
1793
|
+
|
|
1659
1794
|
//#endregion
|
|
1660
1795
|
//#region ../../packages/api/src/domain/ios-app-metadata.ts
|
|
1661
1796
|
const AscAppId = Schema.String.pipe(Schema.pattern(/^[0-9]{1,30}$/u, { message: () => "ASC App ID must be 1-30 digits" }));
|
|
@@ -1792,7 +1927,13 @@ const Me = Schema.Struct({
|
|
|
1792
1927
|
/** Authentication source — "session" for browser + CLI sessions, "api-key" for API-key (CI) bearer tokens. */
|
|
1793
1928
|
source: Schema.Literal("session", "api-key"),
|
|
1794
1929
|
/** Email or descriptor identifying the actor — useful when `user` is null (api-key auth). */
|
|
1795
|
-
actorEmail: Schema.String
|
|
1930
|
+
actorEmail: Schema.String,
|
|
1931
|
+
/** Holds `invitation:create` on `org` — gates the Invite button. */
|
|
1932
|
+
canInviteMembers: Schema.Boolean,
|
|
1933
|
+
/** Holds `member:delete` on `org` — gates the per-member Remove action. */
|
|
1934
|
+
canRemoveMembers: Schema.Boolean,
|
|
1935
|
+
/** Holds `policy:update` on `org` — gates the per-member Manage-policies action. */
|
|
1936
|
+
canManagePolicies: Schema.Boolean
|
|
1796
1937
|
});
|
|
1797
1938
|
|
|
1798
1939
|
//#endregion
|
|
@@ -1805,6 +1946,16 @@ var MeGroup = class extends HttpApiGroup.make("me").add(HttpApiEndpoint.get("get
|
|
|
1805
1946
|
description: "Current authenticated actor information"
|
|
1806
1947
|
})) {};
|
|
1807
1948
|
|
|
1949
|
+
//#endregion
|
|
1950
|
+
//#region ../../packages/api/src/groups/members.ts
|
|
1951
|
+
var MembersGroup = class extends HttpApiGroup.make("members").add(HttpApiEndpoint.del("remove")`/api/members/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
1952
|
+
title: "Remove member",
|
|
1953
|
+
description: "Remove a member from the active organization by member id (org-scoped; no cross-organization removes). Rejects removing the last owner (409). Membership role is `owner | member`; admin/developer/viewer access comes from policy attachments, not the role"
|
|
1954
|
+
}))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
1955
|
+
title: "Members",
|
|
1956
|
+
description: "IAM-gated organization member removal"
|
|
1957
|
+
})) {};
|
|
1958
|
+
|
|
1808
1959
|
//#endregion
|
|
1809
1960
|
//#region ../../packages/api/src/groups/org-vault.ts
|
|
1810
1961
|
/** `:keyId` path parameter — a registered recipient's `user_encryption_keys.id`. */
|
|
@@ -1835,6 +1986,134 @@ var OrgVaultGroup = class extends HttpApiGroup.make("orgVault").add(HttpApiEndpo
|
|
|
1835
1986
|
description: "Manage the organization's end-to-end encrypted vault key wraps"
|
|
1836
1987
|
})) {};
|
|
1837
1988
|
|
|
1989
|
+
//#endregion
|
|
1990
|
+
//#region ../../packages/api/src/domain/organization.ts
|
|
1991
|
+
var Organization = class extends Schema.Class("Organization")({
|
|
1992
|
+
id: Id,
|
|
1993
|
+
name: Schema.String,
|
|
1994
|
+
slug: Schema.String
|
|
1995
|
+
}) {};
|
|
1996
|
+
const UpdateOrganizationBody = Schema.Struct({
|
|
1997
|
+
name: Schema.optional(Schema.String.pipe(Schema.minLength(1), Schema.maxLength(120))),
|
|
1998
|
+
slug: Schema.optional(Schema.String.pipe(Schema.minLength(1), Schema.maxLength(120)))
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
//#endregion
|
|
2002
|
+
//#region ../../packages/api/src/groups/organization.ts
|
|
2003
|
+
var OrganizationGroup = class extends HttpApiGroup.make("organization").add(HttpApiEndpoint.patch("update", "/api/organization").setPayload(UpdateOrganizationBody).addSuccess(Organization).annotateContext(OpenApi.annotations({
|
|
2004
|
+
title: "Update organization",
|
|
2005
|
+
description: "Rename / re-slug the active organization (IAM-gated by organization:update)"
|
|
2006
|
+
}))).addError(NotFound).addError(Conflict).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
2007
|
+
title: "Organization",
|
|
2008
|
+
description: "IAM-gated active-organization settings"
|
|
2009
|
+
})) {};
|
|
2010
|
+
|
|
2011
|
+
//#endregion
|
|
2012
|
+
//#region ../../packages/api/src/domain/policy.ts
|
|
2013
|
+
/** Whether a statement grants or denies the matched actions. */
|
|
2014
|
+
const PolicyEffect = Schema.Literal("allow", "deny");
|
|
2015
|
+
const PolicyStatement = Schema.Struct({
|
|
2016
|
+
effect: PolicyEffect,
|
|
2017
|
+
actions: Schema.Array(Schema.String).pipe(Schema.minItems(1)),
|
|
2018
|
+
resources: Schema.Array(Schema.String).pipe(Schema.minItems(1))
|
|
2019
|
+
});
|
|
2020
|
+
const PolicyDocument = Schema.Struct({ statements: Schema.Array(PolicyStatement) });
|
|
2021
|
+
var Policy = class extends Schema.Class("Policy")({
|
|
2022
|
+
id: Id,
|
|
2023
|
+
organizationId: Id,
|
|
2024
|
+
name: Schema.NonEmptyString,
|
|
2025
|
+
description: Schema.NullOr(Schema.String),
|
|
2026
|
+
document: PolicyDocument,
|
|
2027
|
+
createdAt: DateTimeString,
|
|
2028
|
+
updatedAt: Schema.NullOr(DateTimeString)
|
|
2029
|
+
}) {};
|
|
2030
|
+
const CreatePolicyBody = Schema.Struct({
|
|
2031
|
+
name: Schema.NonEmptyString,
|
|
2032
|
+
description: Schema.optional(Schema.String),
|
|
2033
|
+
document: PolicyDocument
|
|
2034
|
+
});
|
|
2035
|
+
const UpdatePolicyBody = Schema.Struct({
|
|
2036
|
+
name: Schema.optional(Schema.NonEmptyString),
|
|
2037
|
+
description: Schema.optional(Schema.NullOr(Schema.String)),
|
|
2038
|
+
document: Schema.optional(PolicyDocument)
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region ../../packages/api/src/groups/policies.ts
|
|
2043
|
+
var PoliciesGroup = class extends HttpApiGroup.make("policies").add(HttpApiEndpoint.get("list", "/api/policies").addSuccess(Schema.Struct({ items: Schema.Array(Policy) })).annotateContext(OpenApi.annotations({
|
|
2044
|
+
title: "List policies",
|
|
2045
|
+
description: "List policies in the active organization, merging the read-only managed presets (admin/developer/viewer) into the list"
|
|
2046
|
+
}))).add(HttpApiEndpoint.post("create", "/api/policies").setPayload(CreatePolicyBody).addSuccess(Policy, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
2047
|
+
title: "Create policy",
|
|
2048
|
+
description: "Create a named IAM policy document for the active organization"
|
|
2049
|
+
}))).add(HttpApiEndpoint.get("get")`/api/policies/${idParam}`.addSuccess(Policy).annotateContext(OpenApi.annotations({
|
|
2050
|
+
title: "Get policy",
|
|
2051
|
+
description: "Fetch a single policy by id, resolving real ids or managed:* preset ids"
|
|
2052
|
+
}))).add(HttpApiEndpoint.patch("update")`/api/policies/${idParam}`.setPayload(UpdatePolicyBody).addSuccess(Policy).annotateContext(OpenApi.annotations({
|
|
2053
|
+
title: "Update policy",
|
|
2054
|
+
description: "Update a policy's name, description, or document; managed:* ids are rejected"
|
|
2055
|
+
}))).add(HttpApiEndpoint.del("delete")`/api/policies/${idParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
2056
|
+
title: "Delete policy",
|
|
2057
|
+
description: "Delete a policy and sweep its attachments; managed:* ids are rejected"
|
|
2058
|
+
}))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
2059
|
+
title: "Policies",
|
|
2060
|
+
description: "IAM policy documents (named, reusable permission grants)"
|
|
2061
|
+
})) {};
|
|
2062
|
+
|
|
2063
|
+
//#endregion
|
|
2064
|
+
//#region ../../packages/api/src/domain/policy-attachment.ts
|
|
2065
|
+
/** Whether an attachment binds a policy to a member, a group, or an api-key. */
|
|
2066
|
+
const PrincipalType = Schema.Literal("member", "group", "apikey");
|
|
2067
|
+
var PolicyAttachment = class extends Schema.Class("PolicyAttachment")({
|
|
2068
|
+
id: Id,
|
|
2069
|
+
organizationId: Id,
|
|
2070
|
+
policyId: Schema.String,
|
|
2071
|
+
principalType: PrincipalType,
|
|
2072
|
+
principalId: Id,
|
|
2073
|
+
createdAt: DateTimeString
|
|
2074
|
+
}) {};
|
|
2075
|
+
const AttachPolicyBody = Schema.Struct({ policyId: Schema.String });
|
|
2076
|
+
|
|
2077
|
+
//#endregion
|
|
2078
|
+
//#region ../../packages/api/src/groups/policy-attachments.ts
|
|
2079
|
+
/**
|
|
2080
|
+
* `:policyId` path parameter — a real `policy.id` or a managed preset id. Managed
|
|
2081
|
+
* ids contain a colon (`managed:admin`); the single path segment matches it as-is,
|
|
2082
|
+
* and clients URL-encode the colon when building the path.
|
|
2083
|
+
*/
|
|
2084
|
+
const policyIdParam = HttpApiSchema.param("policyId", Schema.String);
|
|
2085
|
+
var PolicyAttachmentsGroup = class extends HttpApiGroup.make("policy-attachments").add(HttpApiEndpoint.get("listForMember")`/api/members/${idParam}/policies`.addSuccess(Schema.Struct({ items: Schema.Array(PolicyAttachment) })).annotateContext(OpenApi.annotations({
|
|
2086
|
+
title: "List member policy attachments",
|
|
2087
|
+
description: "List policies attached directly to an organization member"
|
|
2088
|
+
}))).add(HttpApiEndpoint.post("attachToMember")`/api/members/${idParam}/policies`.setPayload(AttachPolicyBody).addSuccess(PolicyAttachment, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
2089
|
+
title: "Attach policy to member",
|
|
2090
|
+
description: "Attach a policy (real or managed) directly to a member"
|
|
2091
|
+
}))).add(HttpApiEndpoint.del("detachFromMember")`/api/members/${idParam}/policies/${policyIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
2092
|
+
title: "Detach policy from member",
|
|
2093
|
+
description: "Remove a policy attachment from a member"
|
|
2094
|
+
}))).add(HttpApiEndpoint.get("listForGroup")`/api/groups/${idParam}/policies`.addSuccess(Schema.Struct({ items: Schema.Array(PolicyAttachment) })).annotateContext(OpenApi.annotations({
|
|
2095
|
+
title: "List group policy attachments",
|
|
2096
|
+
description: "List policies attached to a group"
|
|
2097
|
+
}))).add(HttpApiEndpoint.post("attachToGroup")`/api/groups/${idParam}/policies`.setPayload(AttachPolicyBody).addSuccess(PolicyAttachment, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
2098
|
+
title: "Attach policy to group",
|
|
2099
|
+
description: "Attach a policy (real or managed) to a group; members inherit it"
|
|
2100
|
+
}))).add(HttpApiEndpoint.del("detachFromGroup")`/api/groups/${idParam}/policies/${policyIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
2101
|
+
title: "Detach policy from group",
|
|
2102
|
+
description: "Remove a policy attachment from a group"
|
|
2103
|
+
}))).add(HttpApiEndpoint.get("listForApiKey")`/api/api-keys/${idParam}/policies`.addSuccess(Schema.Struct({ items: Schema.Array(PolicyAttachment) })).annotateContext(OpenApi.annotations({
|
|
2104
|
+
title: "List api-key policy attachments",
|
|
2105
|
+
description: "List policies attached to an api-key principal"
|
|
2106
|
+
}))).add(HttpApiEndpoint.post("attachToApiKey")`/api/api-keys/${idParam}/policies`.setPayload(AttachPolicyBody).addSuccess(PolicyAttachment, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
2107
|
+
title: "Attach policy to api-key",
|
|
2108
|
+
description: "Attach a policy (real or managed) to an api-key principal"
|
|
2109
|
+
}))).add(HttpApiEndpoint.del("detachFromApiKey")`/api/api-keys/${idParam}/policies/${policyIdParam}`.addSuccess(DeletedResult).annotateContext(OpenApi.annotations({
|
|
2110
|
+
title: "Detach policy from api-key",
|
|
2111
|
+
description: "Remove a policy attachment from an api-key principal"
|
|
2112
|
+
}))).addError(NotFound).addError(Conflict).addError(BadRequest).addError(Forbidden).annotateContext(OpenApi.annotations({
|
|
2113
|
+
title: "Policy Attachments",
|
|
2114
|
+
description: "Bindings of policies to member, group, and api-key principals"
|
|
2115
|
+
})) {};
|
|
2116
|
+
|
|
1838
2117
|
//#endregion
|
|
1839
2118
|
//#region ../../packages/api/src/domain/project.ts
|
|
1840
2119
|
var Project = class extends Schema.Class("Project")({
|
|
@@ -2155,7 +2434,7 @@ var WebhooksGroup = class extends HttpApiGroup.make("webhooks").add(HttpApiEndpo
|
|
|
2155
2434
|
|
|
2156
2435
|
//#endregion
|
|
2157
2436
|
//#region ../../packages/api/src/api.ts
|
|
2158
|
-
var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(EnvVarsGroup).add(FingerprintsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(IosAppMetadataGroup).add(SubmissionsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
|
|
2437
|
+
var ManagementApi = class extends HttpApi.make("management-api").add(ProjectsGroup).add(BranchesGroup).add(ChannelsGroup).add(UpdatesGroup).add(AssetsGroup).add(AnalyticsGroup).add(BuildsGroup).add(EnvVarsGroup).add(FingerprintsGroup).add(AuditLogsGroup).add(DevicesGroup).add(AppleTeamsGroup).add(AppleDistributionCertificatesGroup).add(ApplePushKeysGroup).add(AscApiKeysGroup).add(AppleProvisioningProfilesGroup).add(GoogleServiceAccountKeysGroup).add(IosBundleConfigurationsGroup).add(IosAppMetadataGroup).add(SubmissionsGroup).add(AndroidApplicationIdentifiersGroup).add(AndroidUploadKeystoresGroup).add(AndroidBuildCredentialsGroup).add(BuildCredentialsGroup).add(UserEncryptionKeysGroup).add(OrgVaultGroup).add(MeGroup).add(WebhooksGroup).add(PoliciesGroup).add(GroupsGroup).add(PolicyAttachmentsGroup).add(ApiKeysGroup).add(InvitationsGroup).add(MembersGroup).add(OrganizationGroup).add(AdminGroup).middleware(Authentication).annotateContext(OpenApi.annotations({
|
|
2159
2438
|
title: "Better Update Management API",
|
|
2160
2439
|
version: "1.0.0",
|
|
2161
2440
|
description: "Management API for OTA update publishing, deployment, and analytics"
|
|
@@ -2185,6 +2464,151 @@ var ProtocolApi = class extends HttpApi.make("protocol-api").add(ManifestGroup).
|
|
|
2185
2464
|
description: "Expo Updates protocol endpoints (unauthenticated)"
|
|
2186
2465
|
})) {};
|
|
2187
2466
|
|
|
2467
|
+
//#endregion
|
|
2468
|
+
//#region ../../packages/api/src/domain/policy-selector.ts
|
|
2469
|
+
/**
|
|
2470
|
+
* Pure, framework-agnostic validators for the IAM path-glob SELECTOR GRAMMAR and
|
|
2471
|
+
* the action-token shape. Shared by web / CLI / server so all three reject bad
|
|
2472
|
+
* input identically. This is INPUT-SHAPE validation only — it is distinct from
|
|
2473
|
+
* the server matching algorithm (`selectorMatches`), which is NOT duplicated here.
|
|
2474
|
+
*/
|
|
2475
|
+
/** A selector segment: `*` or a non-empty token of `[A-Za-z0-9._:-]`. */
|
|
2476
|
+
const SEGMENT_PATTERN = /^[A-Za-z0-9._:-]+$/u;
|
|
2477
|
+
/**
|
|
2478
|
+
* An action token of `"<word>:<word>"` or `"<word>:*"`, where a word is a
|
|
2479
|
+
* non-empty run of `[A-Za-z0-9_-]`. The standalone `"*"` token is handled
|
|
2480
|
+
* separately in `isValidActionTokenShape`.
|
|
2481
|
+
*/
|
|
2482
|
+
const ACTION_TOKEN_PATTERN = /^[A-Za-z0-9_-]+:(?:\*|[A-Za-z0-9_-]+)$/u;
|
|
2483
|
+
const isValidSegment = (segment) => segment === "*" || SEGMENT_PATTERN.test(segment);
|
|
2484
|
+
/**
|
|
2485
|
+
* A resource selector is valid when it is `"*"` OR is slash-joined segments where
|
|
2486
|
+
* each segment is `"*"` or a non-empty token of `[A-Za-z0-9._:-]`. Empty segments
|
|
2487
|
+
* (leading/trailing/double slashes) are rejected.
|
|
2488
|
+
*/
|
|
2489
|
+
const isValidSelector = (selector) => {
|
|
2490
|
+
if (selector === "*") return true;
|
|
2491
|
+
if (selector.length === 0) return false;
|
|
2492
|
+
return selector.split("/").every(isValidSegment);
|
|
2493
|
+
};
|
|
2494
|
+
/**
|
|
2495
|
+
* An action token is valid in SHAPE when it is `"*"`, `"<word>:<word>"`, or
|
|
2496
|
+
* `"<word>:*"`. The server still validates the token against the real
|
|
2497
|
+
* resource/action vocabulary — this only guards the grammar.
|
|
2498
|
+
*/
|
|
2499
|
+
const isValidActionTokenShape = (token) => token === "*" || ACTION_TOKEN_PATTERN.test(token);
|
|
2500
|
+
const ID = "@";
|
|
2501
|
+
const CANONICAL_TEMPLATES = [
|
|
2502
|
+
["org"],
|
|
2503
|
+
["project", ID],
|
|
2504
|
+
[
|
|
2505
|
+
"project",
|
|
2506
|
+
ID,
|
|
2507
|
+
"build"
|
|
2508
|
+
],
|
|
2509
|
+
[
|
|
2510
|
+
"project",
|
|
2511
|
+
ID,
|
|
2512
|
+
"build",
|
|
2513
|
+
ID
|
|
2514
|
+
],
|
|
2515
|
+
[
|
|
2516
|
+
"project",
|
|
2517
|
+
ID,
|
|
2518
|
+
"credential"
|
|
2519
|
+
],
|
|
2520
|
+
[
|
|
2521
|
+
"project",
|
|
2522
|
+
ID,
|
|
2523
|
+
"credential",
|
|
2524
|
+
ID
|
|
2525
|
+
],
|
|
2526
|
+
[
|
|
2527
|
+
"project",
|
|
2528
|
+
ID,
|
|
2529
|
+
"submission"
|
|
2530
|
+
],
|
|
2531
|
+
[
|
|
2532
|
+
"project",
|
|
2533
|
+
ID,
|
|
2534
|
+
"submission",
|
|
2535
|
+
ID
|
|
2536
|
+
],
|
|
2537
|
+
[
|
|
2538
|
+
"project",
|
|
2539
|
+
ID,
|
|
2540
|
+
"env",
|
|
2541
|
+
ID
|
|
2542
|
+
],
|
|
2543
|
+
[
|
|
2544
|
+
"project",
|
|
2545
|
+
ID,
|
|
2546
|
+
"env",
|
|
2547
|
+
ID,
|
|
2548
|
+
"envVar"
|
|
2549
|
+
],
|
|
2550
|
+
[
|
|
2551
|
+
"project",
|
|
2552
|
+
ID,
|
|
2553
|
+
"env",
|
|
2554
|
+
ID,
|
|
2555
|
+
"envVar",
|
|
2556
|
+
ID
|
|
2557
|
+
],
|
|
2558
|
+
[
|
|
2559
|
+
"project",
|
|
2560
|
+
ID,
|
|
2561
|
+
"channel",
|
|
2562
|
+
ID
|
|
2563
|
+
],
|
|
2564
|
+
[
|
|
2565
|
+
"project",
|
|
2566
|
+
ID,
|
|
2567
|
+
"channel",
|
|
2568
|
+
ID,
|
|
2569
|
+
"update"
|
|
2570
|
+
],
|
|
2571
|
+
[
|
|
2572
|
+
"project",
|
|
2573
|
+
ID,
|
|
2574
|
+
"channel",
|
|
2575
|
+
ID,
|
|
2576
|
+
"update",
|
|
2577
|
+
ID
|
|
2578
|
+
],
|
|
2579
|
+
[
|
|
2580
|
+
"project",
|
|
2581
|
+
ID,
|
|
2582
|
+
"channel",
|
|
2583
|
+
ID,
|
|
2584
|
+
"rollout"
|
|
2585
|
+
],
|
|
2586
|
+
[
|
|
2587
|
+
"project",
|
|
2588
|
+
ID,
|
|
2589
|
+
"channel",
|
|
2590
|
+
ID,
|
|
2591
|
+
"rollout",
|
|
2592
|
+
ID
|
|
2593
|
+
]
|
|
2594
|
+
];
|
|
2595
|
+
const matchesTemplate = (template, segments) => template.length === segments.length && template.every((slot, index) => {
|
|
2596
|
+
const segment = segments[index];
|
|
2597
|
+
return segment !== void 0 && (slot === ID || segment === slot || segment === "*");
|
|
2598
|
+
});
|
|
2599
|
+
/**
|
|
2600
|
+
* True when a (shape-valid) selector matches one of the canonical resource-path
|
|
2601
|
+
* templates the server can actually produce — so a typo'd or pluralised segment
|
|
2602
|
+
* (e.g. `"project/A/channels/X"`) is caught at policy-write time instead of being
|
|
2603
|
+
* stored as a silently inert policy that can never match. The standalone `"*"`
|
|
2604
|
+
* matches everything and is always canonical.
|
|
2605
|
+
*/
|
|
2606
|
+
const isCanonicalSelector = (selector) => {
|
|
2607
|
+
if (selector === "*") return true;
|
|
2608
|
+
const segments = selector.split("/");
|
|
2609
|
+
return CANONICAL_TEMPLATES.some((template) => matchesTemplate(template, segments));
|
|
2610
|
+
};
|
|
2611
|
+
|
|
2188
2612
|
//#endregion
|
|
2189
2613
|
//#region src/lib/exit-codes.ts
|
|
2190
2614
|
var AuthRequiredError = class extends Data.TaggedError("AuthRequiredError") {};
|
|
@@ -2221,8 +2645,8 @@ var FingerprintMismatchError = class extends Data.TaggedError("FingerprintMismat
|
|
|
2221
2645
|
|
|
2222
2646
|
//#endregion
|
|
2223
2647
|
//#region ../../packages/type-guards/src/index.ts
|
|
2224
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2225
|
-
const asRecord = (value) => isRecord(value) ? value : void 0;
|
|
2648
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2649
|
+
const asRecord = (value) => isRecord$1(value) ? value : void 0;
|
|
2226
2650
|
const toOptional = (value) => value ?? void 0;
|
|
2227
2651
|
const toDbNull = (value) => value ?? null;
|
|
2228
2652
|
const compact = (obj) => Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== void 0));
|
|
@@ -2280,7 +2704,7 @@ const AuthStoreLive = Layer.effect(AuthStore, Effect.gen(function* () {
|
|
|
2280
2704
|
try: () => JSON.parse(content),
|
|
2281
2705
|
catch: () => new AuthRequiredError({ message: "Corrupted auth file. Run `better-update login` to re-authenticate." })
|
|
2282
2706
|
});
|
|
2283
|
-
if (!isRecord(parsed)) return yield* new AuthRequiredError({ message: "Invalid auth file. Run `better-update login` to re-authenticate." });
|
|
2707
|
+
if (!isRecord$1(parsed)) return yield* new AuthRequiredError({ message: "Invalid auth file. Run `better-update login` to re-authenticate." });
|
|
2284
2708
|
const { token } = parsed;
|
|
2285
2709
|
if (typeof token !== "string") return yield* new AuthRequiredError({ message: "Invalid auth file. Run `better-update login` to re-authenticate." });
|
|
2286
2710
|
return token;
|
|
@@ -2308,13 +2732,13 @@ const ConfigStoreLive = Layer.effect(ConfigStore, Effect.gen(function* () {
|
|
|
2308
2732
|
const runtime = yield* CliRuntime;
|
|
2309
2733
|
const homeDirectory = yield* runtime.homeDirectory;
|
|
2310
2734
|
const configFile = path.join(homeDirectory, ".better-update", "config.json");
|
|
2311
|
-
const readConfig = fs.readFileString(configFile).pipe(Effect.
|
|
2735
|
+
const readConfig = fs.readFileString(configFile).pipe(Effect.orElseSucceed(() => ""), Effect.flatMap((content) => content.length === 0 ? Effect.void : Effect.try({
|
|
2312
2736
|
try: () => JSON.parse(content),
|
|
2313
2737
|
catch: (cause) => new ConfigStoreParseError({
|
|
2314
2738
|
message: "Config file contains invalid JSON",
|
|
2315
2739
|
cause
|
|
2316
2740
|
})
|
|
2317
|
-
}).pipe(Effect.map((parsed) => isRecord(parsed) ? parsed : void 0), Effect.
|
|
2741
|
+
}).pipe(Effect.map((parsed) => isRecord$1(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0))));
|
|
2318
2742
|
return {
|
|
2319
2743
|
getBaseUrl: Effect.gen(function* () {
|
|
2320
2744
|
const envUrl = yield* runtime.getEnv("BETTER_UPDATE_URL");
|
|
@@ -2537,10 +2961,10 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
|
|
|
2537
2961
|
const usernameFile = path.join(sessionDir, "apple-username.json");
|
|
2538
2962
|
return {
|
|
2539
2963
|
loadSession: Effect.gen(function* () {
|
|
2540
|
-
const content = yield* fs.readFileString(sessionFile).pipe(Effect.
|
|
2964
|
+
const content = yield* fs.readFileString(sessionFile).pipe(Effect.orElseSucceed(() => null));
|
|
2541
2965
|
if (!content) return null;
|
|
2542
2966
|
const parsed = safeJsonParse(content);
|
|
2543
|
-
if (!isRecord(parsed)) return null;
|
|
2967
|
+
if (!isRecord$1(parsed)) return null;
|
|
2544
2968
|
if (typeof parsed["username"] !== "string" || !parsed["cookies"]) return null;
|
|
2545
2969
|
return {
|
|
2546
2970
|
cookies: parsed["cookies"],
|
|
@@ -2555,10 +2979,10 @@ const AppleSessionStoreLive = Layer.effect(AppleSessionStore, Effect.gen(functio
|
|
|
2555
2979
|
}).pipe(Effect.mapError((cause) => new AppleAuthError$1({ message: `Failed to save Apple session: ${formatCause(cause)}` }))),
|
|
2556
2980
|
clearSession: fs.remove(sessionFile).pipe(Effect.catchAll(() => Effect.void)),
|
|
2557
2981
|
loadLastUsername: Effect.gen(function* () {
|
|
2558
|
-
const content = yield* fs.readFileString(usernameFile).pipe(Effect.
|
|
2982
|
+
const content = yield* fs.readFileString(usernameFile).pipe(Effect.orElseSucceed(() => null));
|
|
2559
2983
|
if (!content) return null;
|
|
2560
2984
|
const parsed = safeJsonParse(content);
|
|
2561
|
-
if (!isRecord(parsed) || typeof parsed["username"] !== "string") return null;
|
|
2985
|
+
if (!isRecord$1(parsed) || typeof parsed["username"] !== "string") return null;
|
|
2562
2986
|
return parsed["username"];
|
|
2563
2987
|
}),
|
|
2564
2988
|
saveLastUsername: (username) => Effect.gen(function* () {
|
|
@@ -2651,7 +3075,7 @@ const interactiveLogin = (appleUtils, options, cachedUsername) => Effect.gen(fun
|
|
|
2651
3075
|
const tryRestore = (appleUtils, store) => Effect.gen(function* () {
|
|
2652
3076
|
const stored = yield* store.loadSession;
|
|
2653
3077
|
if (stored === null) return null;
|
|
2654
|
-
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.
|
|
3078
|
+
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.orElseSucceed(() => null));
|
|
2655
3079
|
if (restored === null) return null;
|
|
2656
3080
|
return yield* resolveSessionTeam(appleUtils, restored);
|
|
2657
3081
|
});
|
|
@@ -2670,7 +3094,7 @@ const makeAppleAuthLive = (appleUtils = defaultAppleUtils) => Layer.effect(Apple
|
|
|
2670
3094
|
whoami: Effect.gen(function* () {
|
|
2671
3095
|
const stored = yield* store.loadSession;
|
|
2672
3096
|
if (stored === null) return null;
|
|
2673
|
-
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.
|
|
3097
|
+
const restored = yield* restoreFromCookies(appleUtils, stored.cookies).pipe(Effect.orElseSucceed(() => null));
|
|
2674
3098
|
if (restored !== null) return sessionFromAuthState(restored);
|
|
2675
3099
|
const info = appleUtils.Session.getAnySessionInfo();
|
|
2676
3100
|
return info === null ? null : sessionFromInfo(stored.username, info);
|
|
@@ -2741,13 +3165,13 @@ const BsdiffServiceLive = Layer.succeed(BsdiffService, { diff: (input) => Effect
|
|
|
2741
3165
|
|
|
2742
3166
|
//#endregion
|
|
2743
3167
|
//#region src/services/identity-store.ts
|
|
2744
|
-
const isArgon2Params = (value) => isRecord(value) && typeof value["time"] === "number" && typeof value["memory"] === "number" && typeof value["parallelism"] === "number";
|
|
3168
|
+
const isArgon2Params = (value) => isRecord$1(value) && typeof value["time"] === "number" && typeof value["memory"] === "number" && typeof value["parallelism"] === "number";
|
|
2745
3169
|
/**
|
|
2746
3170
|
* Structural guard for the on-disk identity envelope. A corrupt or foreign file
|
|
2747
3171
|
* reads as "absent" so the CLI prompts to (re)create rather than crashing — the
|
|
2748
3172
|
* AAD-bound `openIdentity` still fails loudly if a well-formed file was tampered.
|
|
2749
3173
|
*/
|
|
2750
|
-
const isIdentityFile = (value) => isRecord(value) && value["version"] === 1 && typeof value["publicKey"] === "string" && typeof value["fingerprint"] === "string" && value["kdf"] === "argon2id" && isArgon2Params(value["kdfParams"]) && typeof value["salt"] === "string" && value["cipher"] === "xchacha20poly1305" && typeof value["ct"] === "string";
|
|
3174
|
+
const isIdentityFile = (value) => isRecord$1(value) && value["version"] === 1 && typeof value["publicKey"] === "string" && typeof value["fingerprint"] === "string" && value["kdf"] === "argon2id" && isArgon2Params(value["kdfParams"]) && typeof value["salt"] === "string" && value["cipher"] === "xchacha20poly1305" && typeof value["ct"] === "string";
|
|
2751
3175
|
var IdentityStore = class extends Context.Tag("cli/IdentityStore")() {};
|
|
2752
3176
|
const IdentityStoreLive = Layer.effect(IdentityStore, Effect.gen(function* () {
|
|
2753
3177
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -2756,7 +3180,7 @@ const IdentityStoreLive = Layer.effect(IdentityStore, Effect.gen(function* () {
|
|
|
2756
3180
|
const identityFile = path.join(identityDir, "identity.json");
|
|
2757
3181
|
return {
|
|
2758
3182
|
load: Effect.gen(function* () {
|
|
2759
|
-
const content = yield* fs.readFileString(identityFile).pipe(Effect.
|
|
3183
|
+
const content = yield* fs.readFileString(identityFile).pipe(Effect.orElseSucceed(() => ""));
|
|
2760
3184
|
if (content.length === 0) return null;
|
|
2761
3185
|
const parsed = safeJsonParse(content);
|
|
2762
3186
|
return isIdentityFile(parsed) ? parsed : null;
|
|
@@ -2948,7 +3372,7 @@ const UpdateAssetUploaderLive = Layer.effect(UpdateAssetUploader, Effect.gen(fun
|
|
|
2948
3372
|
const VAULT_CACHE_TTL_MS = 900 * 1e3;
|
|
2949
3373
|
/** Keychain service name; the account is the recipient's public key. */
|
|
2950
3374
|
const KEYCHAIN_SERVICE = "better-update-vault";
|
|
2951
|
-
const isCachedVaultEntry = (value) => isRecord(value) && typeof value["vaultKey"] === "string" && typeof value["vaultVersion"] === "number" && typeof value["keyId"] === "string" && typeof value["exp"] === "number";
|
|
3375
|
+
const isCachedVaultEntry = (value) => isRecord$1(value) && typeof value["vaultKey"] === "string" && typeof value["vaultVersion"] === "number" && typeof value["keyId"] === "string" && typeof value["exp"] === "number";
|
|
2952
3376
|
/** Serialize an unlocked vault into a keychain blob, stamping a TTL from `now`. */
|
|
2953
3377
|
const encodeCacheEntry = (vault, now, ttlMs = VAULT_CACHE_TTL_MS) => JSON.stringify({
|
|
2954
3378
|
vaultKey: toBase64(vault.vaultKey),
|
|
@@ -2980,7 +3404,7 @@ const VaultCacheLive = Layer.effect(VaultCache, Effect.gen(function* () {
|
|
|
2980
3404
|
const flag = yield* runtime.getEnv("BETTER_UPDATE_NO_CACHE");
|
|
2981
3405
|
return flag !== void 0 && flag.length > 0 && flag !== "0" && flag !== "false";
|
|
2982
3406
|
});
|
|
2983
|
-
const readRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).getPassword()).pipe(Effect.
|
|
3407
|
+
const readRaw = (publicKey) => Effect.try(() => new Entry(KEYCHAIN_SERVICE, publicKey).getPassword()).pipe(Effect.orElseSucceed(() => null));
|
|
2984
3408
|
const writeRaw = (publicKey, blob) => Effect.try(() => {
|
|
2985
3409
|
new Entry(KEYCHAIN_SERVICE, publicKey).setPassword(blob);
|
|
2986
3410
|
}).pipe(Effect.ignore);
|
|
@@ -3018,13 +3442,13 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
|
|
|
3018
3442
|
const cacheDir = path.join(homeDirectory, ".better-update");
|
|
3019
3443
|
const cacheFile = path.join(cacheDir, "version-check.json");
|
|
3020
3444
|
const readCache = Effect.gen(function* () {
|
|
3021
|
-
const content = yield* fs.readFileString(cacheFile).pipe(Effect.
|
|
3445
|
+
const content = yield* fs.readFileString(cacheFile).pipe(Effect.orElseSucceed(() => ""));
|
|
3022
3446
|
if (content.length === 0) return;
|
|
3023
3447
|
const parsed = yield* Effect.try({
|
|
3024
3448
|
try: () => JSON.parse(content),
|
|
3025
3449
|
catch: () => "parse-error"
|
|
3026
|
-
}).pipe(Effect.
|
|
3027
|
-
if (isRecord(parsed) && typeof parsed["latest"] === "string" && typeof parsed["checkedAt"] === "number") return {
|
|
3450
|
+
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
3451
|
+
if (isRecord$1(parsed) && typeof parsed["latest"] === "string" && typeof parsed["checkedAt"] === "number") return {
|
|
3028
3452
|
latest: parsed["latest"],
|
|
3029
3453
|
checkedAt: parsed["checkedAt"]
|
|
3030
3454
|
};
|
|
@@ -3041,7 +3465,7 @@ const VersionCheckLive = Layer.effect(VersionCheck, Effect.gen(function* () {
|
|
|
3041
3465
|
const response = yield* httpClient.execute(request);
|
|
3042
3466
|
if (response.status < 200 || response.status >= 300) return;
|
|
3043
3467
|
const body = yield* response.json;
|
|
3044
|
-
if (!isRecord(body) || typeof body["version"] !== "string") return;
|
|
3468
|
+
if (!isRecord$1(body) || typeof body["version"] !== "string") return;
|
|
3045
3469
|
const latest = body["version"];
|
|
3046
3470
|
yield* fs.makeDirectory(cacheDir, { recursive: true });
|
|
3047
3471
|
yield* fs.writeFileString(cacheFile, `${JSON.stringify({
|
|
@@ -3142,7 +3566,7 @@ const createBrowserLoginSession = (options = {}) => {
|
|
|
3142
3566
|
if (request.method === "GET" && url.pathname === "/callback") return new Response(CALLBACK_PAGE, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
3143
3567
|
if (request.method === "POST" && url.pathname === "/callback/token") try {
|
|
3144
3568
|
const body = await request.json();
|
|
3145
|
-
if (!isRecord(body)) return new Response("Invalid callback payload", { status: 400 });
|
|
3569
|
+
if (!isRecord$1(body)) return new Response("Invalid callback payload", { status: 400 });
|
|
3146
3570
|
const token = typeof body["token"] === "string" ? body["token"].trim() : "";
|
|
3147
3571
|
if (token.length === 0) return new Response("Missing token", { status: 400 });
|
|
3148
3572
|
Effect.runSync(Deferred.succeed(tokenDeferred, token));
|
|
@@ -3224,7 +3648,7 @@ const buildOpenBrowserCommand = (platform, url) => {
|
|
|
3224
3648
|
};
|
|
3225
3649
|
const openBrowser = (url) => Effect.gen(function* () {
|
|
3226
3650
|
const command = buildOpenBrowserCommand((yield* CliRuntime).platform, url);
|
|
3227
|
-
if (!(yield* Command.exitCode(command).pipe(Effect.map((code) => code === 0), Effect.
|
|
3651
|
+
if (!(yield* Command.exitCode(command).pipe(Effect.map((code) => code === 0), Effect.orElseSucceed(() => false)))) yield* Console.log(`Open this URL manually:\n${url}`);
|
|
3228
3652
|
});
|
|
3229
3653
|
const browserLogin = Effect.scoped(Effect.gen(function* () {
|
|
3230
3654
|
const configStore = yield* ConfigStore;
|
|
@@ -3623,9 +4047,9 @@ const configPath = (projectRoot) => path.join(projectRoot, BETTER_UPDATE_CONFIG_
|
|
|
3623
4047
|
* graceful `readConfig`.
|
|
3624
4048
|
*/
|
|
3625
4049
|
const readBetterUpdateConfig = (projectRoot) => Effect.gen(function* () {
|
|
3626
|
-
const content = yield* (yield* FileSystem.FileSystem).readFileString(configPath(projectRoot)).pipe(Effect.
|
|
4050
|
+
const content = yield* (yield* FileSystem.FileSystem).readFileString(configPath(projectRoot)).pipe(Effect.orElseSucceed(() => ""));
|
|
3627
4051
|
if (content.length === 0) return;
|
|
3628
|
-
return yield* Effect.try(() => JSON.parse(content)).pipe(Effect.map((parsed) => isRecord(parsed) ? parsed : void 0), Effect.
|
|
4052
|
+
return yield* Effect.try(() => JSON.parse(content)).pipe(Effect.map((parsed) => isRecord$1(parsed) ? parsed : void 0), Effect.orElseSucceed(() => void 0));
|
|
3629
4053
|
});
|
|
3630
4054
|
/**
|
|
3631
4055
|
* Resolve the linked project id from `better-update.json`, or `undefined` when
|
|
@@ -4354,14 +4778,8 @@ const KeyValueFromString = Schema.transformOrFail(Schema.String, KeyValuePair, {
|
|
|
4354
4778
|
},
|
|
4355
4779
|
encode: ({ key, value }) => ParseResult.succeed(`${key}=${value}`)
|
|
4356
4780
|
});
|
|
4357
|
-
const parseRolloutPercentage = (raw, flag) => Effect.
|
|
4358
|
-
|
|
4359
|
-
catch: () => new InvalidArgumentError({ message: `--${flag} must be an integer between 1 and 100, got "${raw}".` })
|
|
4360
|
-
});
|
|
4361
|
-
const parseKeyValue = (raw) => Effect.try({
|
|
4362
|
-
try: () => Schema.decodeUnknownSync(KeyValueFromString)(raw),
|
|
4363
|
-
catch: () => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })
|
|
4364
|
-
});
|
|
4781
|
+
const parseRolloutPercentage = (raw, flag) => Schema.decodeUnknown(RolloutPercentage)(Number(raw)).pipe(Effect.mapError(() => new InvalidArgumentError({ message: `--${flag} must be an integer between 1 and 100, got "${raw}".` })));
|
|
4782
|
+
const parseKeyValue = (raw) => Schema.decodeUnknown(KeyValueFromString)(raw).pipe(Effect.mapError(() => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })));
|
|
4365
4783
|
const parseLimit = (raw, defaultValue) => {
|
|
4366
4784
|
if (raw === void 0) return Effect.succeed(defaultValue);
|
|
4367
4785
|
const parsed = Number(raw);
|
|
@@ -18709,7 +19127,7 @@ const asArrayBuffer$1 = (bytes) => {
|
|
|
18709
19127
|
};
|
|
18710
19128
|
const signAscJwt = (credentials) => Effect.gen(function* () {
|
|
18711
19129
|
const der = pemToPkcs8Der(credentials.p8Pem);
|
|
18712
|
-
if (der === null) return yield*
|
|
19130
|
+
if (der === null) return yield* new AppleAuthError({ cause: /* @__PURE__ */ new Error("Invalid .p8 PEM") });
|
|
18713
19131
|
const header = {
|
|
18714
19132
|
alg: "ES256",
|
|
18715
19133
|
kid: credentials.keyId,
|
|
@@ -18760,8 +19178,8 @@ var AscApiError = class extends Data.TaggedError("AscApiError") {};
|
|
|
18760
19178
|
var AscNetworkError = class extends Data.TaggedError("AscNetworkError") {};
|
|
18761
19179
|
const API_BASE = "https://api.appstoreconnect.apple.com";
|
|
18762
19180
|
const extractErrors = (body) => {
|
|
18763
|
-
if (!isRecord(body) || !Array.isArray(body["errors"])) return [];
|
|
18764
|
-
return body["errors"].filter((value) => isRecord(value));
|
|
19181
|
+
if (!isRecord$1(body) || !Array.isArray(body["errors"])) return [];
|
|
19182
|
+
return body["errors"].filter((value) => isRecord$1(value));
|
|
18765
19183
|
};
|
|
18766
19184
|
const parseApiError = (response, body, raw) => {
|
|
18767
19185
|
const [first] = extractErrors(body);
|
|
@@ -18793,13 +19211,13 @@ const fetchRaw = (jwt, path, init) => Effect.gen(function* () {
|
|
|
18793
19211
|
try: () => text.length === 0 ? {} : JSON.parse(text),
|
|
18794
19212
|
catch: (cause) => new AscNetworkError({ cause })
|
|
18795
19213
|
});
|
|
18796
|
-
if (!response.ok) return yield*
|
|
19214
|
+
if (!response.ok) return yield* parseApiError(response, body, text);
|
|
18797
19215
|
return body;
|
|
18798
19216
|
});
|
|
18799
19217
|
const toAscCertificate = (value) => {
|
|
18800
|
-
if (!isRecord(value)) return null;
|
|
19218
|
+
if (!isRecord$1(value)) return null;
|
|
18801
19219
|
const { id, attributes } = value;
|
|
18802
|
-
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
19220
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
18803
19221
|
const { serialNumber, certificateType, expirationDate, certificateContent, displayName } = attributes;
|
|
18804
19222
|
if (typeof serialNumber !== "string" || typeof certificateType !== "string" || typeof expirationDate !== "string") return null;
|
|
18805
19223
|
return {
|
|
@@ -18812,9 +19230,9 @@ const toAscCertificate = (value) => {
|
|
|
18812
19230
|
};
|
|
18813
19231
|
};
|
|
18814
19232
|
const toAscBundleId = (value) => {
|
|
18815
|
-
if (!isRecord(value)) return null;
|
|
19233
|
+
if (!isRecord$1(value)) return null;
|
|
18816
19234
|
const { id, attributes } = value;
|
|
18817
|
-
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
19235
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
18818
19236
|
const { identifier, name } = attributes;
|
|
18819
19237
|
if (typeof identifier !== "string" || typeof name !== "string") return null;
|
|
18820
19238
|
return {
|
|
@@ -18834,9 +19252,9 @@ const asProfileType = (value) => {
|
|
|
18834
19252
|
return match === void 0 ? null : match;
|
|
18835
19253
|
};
|
|
18836
19254
|
const toAscProfile = (value) => {
|
|
18837
|
-
if (!isRecord(value)) return null;
|
|
19255
|
+
if (!isRecord$1(value)) return null;
|
|
18838
19256
|
const { id, attributes } = value;
|
|
18839
|
-
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
19257
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
18840
19258
|
const { name, uuid, expirationDate, profileContent } = attributes;
|
|
18841
19259
|
const profileType = asProfileType(attributes["profileType"]);
|
|
18842
19260
|
if (typeof name !== "string" || typeof uuid !== "string" || typeof expirationDate !== "string" || typeof profileContent !== "string" || profileType === null) return null;
|
|
@@ -18850,9 +19268,9 @@ const toAscProfile = (value) => {
|
|
|
18850
19268
|
};
|
|
18851
19269
|
};
|
|
18852
19270
|
const toAscDevice = (value) => {
|
|
18853
|
-
if (!isRecord(value)) return null;
|
|
19271
|
+
if (!isRecord$1(value)) return null;
|
|
18854
19272
|
const { id, attributes } = value;
|
|
18855
|
-
if (typeof id !== "string" || !isRecord(attributes)) return null;
|
|
19273
|
+
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
18856
19274
|
const { udid, name } = attributes;
|
|
18857
19275
|
if (typeof udid !== "string" || typeof name !== "string") return null;
|
|
18858
19276
|
return {
|
|
@@ -18862,11 +19280,11 @@ const toAscDevice = (value) => {
|
|
|
18862
19280
|
};
|
|
18863
19281
|
};
|
|
18864
19282
|
const extractList = (body, map) => {
|
|
18865
|
-
if (!isRecord(body) || !Array.isArray(body["data"])) return [];
|
|
19283
|
+
if (!isRecord$1(body) || !Array.isArray(body["data"])) return [];
|
|
18866
19284
|
return body["data"].map(map).filter((value) => value !== null);
|
|
18867
19285
|
};
|
|
18868
19286
|
const extractSingle = (body, map) => {
|
|
18869
|
-
if (!isRecord(body)) return null;
|
|
19287
|
+
if (!isRecord$1(body)) return null;
|
|
18870
19288
|
return map(body["data"]);
|
|
18871
19289
|
};
|
|
18872
19290
|
const malformed = (resource) => new AscApiError({
|
|
@@ -18893,12 +19311,10 @@ const createCertificate = (credentials, params) => withJwt(credentials, (jwt) =>
|
|
|
18893
19311
|
}
|
|
18894
19312
|
} })
|
|
18895
19313
|
}), toAscCertificate);
|
|
18896
|
-
if (resource === null) return yield*
|
|
19314
|
+
if (resource === null) return yield* malformed("certificate");
|
|
18897
19315
|
return resource;
|
|
18898
19316
|
}));
|
|
18899
|
-
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.
|
|
18900
|
-
yield* fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
18901
|
-
}));
|
|
19317
|
+
const deleteCertificate = (credentials, id) => withJwt(credentials, (jwt) => Effect.asVoid(fetchRaw(jwt, `/v1/certificates/${encodeURIComponent(id)}`, { method: "DELETE" })));
|
|
18902
19318
|
const listBundleIds = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
18903
19319
|
return extractList(yield* fetchRaw(jwt, "/v1/bundleIds?limit=200"), toAscBundleId);
|
|
18904
19320
|
}));
|
|
@@ -18914,7 +19330,7 @@ const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Ef
|
|
|
18914
19330
|
}
|
|
18915
19331
|
} })
|
|
18916
19332
|
}), toAscBundleId);
|
|
18917
|
-
if (resource === null) return yield*
|
|
19333
|
+
if (resource === null) return yield* malformed("bundleId");
|
|
18918
19334
|
return resource;
|
|
18919
19335
|
}));
|
|
18920
19336
|
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
@@ -18946,7 +19362,7 @@ const createProvisioningProfile = (credentials, params) => withJwt(credentials,
|
|
|
18946
19362
|
relationships
|
|
18947
19363
|
} })
|
|
18948
19364
|
}), toAscProfile);
|
|
18949
|
-
if (resource === null) return yield*
|
|
19365
|
+
if (resource === null) return yield* malformed("profile");
|
|
18950
19366
|
return resource;
|
|
18951
19367
|
}));
|
|
18952
19368
|
const isCertificateLimitError = (error) => {
|
|
@@ -18982,7 +19398,7 @@ const parseCert = (certDerBytes) => {
|
|
|
18982
19398
|
const generatePassword = () => forge.util.encode64(forge.random.getBytesSync(16));
|
|
18983
19399
|
const extractCertMetadata = (cert) => Effect.gen(function* () {
|
|
18984
19400
|
const appleTeamId = extractTeamId$1(cert);
|
|
18985
|
-
if (appleTeamId === null) return yield*
|
|
19401
|
+
if (appleTeamId === null) return yield* new CertParseError({ message: "Could not extract Apple team identifier from certificate subject" });
|
|
18986
19402
|
return {
|
|
18987
19403
|
serialNumber: cert.serialNumber.toUpperCase(),
|
|
18988
19404
|
validFrom: cert.validity.notBefore.toISOString(),
|
|
@@ -19000,7 +19416,7 @@ const extractCertMetadata = (cert) => Effect.gen(function* () {
|
|
|
19000
19416
|
*/
|
|
19001
19417
|
const extractMetadataFromP12 = (params) => Effect.gen(function* () {
|
|
19002
19418
|
const certBagOid = forge.pki.oids["certBag"];
|
|
19003
|
-
if (certBagOid === void 0) return yield*
|
|
19419
|
+
if (certBagOid === void 0) return yield* new CertParseError({ message: "PKCS#12 OID lookup for certBag failed" });
|
|
19004
19420
|
const [first] = yield* Effect.try({
|
|
19005
19421
|
try: () => {
|
|
19006
19422
|
const p12Der = forge.util.decode64(params.p12Base64);
|
|
@@ -19009,7 +19425,7 @@ const extractMetadataFromP12 = (params) => Effect.gen(function* () {
|
|
|
19009
19425
|
},
|
|
19010
19426
|
catch: (error) => new CertParseError({ message: `Failed to parse PKCS#12 bundle: ${error instanceof Error ? error.message : String(error)}` })
|
|
19011
19427
|
});
|
|
19012
|
-
if (first?.cert === void 0) return yield*
|
|
19428
|
+
if (first?.cert === void 0) return yield* new CertParseError({ message: "PKCS#12 bundle does not contain a certificate" });
|
|
19013
19429
|
return yield* extractCertMetadata(first.cert);
|
|
19014
19430
|
});
|
|
19015
19431
|
const buildDistributionCertP12 = (params) => Effect.gen(function* () {
|
|
@@ -19190,10 +19606,10 @@ const generateAndUploadDistributionCertificate = (api, input) => Effect.gen(func
|
|
|
19190
19606
|
csrPem: csrResult.csrPem,
|
|
19191
19607
|
certificateType
|
|
19192
19608
|
}).pipe(Effect.mapError(wrapAscError("apple-create-certificate")));
|
|
19193
|
-
if (apple.certificateContent === null) return yield*
|
|
19609
|
+
if (apple.certificateContent === null) return yield* new GenerateFailedError({
|
|
19194
19610
|
step: "apple-create-certificate",
|
|
19195
19611
|
message: "Apple response missing certificateContent"
|
|
19196
|
-
})
|
|
19612
|
+
});
|
|
19197
19613
|
const bundle = yield* buildDistributionCertP12({
|
|
19198
19614
|
certificateContentBase64: apple.certificateContent,
|
|
19199
19615
|
privateKey: csrResult.privateKey
|
|
@@ -19243,10 +19659,10 @@ const revokeAppleCertificate = (api, input) => Effect.gen(function* () {
|
|
|
19243
19659
|
});
|
|
19244
19660
|
const revokeLocalDistributionCertificate = (api, input) => Effect.gen(function* () {
|
|
19245
19661
|
const local = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === input.distributionCertificateId);
|
|
19246
|
-
if (local === void 0) return yield*
|
|
19662
|
+
if (local === void 0) return yield* new GenerateFailedError({
|
|
19247
19663
|
step: "load-distribution-certificate",
|
|
19248
19664
|
message: `Distribution certificate ${input.distributionCertificateId} not found on this account`
|
|
19249
|
-
})
|
|
19665
|
+
});
|
|
19250
19666
|
const creds = yield* fetchAscCredentials(api, input.ascApiKeyId);
|
|
19251
19667
|
const ascCreds = {
|
|
19252
19668
|
keyId: creds.keyId,
|
|
@@ -19283,10 +19699,10 @@ const listAppleCertificates = (api, input) => Effect.gen(function* () {
|
|
|
19283
19699
|
});
|
|
19284
19700
|
const resolveCertAscId = (creds, serialNumber, certificateType) => Effect.gen(function* () {
|
|
19285
19701
|
const match = (yield* listCertificates(creds, { certificateType }).pipe(Effect.mapError(wrapAscError("apple-list-certificates")))).find((entry) => entry.serialNumber.toUpperCase() === serialNumber);
|
|
19286
|
-
if (match === void 0) return yield*
|
|
19702
|
+
if (match === void 0) return yield* new GenerateFailedError({
|
|
19287
19703
|
step: "match-apple-certificate",
|
|
19288
19704
|
message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
|
|
19289
|
-
})
|
|
19705
|
+
});
|
|
19290
19706
|
return match.id;
|
|
19291
19707
|
});
|
|
19292
19708
|
const ensureBundleId = (creds, bundleIdentifier) => Effect.gen(function* () {
|
|
@@ -19319,10 +19735,10 @@ const generateAndUploadProvisioningProfile = (api, input) => Effect.gen(function
|
|
|
19319
19735
|
const [certAscId, bundleIdAscId] = yield* Effect.all([resolveCertAscId(ascCreds, cert.serialNumber.toUpperCase(), certificateType), ensureBundleId(ascCreds, input.bundleIdentifier)], { concurrency: 2 });
|
|
19320
19736
|
const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
|
|
19321
19737
|
const { ids: deviceAscIds } = useDevices ? yield* collectDeviceAscIds(ascCreds, cert.appleTeamId, input.deviceIds) : { ids: [] };
|
|
19322
|
-
if (useDevices && deviceAscIds.length === 0) return yield*
|
|
19738
|
+
if (useDevices && deviceAscIds.length === 0) return yield* new GenerateFailedError({
|
|
19323
19739
|
step: "collect-devices",
|
|
19324
19740
|
message: "No registered devices to attach to the provisioning profile"
|
|
19325
|
-
})
|
|
19741
|
+
});
|
|
19326
19742
|
const profileBytes = fromBase64((yield* createProvisioningProfile(ascCreds, {
|
|
19327
19743
|
profileName: `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`,
|
|
19328
19744
|
profileType: DISTRIBUTION_TO_PROFILE_TYPE$1[input.distributionType],
|
|
@@ -19633,10 +20049,10 @@ const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
|
|
|
19633
20049
|
...compact({ buildProfile: options.buildProfile })
|
|
19634
20050
|
}
|
|
19635
20051
|
}).pipe(Effect.mapError((cause) => resolveErrorToMissingCredentials(cause, "android")));
|
|
19636
|
-
if (resolved.platform !== "android") return yield*
|
|
20052
|
+
if (resolved.platform !== "android") return yield* new MissingCredentialsError({
|
|
19637
20053
|
message: "Server returned non-Android credentials for an Android build request",
|
|
19638
20054
|
hint: androidBindHint
|
|
19639
|
-
})
|
|
20055
|
+
});
|
|
19640
20056
|
const secret = yield* decryptResolveSecret({
|
|
19641
20057
|
session: yield* openVaultSessionForBuild(api, androidBindHint),
|
|
19642
20058
|
credentialType: "keystore",
|
|
@@ -19757,7 +20173,7 @@ const credentialsJsonPath = (projectRoot) => path.join(projectRoot, CREDENTIALS_
|
|
|
19757
20173
|
const readCredentialsJson = (projectRoot) => Effect.gen(function* () {
|
|
19758
20174
|
const fs = yield* FileSystem.FileSystem;
|
|
19759
20175
|
const filePath = credentialsJsonPath(projectRoot);
|
|
19760
|
-
if (!(yield* fs.exists(filePath).pipe(Effect.
|
|
20176
|
+
if (!(yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false)))) return yield* new CredentialsJsonError({ message: `credentials.json not found at ${filePath}.` });
|
|
19761
20177
|
return yield* parseCredentialsJson(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new CredentialsJsonError({ message: `Failed to read credentials.json: ${String(cause)}` }))));
|
|
19762
20178
|
});
|
|
19763
20179
|
const writeCredentialsJson = (projectRoot, data) => Effect.gen(function* () {
|
|
@@ -19774,7 +20190,7 @@ const resolveCredentialPath = (projectRoot, candidate) => path.isAbsolute(candid
|
|
|
19774
20190
|
|
|
19775
20191
|
//#endregion
|
|
19776
20192
|
//#region src/lib/local-credentials.ts
|
|
19777
|
-
const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.
|
|
20193
|
+
const requirePath = (fs, absolutePath, label) => fs.exists(absolutePath).pipe(Effect.orElseSucceed(() => false), Effect.flatMap((exists) => exists ? Effect.void : Effect.fail(new MissingCredentialsError({
|
|
19778
20194
|
message: `Local credentials.json: ${label} not found at ${absolutePath}.`,
|
|
19779
20195
|
hint: "Run `better-update credentials sync pull` to materialize files, or fix the path in credentials.json."
|
|
19780
20196
|
}))));
|
|
@@ -20029,7 +20445,7 @@ const runStepFormatted = (cmd, step, formatter) => Effect.gen(function* () {
|
|
|
20029
20445
|
if (code !== 0) {
|
|
20030
20446
|
const summary = formatter.getBuildSummary();
|
|
20031
20447
|
if (summary.length > 0) process$1.stderr.write(`${summary}\n`);
|
|
20032
|
-
return yield*
|
|
20448
|
+
return yield* buildFailed(step, code, `${step} exited with code ${code}`);
|
|
20033
20449
|
}
|
|
20034
20450
|
});
|
|
20035
20451
|
|
|
@@ -20265,10 +20681,10 @@ const findAscCertificateId = (ctx, serialNumber, certificateType) => Effect.gen(
|
|
|
20265
20681
|
const certs = yield* wrap("apple-list-certificates", async () => AppleUtils.Certificate.getAsync(ctx, { query: { filter: { certificateType } } }));
|
|
20266
20682
|
const upper = serialNumber.toUpperCase();
|
|
20267
20683
|
const match = certs.find((entry) => entry.attributes.serialNumber.toUpperCase() === upper);
|
|
20268
|
-
if (match === void 0) return yield*
|
|
20684
|
+
if (match === void 0) return yield* new AppleIdGenerateFailedError({
|
|
20269
20685
|
step: "match-apple-certificate",
|
|
20270
20686
|
message: `Distribution certificate ${serialNumber} not present on Apple Developer Portal; upload or re-generate it`
|
|
20271
|
-
})
|
|
20687
|
+
});
|
|
20272
20688
|
return match.id;
|
|
20273
20689
|
});
|
|
20274
20690
|
const collectIosDeviceIds = (ctx, deviceIds) => Effect.gen(function* () {
|
|
@@ -20287,10 +20703,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
|
|
|
20287
20703
|
const [certAscId, bundleIdAscId] = yield* Effect.all([findAscCertificateId(ctx, cert.serialNumber, certificateType), findOrCreateBundleId(ctx, input.bundleIdentifier)], { concurrency: 2 });
|
|
20288
20704
|
const useDevices = input.distributionType === "AD_HOC" || input.distributionType === "DEVELOPMENT";
|
|
20289
20705
|
const deviceIds = useDevices ? yield* collectIosDeviceIds(ctx, input.deviceIds) : [];
|
|
20290
|
-
if (useDevices && deviceIds.length === 0) return yield*
|
|
20706
|
+
if (useDevices && deviceIds.length === 0) return yield* new AppleIdGenerateFailedError({
|
|
20291
20707
|
step: "collect-devices",
|
|
20292
20708
|
message: "No registered devices to attach to the provisioning profile"
|
|
20293
|
-
})
|
|
20709
|
+
});
|
|
20294
20710
|
const profileName = `${input.bundleIdentifier} ${input.distributionType} ${Date.now()}`;
|
|
20295
20711
|
const { profileContent } = (yield* wrap("apple-create-profile", async () => AppleUtils.Profile.createAsync(ctx, {
|
|
20296
20712
|
bundleId: bundleIdAscId,
|
|
@@ -20299,10 +20715,10 @@ const generateAndUploadProvisioningProfileViaAppleId = (api, input) => Effect.ge
|
|
|
20299
20715
|
name: profileName,
|
|
20300
20716
|
profileType: DISTRIBUTION_TO_PROFILE_TYPE[input.distributionType]
|
|
20301
20717
|
}))).attributes;
|
|
20302
|
-
if (profileContent === null) return yield*
|
|
20718
|
+
if (profileContent === null) return yield* new AppleIdGenerateFailedError({
|
|
20303
20719
|
step: "extract-profile-content",
|
|
20304
20720
|
message: "Apple returned a profile with no content (likely expired/invalid)"
|
|
20305
|
-
})
|
|
20721
|
+
});
|
|
20306
20722
|
const profileBytes = fromBase64(profileContent);
|
|
20307
20723
|
const rosterHash = useDevices ? computeDeviceRosterHashHex(deviceIds) : void 0;
|
|
20308
20724
|
const created = yield* api.appleProvisioningProfiles.upload({ payload: {
|
|
@@ -20482,10 +20898,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
|
|
|
20482
20898
|
ascApiKeyId,
|
|
20483
20899
|
certificateType: "IOS_DISTRIBUTION"
|
|
20484
20900
|
});
|
|
20485
|
-
if (certs.length === 0) return yield*
|
|
20901
|
+
if (certs.length === 0) return yield* new MissingCredentialsError({
|
|
20486
20902
|
message: "Apple says the certificate limit is hit but no existing certificates were returned.",
|
|
20487
20903
|
hint: "Try again later or check the Apple Developer portal."
|
|
20488
|
-
})
|
|
20904
|
+
});
|
|
20489
20905
|
const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
|
|
20490
20906
|
value: entry.id,
|
|
20491
20907
|
label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
|
|
@@ -20498,10 +20914,10 @@ const interactiveCertLimitRecover = (api, ascApiKeyId) => Effect.gen(function* (
|
|
|
20498
20914
|
});
|
|
20499
20915
|
const generateDistributionCertInteractive = (api) => Effect.gen(function* () {
|
|
20500
20916
|
const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null);
|
|
20501
|
-
if (teamAscKeys.length === 0) return yield*
|
|
20917
|
+
if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
|
|
20502
20918
|
message: "No ASC API key linked to an Apple team in this organization.",
|
|
20503
20919
|
hint: "Upload an ASC API key with a team assignment via the dashboard, then retry."
|
|
20504
|
-
})
|
|
20920
|
+
});
|
|
20505
20921
|
const ascKeyId = yield* promptSelect("Select an ASC API key to issue the certificate against", teamAscKeys.map((key) => ({
|
|
20506
20922
|
value: key.id,
|
|
20507
20923
|
label: `${key.name} (${key.keyId})`
|
|
@@ -20520,10 +20936,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
|
|
|
20520
20936
|
}, {
|
|
20521
20937
|
value: "abort",
|
|
20522
20938
|
label: "Abort — I'll upload one manually"
|
|
20523
|
-
}])) === "abort") return yield*
|
|
20939
|
+
}])) === "abort") return yield* new MissingCredentialsError({
|
|
20524
20940
|
message: "Build aborted — no distribution certificate available.",
|
|
20525
20941
|
hint: "Run `better-update credentials generate distribution-certificate --asc-key-id <id>` or upload via the dashboard."
|
|
20526
|
-
})
|
|
20942
|
+
});
|
|
20527
20943
|
return (yield* generateDistributionCertInteractive(api)).id;
|
|
20528
20944
|
}
|
|
20529
20945
|
const choice = yield* promptSelect("Select a distribution certificate (or 'generate' for a fresh one)", [{
|
|
@@ -20539,10 +20955,10 @@ const chooseIosCertificateId = (api) => Effect.gen(function* () {
|
|
|
20539
20955
|
const pickIosCertificate = (api) => Effect.gen(function* () {
|
|
20540
20956
|
const chosenId = yield* chooseIosCertificateId(api);
|
|
20541
20957
|
const cert = (yield* api.appleDistributionCertificates.list()).items.find((entry) => entry.id === chosenId);
|
|
20542
|
-
if (cert === void 0) return yield*
|
|
20958
|
+
if (cert === void 0) return yield* new MissingCredentialsError({
|
|
20543
20959
|
message: "Selected certificate not found after generation.",
|
|
20544
20960
|
hint: "Retry."
|
|
20545
|
-
})
|
|
20961
|
+
});
|
|
20546
20962
|
return {
|
|
20547
20963
|
certId: chosenId,
|
|
20548
20964
|
cert
|
|
@@ -20550,10 +20966,10 @@ const pickIosCertificate = (api) => Effect.gen(function* () {
|
|
|
20550
20966
|
});
|
|
20551
20967
|
const pickIosAscKey = (api, appleTeamId) => Effect.gen(function* () {
|
|
20552
20968
|
const teamAscKeys = (yield* api.ascApiKeys.list()).items.filter((key) => key.appleTeamId !== null && key.appleTeamId === appleTeamId);
|
|
20553
|
-
if (teamAscKeys.length === 0) return yield*
|
|
20969
|
+
if (teamAscKeys.length === 0) return yield* new MissingCredentialsError({
|
|
20554
20970
|
message: `No ASC API key linked to Apple team ${appleTeamId}.`,
|
|
20555
20971
|
hint: "Upload an ASC API key for that team via the dashboard, then retry."
|
|
20556
|
-
})
|
|
20972
|
+
});
|
|
20557
20973
|
return yield* promptSelect("Select an ASC API key", teamAscKeys.map((key) => ({
|
|
20558
20974
|
value: key.id,
|
|
20559
20975
|
label: `${key.name} (${key.keyId})`
|
|
@@ -20634,10 +21050,10 @@ const generateKeystoreInteractive = (api) => Effect.gen(function* () {
|
|
|
20634
21050
|
});
|
|
20635
21051
|
const pickExistingKeystore = (api) => Effect.gen(function* () {
|
|
20636
21052
|
const keystores = yield* api.androidUploadKeystores.list();
|
|
20637
|
-
if (keystores.items.length === 0) return yield*
|
|
21053
|
+
if (keystores.items.length === 0) return yield* new MissingCredentialsError({
|
|
20638
21054
|
message: "No existing keystores in this organization.",
|
|
20639
21055
|
hint: "Re-run and choose 'Generate new keystore'."
|
|
20640
|
-
})
|
|
21056
|
+
});
|
|
20641
21057
|
return yield* promptSelect("Select a keystore", keystores.items.map((item) => ({
|
|
20642
21058
|
value: item.id,
|
|
20643
21059
|
label: item.keyAlias
|
|
@@ -20670,10 +21086,10 @@ const setupAndroidInteractive = (api, input) => Effect.gen(function* () {
|
|
|
20670
21086
|
label: "Abort — I'll configure it in the dashboard"
|
|
20671
21087
|
}
|
|
20672
21088
|
]);
|
|
20673
|
-
if (choice === "abort") return yield*
|
|
21089
|
+
if (choice === "abort") return yield* new MissingCredentialsError({
|
|
20674
21090
|
message: `Build aborted — no keystore bound to ${input.applicationIdentifier}.`,
|
|
20675
21091
|
hint: "Run `better-update credentials generate keystore` or upload via the dashboard."
|
|
20676
|
-
})
|
|
21092
|
+
});
|
|
20677
21093
|
const keystoreId = yield* choice === "generate" ? generateKeystoreAuto(api, input.applicationIdentifier) : pickExistingKeystore(api);
|
|
20678
21094
|
yield* api.androidBuildCredentials.create({
|
|
20679
21095
|
path: { applicationIdentifierId: appId },
|
|
@@ -20694,10 +21110,10 @@ const ensureAndroidCredentialsAvailable = (api, input) => api.buildCredentials.r
|
|
|
20694
21110
|
}).pipe(Effect.asVoid);
|
|
20695
21111
|
const ensureAndroidCredentials = (api, input, options) => ensureAndroidCredentialsAvailable(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
|
|
20696
21112
|
const mode = yield* InteractiveMode;
|
|
20697
|
-
if (options.freezeCredentials || !mode.allow) return yield*
|
|
21113
|
+
if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
|
|
20698
21114
|
message: `No Android build credentials for ${input.applicationIdentifier}.`,
|
|
20699
21115
|
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
20700
|
-
})
|
|
21116
|
+
});
|
|
20701
21117
|
yield* setupAndroidInteractive(api, input);
|
|
20702
21118
|
return yield* ensureAndroidCredentialsAvailable(api, input);
|
|
20703
21119
|
})));
|
|
@@ -20718,10 +21134,10 @@ const resolveIosBuildCredentials = (api, input) => api.buildCredentials.resolve(
|
|
|
20718
21134
|
const findBoundIosConfig = (api, input) => Effect.gen(function* () {
|
|
20719
21135
|
const distributionType = IOS_DISTRIBUTION_TO_TYPE[input.distribution];
|
|
20720
21136
|
const match = (yield* api.iosBundleConfigurations.list({ path: { projectId: input.projectId } })).items.find((config) => config.bundleIdentifier === input.bundleIdentifier && config.distributionType === distributionType);
|
|
20721
|
-
if (match === void 0) return yield*
|
|
21137
|
+
if (match === void 0) return yield* new MissingCredentialsError({
|
|
20722
21138
|
message: `iOS bundle configuration vanished while regenerating stale profile for ${input.bundleIdentifier}`,
|
|
20723
21139
|
hint: "Retry; the configuration must exist before regeneration"
|
|
20724
|
-
})
|
|
21140
|
+
});
|
|
20725
21141
|
return match;
|
|
20726
21142
|
});
|
|
20727
21143
|
const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
|
|
@@ -20752,19 +21168,19 @@ const regenerateProvisioningProfile = (api, input) => Effect.gen(function* () {
|
|
|
20752
21168
|
});
|
|
20753
21169
|
const ensureIosCredentials = (api, input, options) => resolveIosBuildCredentials(api, input).pipe(Effect.catchIf(isMissingResolveError, () => Effect.gen(function* () {
|
|
20754
21170
|
const mode = yield* InteractiveMode;
|
|
20755
|
-
if (options.freezeCredentials || !mode.allow) return yield*
|
|
21171
|
+
if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
|
|
20756
21172
|
message: `No iOS build credentials for ${input.bundleIdentifier} (${input.distribution}).`,
|
|
20757
21173
|
hint: options.freezeCredentials ? "Run `better-update credentials generate` first, or remove --freeze-credentials." : "Run `better-update credentials generate` first, or rerun with --interactive to configure now."
|
|
20758
|
-
})
|
|
21174
|
+
});
|
|
20759
21175
|
yield* setupIosInteractive(api, input);
|
|
20760
21176
|
return yield* resolveIosBuildCredentials(api, input);
|
|
20761
21177
|
})), Effect.flatMap((resolved) => Effect.gen(function* () {
|
|
20762
21178
|
if (resolved.platform !== "ios" || !resolved.profileStale) return;
|
|
20763
21179
|
const mode = yield* InteractiveMode;
|
|
20764
|
-
if (options.freezeCredentials || !mode.allow) return yield*
|
|
21180
|
+
if (options.freezeCredentials || !mode.allow) return yield* new MissingCredentialsError({
|
|
20765
21181
|
message: `Stale provisioning profile for ${input.bundleIdentifier}; cannot regenerate without an interactive session.`,
|
|
20766
21182
|
hint: options.freezeCredentials ? "Run a build without --freeze-credentials once to refresh the profile, or run `better-update credentials regenerate-profile`." : "Run `better-update credentials regenerate-profile --bundle <id> --distribution <type>` from an interactive terminal."
|
|
20767
|
-
})
|
|
21183
|
+
});
|
|
20768
21184
|
yield* Console.log(`Stale provisioning profile for ${input.bundleIdentifier} (device roster changed). Regenerating...`);
|
|
20769
21185
|
yield* regenerateProvisioningProfile(api, input);
|
|
20770
21186
|
})));
|
|
@@ -20814,7 +21230,7 @@ const applyTargetSigning = (options) => Effect.gen(function* () {
|
|
|
20814
21230
|
const projectDir = yield* findXcodeProjectDir$1(options.iosDir);
|
|
20815
21231
|
const pbxprojPath = path.join(projectDir, "project.pbxproj");
|
|
20816
21232
|
const project = yield* parseProject$1(pbxprojPath);
|
|
20817
|
-
for (const entry of options.entries) for (const configUuid of entry.buildConfigurationUuids) if (!mutateConfig(project, configUuid, entry.settings)) yield* new XcodeProjectError({ message: `Build configuration ${configUuid} not found for target "${entry.targetName}" in ${pbxprojPath}.` });
|
|
21233
|
+
for (const entry of options.entries) for (const configUuid of entry.buildConfigurationUuids) if (!mutateConfig(project, configUuid, entry.settings)) return yield* new XcodeProjectError({ message: `Build configuration ${configUuid} not found for target "${entry.targetName}" in ${pbxprojPath}.` });
|
|
20818
21234
|
const serialized = yield* Effect.try({
|
|
20819
21235
|
try: () => project.writeSync(),
|
|
20820
21236
|
catch: (cause) => new XcodeProjectError({ message: `Failed to serialize ${pbxprojPath}: ${cause instanceof Error ? cause.message : String(cause)}` })
|
|
@@ -21002,7 +21418,7 @@ const installProvisioningProfile = ({ profilePath }) => Effect.acquireRelease(Ef
|
|
|
21002
21418
|
//#endregion
|
|
21003
21419
|
//#region src/lib/post-build-validation.ts
|
|
21004
21420
|
const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Effect.gen(function* () {
|
|
21005
|
-
const bundleId = yield* readBundleId(bundleDir).pipe(Effect.
|
|
21421
|
+
const bundleId = yield* readBundleId(bundleDir).pipe(Effect.orElseSucceed(() => void 0));
|
|
21006
21422
|
if (!bundleId) return {
|
|
21007
21423
|
bundleId: void 0,
|
|
21008
21424
|
warnings: [`Missing CFBundleIdentifier in Info.plist at ${bundleDir}`]
|
|
@@ -21014,16 +21430,16 @@ const validateOneBundle = (bundleDir, expectedByBundleId, expectedTeamId) => Eff
|
|
|
21014
21430
|
};
|
|
21015
21431
|
return {
|
|
21016
21432
|
bundleId,
|
|
21017
|
-
warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.
|
|
21433
|
+
warnings: yield* validateEmbeddedProfile(bundleDir, expected.profileUuid, expectedTeamId, bundleId).pipe(Effect.orElseSucceed(() => []))
|
|
21018
21434
|
};
|
|
21019
21435
|
});
|
|
21020
21436
|
const validateIosBuild = (params) => Effect.gen(function* () {
|
|
21021
|
-
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.
|
|
21437
|
+
const appDir = yield* findAppDirectory$1(params.archivePath).pipe(Effect.orElseSucceed(() => void 0));
|
|
21022
21438
|
if (!appDir) return {
|
|
21023
21439
|
passed: false,
|
|
21024
21440
|
warnings: ["Could not locate .app bundle in archive — skipping post-build validation"]
|
|
21025
21441
|
};
|
|
21026
|
-
const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.
|
|
21442
|
+
const bundleDirs = yield* listSignedBundleDirs(appDir).pipe(Effect.orElseSucceed(() => [appDir]));
|
|
21027
21443
|
const expectedByBundleId = new Map(params.expectedTargets.map((target) => [target.bundleId, target]));
|
|
21028
21444
|
const perBundle = yield* Effect.forEach(bundleDirs, (bundleDir) => validateOneBundle(bundleDir, expectedByBundleId, params.expectedTeamId));
|
|
21029
21445
|
const warnings = perBundle.flatMap((entry) => [...entry.warnings]);
|
|
@@ -21053,7 +21469,7 @@ const findAppDirectory$1 = (archivePath) => Effect.gen(function* () {
|
|
|
21053
21469
|
const listSignedBundleDirs = (appDir) => Effect.gen(function* () {
|
|
21054
21470
|
const fs = yield* FileSystem.FileSystem;
|
|
21055
21471
|
const plugInsDir = path.join(appDir, "PlugIns");
|
|
21056
|
-
if (!(yield* fs.exists(plugInsDir).pipe(Effect.
|
|
21472
|
+
if (!(yield* fs.exists(plugInsDir).pipe(Effect.orElseSucceed(() => false)))) return [appDir];
|
|
21057
21473
|
return [appDir, ...(yield* fs.readDirectory(plugInsDir)).filter((entry) => entry.endsWith(".appex")).map((entry) => path.join(plugInsDir, entry))];
|
|
21058
21474
|
});
|
|
21059
21475
|
const readBundleId = (bundleDir) => Effect.gen(function* () {
|
|
@@ -21996,7 +22412,7 @@ const easJsonPath = (projectRoot) => Effect.gen(function* () {
|
|
|
21996
22412
|
const readEasJson = (projectRoot) => Effect.gen(function* () {
|
|
21997
22413
|
const fs = yield* FileSystem.FileSystem;
|
|
21998
22414
|
const filePath = yield* easJsonPath(projectRoot);
|
|
21999
|
-
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.
|
|
22415
|
+
return yield* parseEasConfig(yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new BuildProfileError({ message: cause._tag === "SystemError" && cause.reason === "NotFound" ? `No eas.json found at ${filePath}. Create one with a "build" section.` : `Failed to read eas.json: ${cause.message}` }))));
|
|
22000
22416
|
});
|
|
22001
22417
|
const mergeCustom = (base, overlay) => {
|
|
22002
22418
|
const merged = shallowMerge(base, overlay);
|
|
@@ -22208,8 +22624,8 @@ const clearBuildCaches = (projectRoot) => Effect.gen(function* () {
|
|
|
22208
22624
|
const removed = [];
|
|
22209
22625
|
yield* Effect.forEach(CACHE_DIRS, (rel) => Effect.gen(function* () {
|
|
22210
22626
|
const target = path.join(projectRoot, rel);
|
|
22211
|
-
if (!(yield* fs.exists(target).pipe(Effect.
|
|
22212
|
-
yield* fs.remove(target, { recursive: true }).pipe(Effect.
|
|
22627
|
+
if (!(yield* fs.exists(target).pipe(Effect.orElseSucceed(() => false)))) return;
|
|
22628
|
+
yield* fs.remove(target, { recursive: true }).pipe(Effect.orElseSucceed(() => void 0));
|
|
22213
22629
|
removed.push(rel);
|
|
22214
22630
|
}), { concurrency: 4 });
|
|
22215
22631
|
if (removed.length > 0) yield* Console.error(`Cleared caches: ${removed.join(", ")}`);
|
|
@@ -22503,7 +22919,7 @@ const runFingerprintFull = (projectRoot, options = {}) => Effect.gen(function* (
|
|
|
22503
22919
|
try: () => JSON.parse(stdout),
|
|
22504
22920
|
catch: () => new FingerprintError({ message: "Failed to parse @expo/fingerprint output as JSON." })
|
|
22505
22921
|
});
|
|
22506
|
-
if (!isRecord(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
|
|
22922
|
+
if (!isRecord$1(parsed)) return yield* new FingerprintError({ message: "@expo/fingerprint output was not a JSON object." });
|
|
22507
22923
|
const { hash } = parsed;
|
|
22508
22924
|
if (typeof hash !== "string" || hash.length === 0) return yield* new FingerprintError({ message: "@expo/fingerprint output did not contain a \"hash\" string field." });
|
|
22509
22925
|
const sourcesRaw = parsed["sources"];
|
|
@@ -22538,10 +22954,10 @@ const runString = (cmd, cwd) => Command.string(Command.workingDirectory(cmd, cwd
|
|
|
22538
22954
|
*/
|
|
22539
22955
|
const readGitContext = (projectRoot) => Effect.gen(function* () {
|
|
22540
22956
|
const [commit, ref, commitMessage, status] = yield* Effect.all([
|
|
22541
|
-
runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.
|
|
22542
|
-
runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.
|
|
22543
|
-
runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.
|
|
22544
|
-
runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.
|
|
22957
|
+
runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
|
|
22958
|
+
runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
|
|
22959
|
+
runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(Effect.map((output) => output.trim()), Effect.orElseSucceed(() => "")),
|
|
22960
|
+
runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(Effect.orElseSucceed(() => ""))
|
|
22545
22961
|
], { concurrency: "unbounded" });
|
|
22546
22962
|
return {
|
|
22547
22963
|
ref: ref.length > 0 ? ref : void 0,
|
|
@@ -22570,14 +22986,14 @@ const readGradleConfig = (androidDir) => Effect.gen(function* () {
|
|
|
22570
22986
|
const hasKts = yield* fs.exists(ktsPath).pipe(Effect.orElseSucceed(() => false));
|
|
22571
22987
|
if (!hasGroovy && hasKts) return;
|
|
22572
22988
|
if (!hasGroovy) return;
|
|
22573
|
-
const content = yield* fs.readFileString(gradlePath).pipe(Effect.
|
|
22989
|
+
const content = yield* fs.readFileString(gradlePath).pipe(Effect.orElseSucceed(() => void 0));
|
|
22574
22990
|
if (!content) return;
|
|
22575
22991
|
return yield* Effect.tryPromise({
|
|
22576
22992
|
try: async () => {
|
|
22577
22993
|
return __require("gradle-to-js").parseText(stripGroovyComments(content));
|
|
22578
22994
|
},
|
|
22579
22995
|
catch: () => void 0
|
|
22580
|
-
}).pipe(Effect.map(extractGradleConfig), Effect.
|
|
22996
|
+
}).pipe(Effect.map(extractGradleConfig), Effect.orElseSucceed(() => void 0));
|
|
22581
22997
|
});
|
|
22582
22998
|
/**
|
|
22583
22999
|
* Log a warning if Gradle applicationId differs from app.json package name.
|
|
@@ -22697,7 +23113,7 @@ const ALWAYS_IGNORE = [...[
|
|
|
22697
23113
|
"dist"
|
|
22698
23114
|
], ...NATIVE_BUILD_OUTPUTS];
|
|
22699
23115
|
const findLockfile = (fs, dir) => Effect.gen(function* () {
|
|
22700
|
-
for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.
|
|
23116
|
+
for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.orElseSucceed(() => false))) return pm;
|
|
22701
23117
|
});
|
|
22702
23118
|
const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
|
|
22703
23119
|
const pm = yield* findLockfile(yield* FileSystem.FileSystem, dir);
|
|
@@ -22733,13 +23149,13 @@ const buildIgnoreInstance = (workspaceRoot, options = {}) => Effect.gen(function
|
|
|
22733
23149
|
const ig = ignore();
|
|
22734
23150
|
ig.add([...ALWAYS_IGNORE]);
|
|
22735
23151
|
const easignorePath = path.join(workspaceRoot, ".easignore");
|
|
22736
|
-
if (yield* fs.exists(easignorePath).pipe(Effect.
|
|
22737
|
-
const content = yield* fs.readFileString(easignorePath).pipe(Effect.
|
|
23152
|
+
if (yield* fs.exists(easignorePath).pipe(Effect.orElseSucceed(() => false))) {
|
|
23153
|
+
const content = yield* fs.readFileString(easignorePath).pipe(Effect.orElseSucceed(() => ""));
|
|
22738
23154
|
ig.add(content);
|
|
22739
23155
|
} else {
|
|
22740
23156
|
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
22741
|
-
if (yield* fs.exists(gitignorePath).pipe(Effect.
|
|
22742
|
-
const content = yield* fs.readFileString(gitignorePath).pipe(Effect.
|
|
23157
|
+
if (yield* fs.exists(gitignorePath).pipe(Effect.orElseSucceed(() => false))) {
|
|
23158
|
+
const content = yield* fs.readFileString(gitignorePath).pipe(Effect.orElseSucceed(() => ""));
|
|
22743
23159
|
ig.add(content);
|
|
22744
23160
|
}
|
|
22745
23161
|
}
|
|
@@ -22810,7 +23226,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
|
22810
23226
|
})
|
|
22811
23227
|
});
|
|
22812
23228
|
yield* initGitRepo(stagingRoot);
|
|
22813
|
-
if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.
|
|
23229
|
+
if (yield* fs.exists(path.join(workspaceRoot, "package.json")).pipe(Effect.orElseSucceed(() => false))) yield* runInstall({
|
|
22814
23230
|
stagingRoot,
|
|
22815
23231
|
packageManager,
|
|
22816
23232
|
env: yield* runtime.commandEnvironment(input.envVars)
|
|
@@ -22827,7 +23243,7 @@ const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
|
22827
23243
|
//#endregion
|
|
22828
23244
|
//#region src/lib/repo-clean.ts
|
|
22829
23245
|
const MAX_FILES_SHOWN = 10;
|
|
22830
|
-
const readPorcelain = (projectRoot) => Command.make("git", "status", "--porcelain").pipe(Command.workingDirectory(projectRoot), Command.string, Effect.map((output) => output.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0)), Effect.
|
|
23246
|
+
const readPorcelain = (projectRoot) => Command.make("git", "status", "--porcelain").pipe(Command.workingDirectory(projectRoot), Command.string, Effect.map((output) => output.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0)), Effect.orElseSucceed(() => []));
|
|
22831
23247
|
/**
|
|
22832
23248
|
* Refuse to proceed when the working tree has uncommitted changes. Skipped when
|
|
22833
23249
|
* `allowDirty` is true. In interactive mode, prompts the user to confirm; in
|
|
@@ -22840,11 +23256,8 @@ const ensureRepoClean = ({ projectRoot, allowDirty, label }) => Effect.gen(funct
|
|
|
22840
23256
|
const preview = dirty.slice(0, MAX_FILES_SHOWN).join("\n ");
|
|
22841
23257
|
const overflow = dirty.length > MAX_FILES_SHOWN ? `\n ... and ${String(dirty.length - MAX_FILES_SHOWN)} more` : "";
|
|
22842
23258
|
yield* Console.error(`Uncommitted changes (${String(dirty.length)} file(s)):\n ${preview}${overflow}`);
|
|
22843
|
-
if (!(yield* InteractiveMode).allow) {
|
|
22844
|
-
|
|
22845
|
-
return;
|
|
22846
|
-
}
|
|
22847
|
-
if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
|
|
23259
|
+
if (!(yield* InteractiveMode).allow) return yield* new DirtyRepoError({ message: `Refusing to ${label} with a dirty working tree. Commit your changes or pass --allow-dirty.` });
|
|
23260
|
+
if (!(yield* promptConfirm(`Continue ${label} with uncommitted changes?`, { initialValue: false }))) return yield* new DirtyRepoError({ message: `${label} cancelled by user.` });
|
|
22848
23261
|
});
|
|
22849
23262
|
|
|
22850
23263
|
//#endregion
|
|
@@ -22984,7 +23397,7 @@ const postTokenRequest = (tokenUri, jwt) => Effect.tryPromise({
|
|
|
22984
23397
|
});
|
|
22985
23398
|
const exchangeJwtForAccessToken = (tokenUri, jwt) => Effect.gen(function* () {
|
|
22986
23399
|
const result = yield* postTokenRequest(tokenUri, jwt);
|
|
22987
|
-
if (!result.ok) return yield*
|
|
23400
|
+
if (!result.ok) return yield* new GooglePlayAuthError({ message: `OAuth token exchange failed: ${String(result.status)} ${result.text}` });
|
|
22988
23401
|
const json = yield* Effect.try({
|
|
22989
23402
|
try: () => JSON.parse(result.text),
|
|
22990
23403
|
catch: (cause) => new GooglePlayAuthError({
|
|
@@ -23009,7 +23422,7 @@ const acquireGooglePlayAccessToken = (serviceAccountJson) => Effect.gen(function
|
|
|
23009
23422
|
message: "Service account JSON missing required fields (type, client_email, private_key)",
|
|
23010
23423
|
cause
|
|
23011
23424
|
})));
|
|
23012
|
-
if (parsed.type !== "service_account") return yield*
|
|
23425
|
+
if (parsed.type !== "service_account") return yield* new GooglePlayAuthError({ message: `Service account JSON has wrong type: ${parsed.type}` });
|
|
23013
23426
|
const tokenUri = parsed.token_uri ?? GOOGLE_OAUTH_TOKEN_URL;
|
|
23014
23427
|
return {
|
|
23015
23428
|
accessToken: yield* exchangeJwtForAccessToken(tokenUri, yield* signJwt(yield* importPrivateKey(parsed.private_key), buildJwtAssertion({
|
|
@@ -23049,10 +23462,10 @@ const performFetch = (params) => Effect.tryPromise({
|
|
|
23049
23462
|
});
|
|
23050
23463
|
const callJsonRaw = (params) => Effect.gen(function* () {
|
|
23051
23464
|
const result = yield* performFetch(params);
|
|
23052
|
-
if (!result.ok) return yield*
|
|
23465
|
+
if (!result.ok) return yield* new GooglePlayApiError({
|
|
23053
23466
|
message: `${params.label} failed: ${String(result.status)} ${result.text}`,
|
|
23054
23467
|
httpStatus: result.status
|
|
23055
|
-
})
|
|
23468
|
+
});
|
|
23056
23469
|
return yield* Effect.try({
|
|
23057
23470
|
try: () => result.text === "" ? {} : JSON.parse(result.text),
|
|
23058
23471
|
catch: (cause) => new GooglePlayApiError({
|
|
@@ -23107,10 +23520,10 @@ const performBundleUpload = (params) => Effect.tryPromise({
|
|
|
23107
23520
|
});
|
|
23108
23521
|
const uploadBundle = (params) => Effect.gen(function* () {
|
|
23109
23522
|
const result = yield* performBundleUpload(params);
|
|
23110
|
-
if (!result.ok) return yield*
|
|
23523
|
+
if (!result.ok) return yield* new GooglePlayApiError({
|
|
23111
23524
|
message: `edits.bundles.upload failed: ${String(result.status)} ${result.text}`,
|
|
23112
23525
|
httpStatus: result.status
|
|
23113
|
-
})
|
|
23526
|
+
});
|
|
23114
23527
|
const raw = yield* Effect.try({
|
|
23115
23528
|
try: () => JSON.parse(result.text),
|
|
23116
23529
|
catch: (cause) => new GooglePlayApiError({
|
|
@@ -23301,10 +23714,10 @@ const fetchArchiveOverHttp = (url) => Effect.gen(function* () {
|
|
|
23301
23714
|
message: `Failed to download AAB from ${url}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
23302
23715
|
})
|
|
23303
23716
|
});
|
|
23304
|
-
if (!result.ok || result.bytes === null) return yield*
|
|
23717
|
+
if (!result.ok || result.bytes === null) return yield* new CliSubmitError({
|
|
23305
23718
|
code: "SUBMISSION_ARCHIVE_DOWNLOAD_FAILED",
|
|
23306
23719
|
message: `HTTP ${String(result.status)} fetching archive at ${url}`
|
|
23307
|
-
})
|
|
23720
|
+
});
|
|
23308
23721
|
return result.bytes;
|
|
23309
23722
|
});
|
|
23310
23723
|
const readArchiveBytes = (archive) => archive.source === "path" ? Effect.map(readLocalFile(archive.value, "SUBMISSION_ARCHIVE_READ_FAILED", (cause) => `Failed to read AAB at ${archive.value}: ${cause instanceof Error ? cause.message : String(cause)}`), (buf) => new Uint8Array(buf)) : fetchArchiveOverHttp(archive.value);
|
|
@@ -23379,10 +23792,10 @@ const runGooglePlayPipeline = (params) => Effect.gen(function* () {
|
|
|
23379
23792
|
});
|
|
23380
23793
|
const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
23381
23794
|
const { applicationId } = inputs.androidProfile;
|
|
23382
|
-
if (applicationId === void 0) return yield*
|
|
23795
|
+
if (applicationId === void 0) return yield* new CliSubmitError({
|
|
23383
23796
|
code: "SUBMISSION_ANDROID_APP_ID_MISSING",
|
|
23384
23797
|
message: "Android submit profile requires applicationId — set submit.<profile>.android.applicationId in eas.json"
|
|
23385
|
-
})
|
|
23798
|
+
});
|
|
23386
23799
|
const serviceAccountJson = yield* resolveServiceAccountJson({
|
|
23387
23800
|
api: inputs.api,
|
|
23388
23801
|
serviceAccountKeyId: inputs.serviceAccountKeyId,
|
|
@@ -23407,7 +23820,7 @@ const runAndroidGooglePlayUpload = (inputs) => Effect.gen(function* () {
|
|
|
23407
23820
|
errorCode: engineError.code,
|
|
23408
23821
|
errorMessage: engineError.message
|
|
23409
23822
|
});
|
|
23410
|
-
return yield*
|
|
23823
|
+
return yield* engineError;
|
|
23411
23824
|
})));
|
|
23412
23825
|
yield* patchSubmissionStatus(inputs.api, inputs.submissionId, { status: "FINISHED" });
|
|
23413
23826
|
yield* printHuman(`Google Play bundle uploaded (versionCode ${String(result.versionCode)})`);
|
|
@@ -23824,7 +24237,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
23824
24237
|
commit: rawGitContext.commit,
|
|
23825
24238
|
dirty: rawGitContext.dirty
|
|
23826
24239
|
});
|
|
23827
|
-
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.
|
|
24240
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(userCwd, platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
|
|
23828
24241
|
const result = yield* reserveAndUpload(api, {
|
|
23829
24242
|
target,
|
|
23830
24243
|
projectId,
|
|
@@ -24179,10 +24592,7 @@ const downloadCommand$1 = defineCommand({
|
|
|
24179
24592
|
const fs = yield* FileSystem.FileSystem;
|
|
24180
24593
|
const cwd = yield* (yield* CliRuntime).cwd;
|
|
24181
24594
|
const { artifact } = yield* api.builds.get({ path: { id: args.id } });
|
|
24182
|
-
if (!artifact) {
|
|
24183
|
-
yield* Effect.fail(new UploadFailedError({ message: `Build ${args.id} has no artifact yet.` }));
|
|
24184
|
-
return;
|
|
24185
|
-
}
|
|
24595
|
+
if (!artifact) return yield* new UploadFailedError({ message: `Build ${args.id} has no artifact yet.` });
|
|
24186
24596
|
const link = yield* api.builds.getInstallLink({ path: { id: args.id } });
|
|
24187
24597
|
const ext = artifact.format;
|
|
24188
24598
|
const outputPath = args.output ?? path.join(cwd, `${args.id}.${ext}`);
|
|
@@ -24455,7 +24865,7 @@ const runInherit = (step, bin, ...args) => Command.exitCode(Command.make(bin, ..
|
|
|
24455
24865
|
* Locate a tool on PATH. Returns the absolute path or fails with NativeRunError.
|
|
24456
24866
|
*/
|
|
24457
24867
|
const which = (bin) => Effect.gen(function* () {
|
|
24458
|
-
const trimmed = (yield* Command.string(Command.make("which", bin)).pipe(Effect.
|
|
24868
|
+
const trimmed = (yield* Command.string(Command.make("which", bin)).pipe(Effect.mapError(() => new NativeRunError({ message: `${bin} not found in PATH` })))).trim();
|
|
24459
24869
|
if (trimmed === "") return yield* new NativeRunError({ message: `${bin} not found in PATH` });
|
|
24460
24870
|
return trimmed;
|
|
24461
24871
|
});
|
|
@@ -24474,7 +24884,7 @@ const findAppBundle = (root) => Effect.gen(function* () {
|
|
|
24474
24884
|
const fs = yield* FileSystem.FileSystem;
|
|
24475
24885
|
const candidates = [root, path.join(root, "Payload")];
|
|
24476
24886
|
for (const candidate of candidates) {
|
|
24477
|
-
const app = (yield* fs.readDirectory(candidate).pipe(Effect.
|
|
24887
|
+
const app = (yield* fs.readDirectory(candidate).pipe(Effect.orElseSucceed(() => []))).find((entry) => entry.endsWith(".app"));
|
|
24478
24888
|
if (app) return path.join(candidate, app);
|
|
24479
24889
|
}
|
|
24480
24890
|
return yield* new NativeRunError({ message: `No .app bundle found inside ${root} or ${path.join(root, "Payload")}.` });
|
|
@@ -24553,8 +24963,8 @@ const installAndLaunchIosDevice = (params) => Effect.gen(function* () {
|
|
|
24553
24963
|
* fall back to a CLI flag.
|
|
24554
24964
|
*/
|
|
24555
24965
|
const tryReadApkPackageWith = (bin, apkPath) => Effect.gen(function* () {
|
|
24556
|
-
if (!(yield* which(bin).pipe(Effect.
|
|
24557
|
-
const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.
|
|
24966
|
+
if (!(yield* which(bin).pipe(Effect.orElseSucceed(() => null)))) return;
|
|
24967
|
+
const raw = yield* execCapture(`${bin} dump`, bin, "dump", "badging", apkPath).pipe(Effect.orElseSucceed(() => null));
|
|
24558
24968
|
if (!raw) return;
|
|
24559
24969
|
return /package: name='(?<packageName>[^']+)'/u.exec(raw)?.[1];
|
|
24560
24970
|
});
|
|
@@ -24626,10 +25036,7 @@ const runIosSimulator = (params) => Effect.gen(function* () {
|
|
|
24626
25036
|
});
|
|
24627
25037
|
const runIosDevice = (params) => Effect.gen(function* () {
|
|
24628
25038
|
const { deviceSelector } = params;
|
|
24629
|
-
if (deviceSelector === void 0) {
|
|
24630
|
-
yield* Effect.fail(new InvalidArgumentError({ message: "Pass --device-id <udid>. Run `xcrun devicectl list devices` to list connected devices." }));
|
|
24631
|
-
return;
|
|
24632
|
-
}
|
|
25039
|
+
if (deviceSelector === void 0) return yield* new InvalidArgumentError({ message: "Pass --device-id <udid>. Run `xcrun devicectl list devices` to list connected devices." });
|
|
24633
25040
|
const bundleId = yield* readBundleIdFromApp(yield* findAppBundle(yield* extractIosArtifact({
|
|
24634
25041
|
tempDir: params.tempDir,
|
|
24635
25042
|
artifactPath: params.artifactPath,
|
|
@@ -24654,21 +25061,12 @@ const runIos = (params) => {
|
|
|
24654
25061
|
return Effect.fail(new NativeRunError({ message: `Cannot install ${params.format} on iOS; only tar.gz (simulator) or ipa are supported.` }));
|
|
24655
25062
|
};
|
|
24656
25063
|
const runAndroid = (params) => Effect.gen(function* () {
|
|
24657
|
-
if (params.format === "aab") {
|
|
24658
|
-
|
|
24659
|
-
return;
|
|
24660
|
-
}
|
|
24661
|
-
if (params.format !== "apk") {
|
|
24662
|
-
yield* Effect.fail(new NativeRunError({ message: `Cannot install ${params.format} on Android; only apk is supported.` }));
|
|
24663
|
-
return;
|
|
24664
|
-
}
|
|
25064
|
+
if (params.format === "aab") return yield* new InvalidArgumentError({ message: ".aab artifacts cannot be installed directly. Use bundletool to convert to apks, or download the play-store APK." });
|
|
25065
|
+
if (params.format !== "apk") return yield* new NativeRunError({ message: `Cannot install ${params.format} on Android; only apk is supported.` });
|
|
24665
25066
|
const device = yield* pickAndroidDevice(params.emulatorSelector);
|
|
24666
25067
|
const detected = yield* readApkPackageName(params.artifactPath);
|
|
24667
25068
|
const packageName = params.packageOverride ?? detected;
|
|
24668
|
-
if (!packageName) {
|
|
24669
|
-
yield* Effect.fail(new InvalidArgumentError({ message: "Could not detect APK package name (aapt/aapt2 not on PATH). Pass --package <name> explicitly." }));
|
|
24670
|
-
return;
|
|
24671
|
-
}
|
|
25069
|
+
if (!packageName) return yield* new InvalidArgumentError({ message: "Could not detect APK package name (aapt/aapt2 not on PATH). Pass --package <name> explicitly." });
|
|
24672
25070
|
yield* printHuman(`Installing on Android device ${device.serial}...`);
|
|
24673
25071
|
yield* installAndLaunchAndroid({
|
|
24674
25072
|
serial: device.serial,
|
|
@@ -24733,7 +25131,7 @@ const runCommand$1 = defineCommand({
|
|
|
24733
25131
|
projectId
|
|
24734
25132
|
});
|
|
24735
25133
|
const { artifact } = build;
|
|
24736
|
-
if (!artifact) return yield*
|
|
25134
|
+
if (!artifact) return yield* new UploadFailedError({ message: `Build ${build.id} has no artifact yet.` });
|
|
24737
25135
|
const link = yield* api.builds.getInstallLink({ path: { id: build.id } });
|
|
24738
25136
|
const tempDir = yield* acquireBuildTempDir;
|
|
24739
25137
|
const artifactPath = path.join(tempDir, `artifact.${artifact.format}`);
|
|
@@ -24828,7 +25226,7 @@ const resolveUploadMeta = (params) => Effect.gen(function* () {
|
|
|
24828
25226
|
const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
24829
25227
|
const api = yield* apiClient;
|
|
24830
25228
|
const projectRoot = yield* (yield* CliRuntime).cwd;
|
|
24831
|
-
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
25229
|
+
if (!(yield* (yield* FileSystem.FileSystem).exists(options.artifactPath).pipe(Effect.orElseSucceed(() => false)))) return yield* new ArtifactNotFoundError({ message: `Artifact not found at ${options.artifactPath}.` });
|
|
24832
25230
|
const projectType = yield* detectProjectType({
|
|
24833
25231
|
projectRoot,
|
|
24834
25232
|
override: asProjectType((yield* readBetterUpdateConfig(projectRoot))?.["projectType"])
|
|
@@ -24858,7 +25256,7 @@ const runUploadWorkflow = (options) => Effect.gen(function* () {
|
|
|
24858
25256
|
commit: rawGitContext.commit,
|
|
24859
25257
|
dirty: rawGitContext.dirty
|
|
24860
25258
|
});
|
|
24861
|
-
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.
|
|
25259
|
+
const fingerprintHash = isExpo ? yield* runFingerprintForPlatform(projectRoot, options.platform).pipe(Effect.map((entry) => entry.hash), Effect.orElseSucceed(() => void 0)) : void 0;
|
|
24862
25260
|
const result = yield* reserveAndUpload(api, compact({
|
|
24863
25261
|
target,
|
|
24864
25262
|
projectId,
|
|
@@ -25383,7 +25781,7 @@ const viewCommand$2 = defineCommand({
|
|
|
25383
25781
|
page
|
|
25384
25782
|
} }))]);
|
|
25385
25783
|
const channel = channels.find((entry) => entry.id === args.target) ?? channels.find((entry) => entry.name === args.target);
|
|
25386
|
-
if (!channel) return yield*
|
|
25784
|
+
if (!channel) return yield* new ChannelCommandError({ message: `Channel "${args.target}" not found by ID or name.` });
|
|
25387
25785
|
const branchName = new Map(branches.map((branch) => [branch.id, branch.name])).get(channel.branchId) ?? channel.branchId;
|
|
25388
25786
|
yield* printHumanKeyValue([
|
|
25389
25787
|
["ID", channel.id],
|
|
@@ -25808,7 +26206,7 @@ const announce = (heading) => Effect.gen(function* () {
|
|
|
25808
26206
|
});
|
|
25809
26207
|
const reportError = (label, cause) => Console.log(`✗ ${label}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
25810
26208
|
const safely = (label, effect) => effect.pipe(Effect.catchAll((cause) => reportError(label, cause)), Effect.asVoid);
|
|
25811
|
-
const safePrompt = (effect) => effect.pipe(Effect.
|
|
26209
|
+
const safePrompt = (effect) => effect.pipe(Effect.orElseSucceed(() => BACK));
|
|
25812
26210
|
const promptForBundleConfig = (ctx) => Effect.gen(function* () {
|
|
25813
26211
|
const list = yield* ctx.api.iosBundleConfigurations.list({ path: { projectId: ctx.projectId } });
|
|
25814
26212
|
if (list.items.length === 0) return yield* new MissingCredentialsError({
|
|
@@ -27739,7 +28137,7 @@ const handleCertLimitInteractive = (api, ascApiKeyId, certificateType) => Effect
|
|
|
27739
28137
|
ascApiKeyId,
|
|
27740
28138
|
certificateType
|
|
27741
28139
|
});
|
|
27742
|
-
if (certs.length === 0) return yield*
|
|
28140
|
+
if (certs.length === 0) return yield* new CertificateLimitError({ message: "Apple says the certificate limit is hit but no existing certificates were returned — try again later." });
|
|
27743
28141
|
const toRevoke = yield* promptMultiSelect("Select one or more certificates to revoke before retrying", certs.map((entry) => ({
|
|
27744
28142
|
value: entry.id,
|
|
27745
28143
|
label: `${entry.serialNumber.slice(0, 12)}… (${entry.displayName ?? entry.certificateType}, exp ${entry.expirationDate.slice(0, 10)})`
|
|
@@ -28501,7 +28899,7 @@ const writeArtifact = (fs, projectRoot, relPath, bytes) => Effect.gen(function*
|
|
|
28501
28899
|
const writeText = (fs, projectRoot, relPath, text) => writeArtifact(fs, projectRoot, relPath, new TextEncoder().encode(text));
|
|
28502
28900
|
const ensureGitignoreEntries = (fs, projectRoot, paths) => Effect.gen(function* () {
|
|
28503
28901
|
const filePath = path.join(projectRoot, ".gitignore");
|
|
28504
|
-
const lines = ((yield* fs.exists(filePath).pipe(Effect.
|
|
28902
|
+
const lines = ((yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false))) ? yield* fs.readFileString(filePath).pipe(Effect.orElseSucceed(() => "")) : "").split("\n");
|
|
28505
28903
|
const added = [];
|
|
28506
28904
|
const next = [...lines];
|
|
28507
28905
|
for (const entry of paths) if (!lines.includes(entry)) {
|
|
@@ -29721,6 +30119,7 @@ const devicesCommand = defineCommand({
|
|
|
29721
30119
|
|
|
29722
30120
|
//#endregion
|
|
29723
30121
|
//#region src/commands/doctor.ts
|
|
30122
|
+
var HealthCheckError = class extends Data.TaggedError("HealthCheckError") {};
|
|
29724
30123
|
const pass = (id, name, message) => ({
|
|
29725
30124
|
id,
|
|
29726
30125
|
name,
|
|
@@ -29759,7 +30158,10 @@ const checkServerHealth = Effect.gen(function* () {
|
|
|
29759
30158
|
const url = `${yield* (yield* ConfigStore).getBaseUrl}/api/health`;
|
|
29760
30159
|
const response = yield* Effect.tryPromise({
|
|
29761
30160
|
try: async () => fetch(url, { signal: AbortSignal.timeout(3e3) }),
|
|
29762
|
-
catch: (cause) => new
|
|
30161
|
+
catch: (cause) => new HealthCheckError({
|
|
30162
|
+
message: String(cause),
|
|
30163
|
+
cause
|
|
30164
|
+
})
|
|
29763
30165
|
}).pipe(Effect.either);
|
|
29764
30166
|
if (response._tag === "Left") return fail("health", "Server reachable", `${url} unreachable: ${response.left.message}`);
|
|
29765
30167
|
const res = response.right;
|
|
@@ -29970,7 +30372,7 @@ const getExecTrailingArgv = () => trailing$1;
|
|
|
29970
30372
|
const pullForExec = (api, projectId, environment) => pullEnvVars(api, {
|
|
29971
30373
|
projectId,
|
|
29972
30374
|
environment
|
|
29973
|
-
}).pipe(Effect.
|
|
30375
|
+
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
29974
30376
|
const splitTrailing = (trailing) => {
|
|
29975
30377
|
if (!trailing || trailing.length === 0) return Effect.fail(new InvalidArgumentError({ message: "Pass the command after `--`. Example: `better-update env exec production -- bun run dev`." }));
|
|
29976
30378
|
const [bin, ...rest] = trailing;
|
|
@@ -30795,12 +31197,382 @@ const fingerprintCommand = defineCommand({
|
|
|
30795
31197
|
}
|
|
30796
31198
|
});
|
|
30797
31199
|
|
|
31200
|
+
//#endregion
|
|
31201
|
+
//#region src/commands/groups/helpers.ts
|
|
31202
|
+
const groupErrorExtras = { GroupCommandError: 2 };
|
|
31203
|
+
|
|
31204
|
+
//#endregion
|
|
31205
|
+
//#region src/commands/groups/attach.ts
|
|
31206
|
+
const attachGroupPolicyCommand = defineCommand({
|
|
31207
|
+
meta: {
|
|
31208
|
+
name: "attach",
|
|
31209
|
+
description: "Attach a policy (real or managed:*) to a group; members inherit it"
|
|
31210
|
+
},
|
|
31211
|
+
args: {
|
|
31212
|
+
id: {
|
|
31213
|
+
type: "positional",
|
|
31214
|
+
required: true,
|
|
31215
|
+
description: "Group ID"
|
|
31216
|
+
},
|
|
31217
|
+
"policy-id": {
|
|
31218
|
+
type: "string",
|
|
31219
|
+
required: true,
|
|
31220
|
+
description: "Policy ID to attach (real id or managed preset like managed:admin)"
|
|
31221
|
+
}
|
|
31222
|
+
},
|
|
31223
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31224
|
+
const attachment = yield* (yield* apiClient)["policy-attachments"].attachToGroup({
|
|
31225
|
+
path: { id: args.id },
|
|
31226
|
+
payload: { policyId: args["policy-id"] }
|
|
31227
|
+
});
|
|
31228
|
+
yield* printHumanKeyValue([
|
|
31229
|
+
["Attachment ID", attachment.id],
|
|
31230
|
+
["Group ID", attachment.principalId],
|
|
31231
|
+
["Policy ID", attachment.policyId],
|
|
31232
|
+
["Created", attachment.createdAt]
|
|
31233
|
+
]);
|
|
31234
|
+
return attachment;
|
|
31235
|
+
}), {
|
|
31236
|
+
exits: groupErrorExtras,
|
|
31237
|
+
json: "value"
|
|
31238
|
+
})
|
|
31239
|
+
});
|
|
31240
|
+
|
|
31241
|
+
//#endregion
|
|
31242
|
+
//#region src/commands/groups/create.ts
|
|
31243
|
+
const createGroupCommand = defineCommand({
|
|
31244
|
+
meta: {
|
|
31245
|
+
name: "create",
|
|
31246
|
+
description: "Create a member group"
|
|
31247
|
+
},
|
|
31248
|
+
args: {
|
|
31249
|
+
name: {
|
|
31250
|
+
type: "string",
|
|
31251
|
+
required: true,
|
|
31252
|
+
description: "Group display name"
|
|
31253
|
+
},
|
|
31254
|
+
description: {
|
|
31255
|
+
type: "string",
|
|
31256
|
+
description: "Optional human description"
|
|
31257
|
+
}
|
|
31258
|
+
},
|
|
31259
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31260
|
+
const group = yield* (yield* apiClient).groups.create({ payload: {
|
|
31261
|
+
name: args.name,
|
|
31262
|
+
...compact({ description: args.description })
|
|
31263
|
+
} });
|
|
31264
|
+
yield* printHumanKeyValue([
|
|
31265
|
+
["ID", group.id],
|
|
31266
|
+
["Name", group.name],
|
|
31267
|
+
["Description", group.description ?? "-"],
|
|
31268
|
+
["Created", group.createdAt]
|
|
31269
|
+
]);
|
|
31270
|
+
return group;
|
|
31271
|
+
}), {
|
|
31272
|
+
exits: groupErrorExtras,
|
|
31273
|
+
json: "value"
|
|
31274
|
+
})
|
|
31275
|
+
});
|
|
31276
|
+
|
|
31277
|
+
//#endregion
|
|
31278
|
+
//#region src/commands/groups/delete.ts
|
|
31279
|
+
const deleteGroupCommand = defineCommand({
|
|
31280
|
+
meta: {
|
|
31281
|
+
name: "delete",
|
|
31282
|
+
description: "Delete a group and sweep its memberships and policy attachments"
|
|
31283
|
+
},
|
|
31284
|
+
args: {
|
|
31285
|
+
id: {
|
|
31286
|
+
type: "positional",
|
|
31287
|
+
required: true,
|
|
31288
|
+
description: "Group ID"
|
|
31289
|
+
},
|
|
31290
|
+
yes: {
|
|
31291
|
+
type: "boolean",
|
|
31292
|
+
description: "Skip confirmation prompt"
|
|
31293
|
+
}
|
|
31294
|
+
},
|
|
31295
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31296
|
+
if (!args.yes) {
|
|
31297
|
+
if (!(yield* promptConfirm(`Delete group ${args.id}?`, { initialValue: false }))) {
|
|
31298
|
+
yield* printHuman("Cancelled.");
|
|
31299
|
+
return;
|
|
31300
|
+
}
|
|
31301
|
+
}
|
|
31302
|
+
yield* (yield* apiClient).groups.delete({ path: { id: args.id } });
|
|
31303
|
+
yield* printHuman(`Deleted group ${args.id}.`);
|
|
31304
|
+
return {
|
|
31305
|
+
id: args.id,
|
|
31306
|
+
deleted: true
|
|
31307
|
+
};
|
|
31308
|
+
}), {
|
|
31309
|
+
exits: groupErrorExtras,
|
|
31310
|
+
json: "value"
|
|
31311
|
+
})
|
|
31312
|
+
});
|
|
31313
|
+
|
|
31314
|
+
//#endregion
|
|
31315
|
+
//#region src/commands/groups/detach.ts
|
|
31316
|
+
const detachGroupPolicyCommand = defineCommand({
|
|
31317
|
+
meta: {
|
|
31318
|
+
name: "detach",
|
|
31319
|
+
description: "Remove a policy attachment from a group"
|
|
31320
|
+
},
|
|
31321
|
+
args: {
|
|
31322
|
+
id: {
|
|
31323
|
+
type: "positional",
|
|
31324
|
+
required: true,
|
|
31325
|
+
description: "Group ID"
|
|
31326
|
+
},
|
|
31327
|
+
"policy-id": {
|
|
31328
|
+
type: "string",
|
|
31329
|
+
required: true,
|
|
31330
|
+
description: "Policy ID to detach (real id or managed preset like managed:admin)"
|
|
31331
|
+
}
|
|
31332
|
+
},
|
|
31333
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31334
|
+
yield* (yield* apiClient)["policy-attachments"].detachFromGroup({ path: {
|
|
31335
|
+
id: args.id,
|
|
31336
|
+
policyId: encodeURIComponent(args["policy-id"])
|
|
31337
|
+
} });
|
|
31338
|
+
yield* printHuman(`Detached policy ${args["policy-id"]} from group ${args.id}.`);
|
|
31339
|
+
return {
|
|
31340
|
+
groupId: args.id,
|
|
31341
|
+
policyId: args["policy-id"],
|
|
31342
|
+
detached: true
|
|
31343
|
+
};
|
|
31344
|
+
}), {
|
|
31345
|
+
exits: groupErrorExtras,
|
|
31346
|
+
json: "value"
|
|
31347
|
+
})
|
|
31348
|
+
});
|
|
31349
|
+
|
|
31350
|
+
//#endregion
|
|
31351
|
+
//#region src/commands/groups/list.ts
|
|
31352
|
+
const listGroupsCommand = defineCommand({
|
|
31353
|
+
meta: {
|
|
31354
|
+
name: "list",
|
|
31355
|
+
description: "List member groups in the active organization"
|
|
31356
|
+
},
|
|
31357
|
+
run: async () => runEffect(Effect.gen(function* () {
|
|
31358
|
+
const result = yield* (yield* apiClient).groups.list();
|
|
31359
|
+
yield* printHumanTable([
|
|
31360
|
+
"ID",
|
|
31361
|
+
"Name",
|
|
31362
|
+
"Description",
|
|
31363
|
+
"Created"
|
|
31364
|
+
], result.items.map((group) => [
|
|
31365
|
+
group.id,
|
|
31366
|
+
group.name,
|
|
31367
|
+
group.description ?? "-",
|
|
31368
|
+
group.createdAt
|
|
31369
|
+
]));
|
|
31370
|
+
return result;
|
|
31371
|
+
}), {
|
|
31372
|
+
exits: groupErrorExtras,
|
|
31373
|
+
json: "value"
|
|
31374
|
+
})
|
|
31375
|
+
});
|
|
31376
|
+
|
|
31377
|
+
//#endregion
|
|
31378
|
+
//#region src/commands/groups/members/add.ts
|
|
31379
|
+
const addGroupMemberCommand = defineCommand({
|
|
31380
|
+
meta: {
|
|
31381
|
+
name: "add",
|
|
31382
|
+
description: "Add an organization member to a group"
|
|
31383
|
+
},
|
|
31384
|
+
args: {
|
|
31385
|
+
id: {
|
|
31386
|
+
type: "positional",
|
|
31387
|
+
required: true,
|
|
31388
|
+
description: "Group ID"
|
|
31389
|
+
},
|
|
31390
|
+
"member-id": {
|
|
31391
|
+
type: "string",
|
|
31392
|
+
required: true,
|
|
31393
|
+
description: "Organization member ID to add"
|
|
31394
|
+
}
|
|
31395
|
+
},
|
|
31396
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31397
|
+
const member = yield* (yield* apiClient).groups.addMember({
|
|
31398
|
+
path: { id: args.id },
|
|
31399
|
+
payload: { memberId: args["member-id"] }
|
|
31400
|
+
});
|
|
31401
|
+
yield* printHumanKeyValue([["Member ID", member.memberId], ["Added", member.createdAt]]);
|
|
31402
|
+
return member;
|
|
31403
|
+
}), {
|
|
31404
|
+
exits: groupErrorExtras,
|
|
31405
|
+
json: "value"
|
|
31406
|
+
})
|
|
31407
|
+
});
|
|
31408
|
+
|
|
31409
|
+
//#endregion
|
|
31410
|
+
//#region src/commands/groups/members/remove.ts
|
|
31411
|
+
const removeGroupMemberCommand = defineCommand({
|
|
31412
|
+
meta: {
|
|
31413
|
+
name: "remove",
|
|
31414
|
+
description: "Remove a member from a group"
|
|
31415
|
+
},
|
|
31416
|
+
args: {
|
|
31417
|
+
id: {
|
|
31418
|
+
type: "positional",
|
|
31419
|
+
required: true,
|
|
31420
|
+
description: "Group ID"
|
|
31421
|
+
},
|
|
31422
|
+
"member-id": {
|
|
31423
|
+
type: "string",
|
|
31424
|
+
required: true,
|
|
31425
|
+
description: "Organization member ID to remove"
|
|
31426
|
+
}
|
|
31427
|
+
},
|
|
31428
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31429
|
+
yield* (yield* apiClient).groups.removeMember({ path: {
|
|
31430
|
+
id: args.id,
|
|
31431
|
+
memberId: args["member-id"]
|
|
31432
|
+
} });
|
|
31433
|
+
yield* printHuman(`Removed member ${args["member-id"]} from group ${args.id}.`);
|
|
31434
|
+
return {
|
|
31435
|
+
groupId: args.id,
|
|
31436
|
+
memberId: args["member-id"],
|
|
31437
|
+
removed: true
|
|
31438
|
+
};
|
|
31439
|
+
}), {
|
|
31440
|
+
exits: groupErrorExtras,
|
|
31441
|
+
json: "value"
|
|
31442
|
+
})
|
|
31443
|
+
});
|
|
31444
|
+
|
|
31445
|
+
//#endregion
|
|
31446
|
+
//#region src/commands/groups/members/index.ts
|
|
31447
|
+
const listGroupMembersCommand = defineCommand({
|
|
31448
|
+
meta: {
|
|
31449
|
+
name: "list",
|
|
31450
|
+
description: "List the members belonging to a group"
|
|
31451
|
+
},
|
|
31452
|
+
args: { id: {
|
|
31453
|
+
type: "positional",
|
|
31454
|
+
required: true,
|
|
31455
|
+
description: "Group ID"
|
|
31456
|
+
} },
|
|
31457
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31458
|
+
const result = yield* (yield* apiClient).groups.listMembers({ path: { id: args.id } });
|
|
31459
|
+
yield* printHumanTable(["Member ID", "Added"], result.items.map((member) => [member.memberId, member.createdAt]));
|
|
31460
|
+
return result;
|
|
31461
|
+
}), {
|
|
31462
|
+
exits: groupErrorExtras,
|
|
31463
|
+
json: "value"
|
|
31464
|
+
})
|
|
31465
|
+
});
|
|
31466
|
+
const membersCommand = defineCommand({
|
|
31467
|
+
meta: {
|
|
31468
|
+
name: "members",
|
|
31469
|
+
description: "Manage the members of a group"
|
|
31470
|
+
},
|
|
31471
|
+
subCommands: {
|
|
31472
|
+
list: listGroupMembersCommand,
|
|
31473
|
+
add: addGroupMemberCommand,
|
|
31474
|
+
remove: removeGroupMemberCommand
|
|
31475
|
+
}
|
|
31476
|
+
});
|
|
31477
|
+
|
|
31478
|
+
//#endregion
|
|
31479
|
+
//#region src/commands/groups/policies.ts
|
|
31480
|
+
const listGroupPoliciesCommand = defineCommand({
|
|
31481
|
+
meta: {
|
|
31482
|
+
name: "policies",
|
|
31483
|
+
description: "List policies attached to a group"
|
|
31484
|
+
},
|
|
31485
|
+
args: { id: {
|
|
31486
|
+
type: "positional",
|
|
31487
|
+
required: true,
|
|
31488
|
+
description: "Group ID"
|
|
31489
|
+
} },
|
|
31490
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31491
|
+
const result = yield* (yield* apiClient)["policy-attachments"].listForGroup({ path: { id: args.id } });
|
|
31492
|
+
yield* printHumanTable([
|
|
31493
|
+
"Attachment ID",
|
|
31494
|
+
"Policy ID",
|
|
31495
|
+
"Created"
|
|
31496
|
+
], result.items.map((attachment) => [
|
|
31497
|
+
attachment.id,
|
|
31498
|
+
attachment.policyId,
|
|
31499
|
+
attachment.createdAt
|
|
31500
|
+
]));
|
|
31501
|
+
return result;
|
|
31502
|
+
}), {
|
|
31503
|
+
exits: groupErrorExtras,
|
|
31504
|
+
json: "value"
|
|
31505
|
+
})
|
|
31506
|
+
});
|
|
31507
|
+
|
|
31508
|
+
//#endregion
|
|
31509
|
+
//#region src/commands/groups/update.ts
|
|
31510
|
+
const updateGroupCommand = defineCommand({
|
|
31511
|
+
meta: {
|
|
31512
|
+
name: "update",
|
|
31513
|
+
description: "Update a group's name or description"
|
|
31514
|
+
},
|
|
31515
|
+
args: {
|
|
31516
|
+
id: {
|
|
31517
|
+
type: "positional",
|
|
31518
|
+
required: true,
|
|
31519
|
+
description: "Group ID"
|
|
31520
|
+
},
|
|
31521
|
+
name: {
|
|
31522
|
+
type: "string",
|
|
31523
|
+
description: "New display name"
|
|
31524
|
+
},
|
|
31525
|
+
description: {
|
|
31526
|
+
type: "string",
|
|
31527
|
+
description: "New description"
|
|
31528
|
+
}
|
|
31529
|
+
},
|
|
31530
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31531
|
+
const group = yield* (yield* apiClient).groups.update({
|
|
31532
|
+
path: { id: args.id },
|
|
31533
|
+
payload: compact({
|
|
31534
|
+
name: args.name,
|
|
31535
|
+
description: args.description
|
|
31536
|
+
})
|
|
31537
|
+
});
|
|
31538
|
+
yield* printHumanKeyValue([
|
|
31539
|
+
["ID", group.id],
|
|
31540
|
+
["Name", group.name],
|
|
31541
|
+
["Description", group.description ?? "-"],
|
|
31542
|
+
["Updated", group.updatedAt ?? "-"]
|
|
31543
|
+
]);
|
|
31544
|
+
return group;
|
|
31545
|
+
}), {
|
|
31546
|
+
exits: groupErrorExtras,
|
|
31547
|
+
json: "value"
|
|
31548
|
+
})
|
|
31549
|
+
});
|
|
31550
|
+
|
|
31551
|
+
//#endregion
|
|
31552
|
+
//#region src/commands/groups/index.ts
|
|
31553
|
+
const groupsCommand = defineCommand({
|
|
31554
|
+
meta: {
|
|
31555
|
+
name: "groups",
|
|
31556
|
+
description: "Manage member groups for collective policy attachment"
|
|
31557
|
+
},
|
|
31558
|
+
subCommands: {
|
|
31559
|
+
list: listGroupsCommand,
|
|
31560
|
+
create: createGroupCommand,
|
|
31561
|
+
update: updateGroupCommand,
|
|
31562
|
+
delete: deleteGroupCommand,
|
|
31563
|
+
members: membersCommand,
|
|
31564
|
+
policies: listGroupPoliciesCommand,
|
|
31565
|
+
attach: attachGroupPolicyCommand,
|
|
31566
|
+
detach: detachGroupPolicyCommand
|
|
31567
|
+
}
|
|
31568
|
+
});
|
|
31569
|
+
|
|
30798
31570
|
//#endregion
|
|
30799
31571
|
//#region src/commands/init.ts
|
|
30800
31572
|
const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
|
|
30801
31573
|
const existingId = config.extra?.betterUpdate?.projectId;
|
|
30802
31574
|
if (typeof existingId !== "string" || existingId.length === 0) return "no-link";
|
|
30803
|
-
const project = yield* api.projects.get({ path: { id: existingId } }).pipe(Effect.
|
|
31575
|
+
const project = yield* api.projects.get({ path: { id: existingId } }).pipe(Effect.orElseSucceed(() => void 0));
|
|
30804
31576
|
if (project === void 0) {
|
|
30805
31577
|
yield* printHuman(`Existing projectId "${existingId}" not found on server. Re-linking by local slug "${localSlug}".`);
|
|
30806
31578
|
return "stale";
|
|
@@ -30820,10 +31592,10 @@ const checkExistingLink = (api, config, localSlug) => Effect.gen(function* () {
|
|
|
30820
31592
|
const slugify = (value) => value.toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "");
|
|
30821
31593
|
/** Best-effort `name` from the local package.json, or undefined when absent. */
|
|
30822
31594
|
const readPackageJsonName = (projectRoot) => Effect.gen(function* () {
|
|
30823
|
-
const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.
|
|
31595
|
+
const content = yield* (yield* FileSystem.FileSystem).readFileString(path.join(projectRoot, "package.json")).pipe(Effect.orElseSucceed(() => ""));
|
|
30824
31596
|
if (content.length === 0) return;
|
|
30825
|
-
const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.
|
|
30826
|
-
const name = isRecord(parsed) ? parsed["name"] : void 0;
|
|
31597
|
+
const parsed = yield* Effect.try(() => JSON.parse(content)).pipe(Effect.orElseSucceed(() => void 0));
|
|
31598
|
+
const name = isRecord$1(parsed) ? parsed["name"] : void 0;
|
|
30827
31599
|
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
30828
31600
|
});
|
|
30829
31601
|
/**
|
|
@@ -31064,6 +31836,236 @@ const openCommand = defineCommand({
|
|
|
31064
31836
|
}))
|
|
31065
31837
|
});
|
|
31066
31838
|
|
|
31839
|
+
//#endregion
|
|
31840
|
+
//#region src/commands/policies/helpers.ts
|
|
31841
|
+
var PolicyCommandError = class extends Data.TaggedError("PolicyCommandError") {};
|
|
31842
|
+
const policyErrorExtras = { PolicyCommandError: 2 };
|
|
31843
|
+
/** A managed preset id is virtual + read-only; its id is prefixed with `managed:`. */
|
|
31844
|
+
const isManagedPolicyId = (id) => id.startsWith("managed:");
|
|
31845
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
31846
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31847
|
+
/**
|
|
31848
|
+
* Parse + shape-validate a `--document` JSON string client-side so a malformed
|
|
31849
|
+
* document fails with a clear local message before any network round-trip. The
|
|
31850
|
+
* server still re-validates action tokens against the real vocabulary and the
|
|
31851
|
+
* selectors against the shared grammar; this only guards the JSON shape and the
|
|
31852
|
+
* action/selector token SHAPE via the contract's pure validators.
|
|
31853
|
+
*
|
|
31854
|
+
* Expected shape:
|
|
31855
|
+
* { "statements": [ { "effect": "allow"|"deny", "actions": ["project:read"|"*"], "resources": ["*"|"project/A"] } ] }
|
|
31856
|
+
*/
|
|
31857
|
+
const parsePolicyDocument = (raw) => Effect.gen(function* () {
|
|
31858
|
+
const parsed = yield* Effect.try({
|
|
31859
|
+
try: () => JSON.parse(raw),
|
|
31860
|
+
catch: () => new PolicyCommandError({ message: "The --document value is not valid JSON. Pass a JSON object string." })
|
|
31861
|
+
});
|
|
31862
|
+
if (!isRecord(parsed) || !Array.isArray(parsed["statements"])) return yield* new PolicyCommandError({ message: "The document must be a JSON object with a \"statements\" array. Example: {\"statements\":[{\"effect\":\"allow\",\"actions\":[\"project:read\"],\"resources\":[\"*\"]}]}" });
|
|
31863
|
+
const statements = [];
|
|
31864
|
+
for (const [index, entry] of parsed["statements"].entries()) {
|
|
31865
|
+
const statement = yield* validateStatement(entry, index);
|
|
31866
|
+
statements.push(statement);
|
|
31867
|
+
}
|
|
31868
|
+
if (statements.length === 0) return yield* new PolicyCommandError({ message: "The document must contain at least one statement." });
|
|
31869
|
+
return { statements };
|
|
31870
|
+
});
|
|
31871
|
+
const validateStatement = (entry, index) => Effect.gen(function* () {
|
|
31872
|
+
const at = `statements[${index}]`;
|
|
31873
|
+
if (!isRecord(entry)) return yield* new PolicyCommandError({ message: `${at} must be a JSON object.` });
|
|
31874
|
+
const { effect } = entry;
|
|
31875
|
+
if (effect !== "allow" && effect !== "deny") return yield* new PolicyCommandError({ message: `${at}.effect must be "allow" or "deny".` });
|
|
31876
|
+
const { actions } = entry;
|
|
31877
|
+
if (!isStringArray(actions) || actions.length === 0) return yield* new PolicyCommandError({ message: `${at}.actions must be a non-empty array of action-token strings.` });
|
|
31878
|
+
const badAction = actions.find((token) => !isValidActionTokenShape(token));
|
|
31879
|
+
if (badAction !== void 0) return yield* new PolicyCommandError({ message: `${at}.actions has an invalid token "${badAction}". Use "*", "<resource>:*", or "<resource>:<action>".` });
|
|
31880
|
+
const { resources } = entry;
|
|
31881
|
+
if (!isStringArray(resources) || resources.length === 0) return yield* new PolicyCommandError({ message: `${at}.resources must be a non-empty array of selector strings.` });
|
|
31882
|
+
const badResource = resources.find((selector) => !isValidSelector(selector));
|
|
31883
|
+
if (badResource !== void 0) return yield* new PolicyCommandError({ message: `${at}.resources has an invalid selector "${badResource}". Use "*" or slash-joined segments like "project/A".` });
|
|
31884
|
+
const inertResource = resources.find((selector) => !isCanonicalSelector(selector));
|
|
31885
|
+
if (inertResource !== void 0) return yield* new PolicyCommandError({ message: `${at}.resources selector "${inertResource}" matches no known resource path. Use segments like "project/{id}/channel/{id}" or "project/{id}/env/{env}".` });
|
|
31886
|
+
return {
|
|
31887
|
+
effect,
|
|
31888
|
+
actions,
|
|
31889
|
+
resources
|
|
31890
|
+
};
|
|
31891
|
+
});
|
|
31892
|
+
|
|
31893
|
+
//#endregion
|
|
31894
|
+
//#region src/commands/policies/create.ts
|
|
31895
|
+
const DOCUMENT_HELP = "JSON document string: {\"statements\":[{\"effect\":\"allow\"|\"deny\",\"actions\":[\"<resource>:<action>\"|\"<resource>:*\"|\"*\"],\"resources\":[\"*\"|\"project/A\"|\"project/*/env/production\"]}]}";
|
|
31896
|
+
const createPolicyCommand = defineCommand({
|
|
31897
|
+
meta: {
|
|
31898
|
+
name: "create",
|
|
31899
|
+
description: "Create a named IAM policy from a JSON document"
|
|
31900
|
+
},
|
|
31901
|
+
args: {
|
|
31902
|
+
name: {
|
|
31903
|
+
type: "string",
|
|
31904
|
+
required: true,
|
|
31905
|
+
description: "Policy display name"
|
|
31906
|
+
},
|
|
31907
|
+
description: {
|
|
31908
|
+
type: "string",
|
|
31909
|
+
description: "Optional human description"
|
|
31910
|
+
},
|
|
31911
|
+
document: {
|
|
31912
|
+
type: "string",
|
|
31913
|
+
required: true,
|
|
31914
|
+
description: DOCUMENT_HELP
|
|
31915
|
+
}
|
|
31916
|
+
},
|
|
31917
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31918
|
+
const document = yield* parsePolicyDocument(args.document);
|
|
31919
|
+
const policy = yield* (yield* apiClient).policies.create({ payload: {
|
|
31920
|
+
name: args.name,
|
|
31921
|
+
document,
|
|
31922
|
+
...compact({ description: args.description })
|
|
31923
|
+
} });
|
|
31924
|
+
yield* printHumanKeyValue([
|
|
31925
|
+
["ID", policy.id],
|
|
31926
|
+
["Name", policy.name],
|
|
31927
|
+
["Description", policy.description ?? "-"],
|
|
31928
|
+
["Statements", String(policy.document.statements.length)],
|
|
31929
|
+
["Created", policy.createdAt]
|
|
31930
|
+
]);
|
|
31931
|
+
return policy;
|
|
31932
|
+
}), {
|
|
31933
|
+
exits: policyErrorExtras,
|
|
31934
|
+
json: "value"
|
|
31935
|
+
})
|
|
31936
|
+
});
|
|
31937
|
+
|
|
31938
|
+
//#endregion
|
|
31939
|
+
//#region src/commands/policies/delete.ts
|
|
31940
|
+
const deletePolicyCommand = defineCommand({
|
|
31941
|
+
meta: {
|
|
31942
|
+
name: "delete",
|
|
31943
|
+
description: "Delete a policy and sweep its attachments (managed presets cannot be deleted)"
|
|
31944
|
+
},
|
|
31945
|
+
args: {
|
|
31946
|
+
id: {
|
|
31947
|
+
type: "positional",
|
|
31948
|
+
required: true,
|
|
31949
|
+
description: "Policy ID"
|
|
31950
|
+
},
|
|
31951
|
+
yes: {
|
|
31952
|
+
type: "boolean",
|
|
31953
|
+
description: "Skip confirmation prompt"
|
|
31954
|
+
}
|
|
31955
|
+
},
|
|
31956
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
31957
|
+
if (isManagedPolicyId(args.id)) return yield* new PolicyCommandError({ message: `Policy "${args.id}" is a managed preset and cannot be deleted.` });
|
|
31958
|
+
if (!args.yes) {
|
|
31959
|
+
if (!(yield* promptConfirm(`Delete policy ${args.id}?`, { initialValue: false }))) {
|
|
31960
|
+
yield* printHuman("Cancelled.");
|
|
31961
|
+
return;
|
|
31962
|
+
}
|
|
31963
|
+
}
|
|
31964
|
+
yield* (yield* apiClient).policies.delete({ path: { id: args.id } });
|
|
31965
|
+
yield* printHuman(`Deleted policy ${args.id}.`);
|
|
31966
|
+
return {
|
|
31967
|
+
id: args.id,
|
|
31968
|
+
deleted: true
|
|
31969
|
+
};
|
|
31970
|
+
}), {
|
|
31971
|
+
exits: policyErrorExtras,
|
|
31972
|
+
json: "value"
|
|
31973
|
+
})
|
|
31974
|
+
});
|
|
31975
|
+
|
|
31976
|
+
//#endregion
|
|
31977
|
+
//#region src/commands/policies/list.ts
|
|
31978
|
+
const listPoliciesCommand = defineCommand({
|
|
31979
|
+
meta: {
|
|
31980
|
+
name: "list",
|
|
31981
|
+
description: "List policies in the active organization (includes read-only managed presets)"
|
|
31982
|
+
},
|
|
31983
|
+
run: async () => runEffect(Effect.gen(function* () {
|
|
31984
|
+
const result = yield* (yield* apiClient).policies.list();
|
|
31985
|
+
yield* printHumanTable([
|
|
31986
|
+
"ID",
|
|
31987
|
+
"Name",
|
|
31988
|
+
"Statements",
|
|
31989
|
+
"Managed"
|
|
31990
|
+
], result.items.map((policy) => [
|
|
31991
|
+
policy.id,
|
|
31992
|
+
policy.name,
|
|
31993
|
+
String(policy.document.statements.length),
|
|
31994
|
+
isManagedPolicyId(policy.id) ? "yes" : "no"
|
|
31995
|
+
]));
|
|
31996
|
+
return result;
|
|
31997
|
+
}), {
|
|
31998
|
+
exits: policyErrorExtras,
|
|
31999
|
+
json: "value"
|
|
32000
|
+
})
|
|
32001
|
+
});
|
|
32002
|
+
|
|
32003
|
+
//#endregion
|
|
32004
|
+
//#region src/commands/policies/update.ts
|
|
32005
|
+
const updatePolicyCommand = defineCommand({
|
|
32006
|
+
meta: {
|
|
32007
|
+
name: "update",
|
|
32008
|
+
description: "Update a policy's name, description, or document (managed presets are read-only)"
|
|
32009
|
+
},
|
|
32010
|
+
args: {
|
|
32011
|
+
id: {
|
|
32012
|
+
type: "positional",
|
|
32013
|
+
required: true,
|
|
32014
|
+
description: "Policy ID"
|
|
32015
|
+
},
|
|
32016
|
+
name: {
|
|
32017
|
+
type: "string",
|
|
32018
|
+
description: "New display name"
|
|
32019
|
+
},
|
|
32020
|
+
description: {
|
|
32021
|
+
type: "string",
|
|
32022
|
+
description: "New description"
|
|
32023
|
+
},
|
|
32024
|
+
document: {
|
|
32025
|
+
type: "string",
|
|
32026
|
+
description: "Replacement JSON document string"
|
|
32027
|
+
}
|
|
32028
|
+
},
|
|
32029
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
32030
|
+
if (isManagedPolicyId(args.id)) return yield* new PolicyCommandError({ message: `Policy "${args.id}" is a managed preset and is read-only. Create a custom policy instead.` });
|
|
32031
|
+
const document = args.document === void 0 ? void 0 : yield* parsePolicyDocument(args.document);
|
|
32032
|
+
const policy = yield* (yield* apiClient).policies.update({
|
|
32033
|
+
path: { id: args.id },
|
|
32034
|
+
payload: compact({
|
|
32035
|
+
name: args.name,
|
|
32036
|
+
description: args.description,
|
|
32037
|
+
document
|
|
32038
|
+
})
|
|
32039
|
+
});
|
|
32040
|
+
yield* printHumanKeyValue([
|
|
32041
|
+
["ID", policy.id],
|
|
32042
|
+
["Name", policy.name],
|
|
32043
|
+
["Description", policy.description ?? "-"],
|
|
32044
|
+
["Statements", String(policy.document.statements.length)],
|
|
32045
|
+
["Updated", policy.updatedAt ?? "-"]
|
|
32046
|
+
]);
|
|
32047
|
+
return policy;
|
|
32048
|
+
}), {
|
|
32049
|
+
exits: policyErrorExtras,
|
|
32050
|
+
json: "value"
|
|
32051
|
+
})
|
|
32052
|
+
});
|
|
32053
|
+
|
|
32054
|
+
//#endregion
|
|
32055
|
+
//#region src/commands/policies/index.ts
|
|
32056
|
+
const policiesCommand = defineCommand({
|
|
32057
|
+
meta: {
|
|
32058
|
+
name: "policies",
|
|
32059
|
+
description: "Manage IAM policies (named, reusable permission grants)"
|
|
32060
|
+
},
|
|
32061
|
+
subCommands: {
|
|
32062
|
+
list: listPoliciesCommand,
|
|
32063
|
+
create: createPolicyCommand,
|
|
32064
|
+
update: updatePolicyCommand,
|
|
32065
|
+
delete: deletePolicyCommand
|
|
32066
|
+
}
|
|
32067
|
+
});
|
|
32068
|
+
|
|
31067
32069
|
//#endregion
|
|
31068
32070
|
//#region src/commands/projects.ts
|
|
31069
32071
|
const listCommand$1 = defineCommand({
|
|
@@ -32700,10 +33702,10 @@ const assertSignedManifestBundleUrl = (params) => Effect.gen(function* () {
|
|
|
32700
33702
|
try: () => JSON.parse(params.manifestBody),
|
|
32701
33703
|
catch: () => params.makeError(`Signed ${params.platform} manifestBody is not valid JSON.`)
|
|
32702
33704
|
});
|
|
32703
|
-
if (!isRecord(parsed)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must be a JSON object.`));
|
|
33705
|
+
if (!isRecord$1(parsed)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must be a JSON object.`));
|
|
32704
33706
|
const { id } = parsed;
|
|
32705
33707
|
const { launchAsset } = parsed;
|
|
32706
|
-
if (typeof id !== "string" || !isRecord(launchAsset)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must carry a string "id" and an object "launchAsset".`));
|
|
33708
|
+
if (typeof id !== "string" || !isRecord$1(launchAsset)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody must carry a string "id" and an object "launchAsset".`));
|
|
32707
33709
|
const { url } = launchAsset;
|
|
32708
33710
|
const expectedPrefix = `${params.serverBaseUrl}/manifest/${params.projectId}/bundle/${id}/`;
|
|
32709
33711
|
if (typeof url !== "string" || !url.startsWith(expectedPrefix)) return yield* Effect.fail(params.makeError(`Signed ${params.platform} manifestBody launchAsset.url must point at the Worker bundle route (${expectedPrefix}…) so bsdiff patches apply; got ${typeof url === "string" ? url : "a non-string value"}. Re-render the signed manifest with the Worker bundle URL.`));
|
|
@@ -32872,7 +33874,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
|
|
|
32872
33874
|
runtimeVersion: input.runtimeVersion,
|
|
32873
33875
|
platform: input.platform,
|
|
32874
33876
|
limit: Math.max(1, input.baseWindow)
|
|
32875
|
-
} }).pipe(Effect.
|
|
33877
|
+
} }).pipe(Effect.orElseSucceed(() => []));
|
|
32876
33878
|
const bases = selectBaseWindow(candidates, {
|
|
32877
33879
|
newUpdateId: input.newUpdateId,
|
|
32878
33880
|
maxRecent: input.baseWindow
|
|
@@ -32888,7 +33890,7 @@ const runPatchPhase = (input) => Effect.gen(function* () {
|
|
|
32888
33890
|
bestSavingsPct: void 0
|
|
32889
33891
|
};
|
|
32890
33892
|
}
|
|
32891
|
-
const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.
|
|
33893
|
+
const newBundleBytes = yield* sha256File(input.newLaunchPath).pipe(Effect.map((result) => result.byteSize), Effect.orElseSucceed(() => void 0));
|
|
32892
33894
|
yield* printHuman(`Diffing against ${bases.length} base(s) (window=${input.baseWindow}; ${candidates.length} candidate(s) available).`);
|
|
32893
33895
|
const uploadedOutcomes = (yield* Effect.forEach(bases, (base, index) => Effect.gen(function* () {
|
|
32894
33896
|
const basePath = path.join(input.workDir, `base-${index}.bundle`);
|
|
@@ -32983,7 +33985,7 @@ const dedupeAssetsByHash = (assets) => uniqBy(assets, (asset) => asset.hash);
|
|
|
32983
33985
|
* is informational, so a fingerprint failure resolves to `undefined` rather than
|
|
32984
33986
|
* failing the publish.
|
|
32985
33987
|
*/
|
|
32986
|
-
const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.
|
|
33988
|
+
const resolvePlatformFingerprintHash = (projectRoot, platform) => runFingerprintForPlatform(projectRoot, platform).pipe(Effect.map((result) => result.hash), Effect.orElseSucceed(() => void 0));
|
|
32987
33989
|
const preparePlatformAssets = ({ exportDir, platform }) => Effect.gen(function* () {
|
|
32988
33990
|
const exportedAssets = yield* readExpoExportAssets({
|
|
32989
33991
|
exportDir,
|
|
@@ -33079,7 +34081,7 @@ const publishPlatform = (params) => Effect.gen(function* () {
|
|
|
33079
34081
|
const uploadDetailsByHash = new Map(assetRegistration.uploaded.map((asset) => [asset.hash, asset]));
|
|
33080
34082
|
yield* Effect.forEach(uniqueAssets.filter((asset) => uploadDetailsByHash.has(asset.hash)), (asset) => Effect.gen(function* () {
|
|
33081
34083
|
const detail = uploadDetailsByHash.get(asset.hash);
|
|
33082
|
-
if (!detail) return yield*
|
|
34084
|
+
if (!detail) return yield* new UpdatePublishError({ message: `Missing upload details for asset ${asset.hash}` });
|
|
33083
34085
|
return yield* assetUploader.uploadAssetBinary({
|
|
33084
34086
|
path: asset.path,
|
|
33085
34087
|
hash: asset.hash,
|
|
@@ -33631,10 +34633,10 @@ const extractDirectiveCommitTime = (directiveBody) => Effect.gen(function* () {
|
|
|
33631
34633
|
try: () => JSON.parse(directiveBody),
|
|
33632
34634
|
catch: () => new UpdateRollbackError({ message: "directiveBody must be valid JSON." })
|
|
33633
34635
|
});
|
|
33634
|
-
if (!isRecord(directive)) return yield* new UpdateRollbackError({ message: "directiveBody must decode to a JSON object." });
|
|
34636
|
+
if (!isRecord$1(directive)) return yield* new UpdateRollbackError({ message: "directiveBody must decode to a JSON object." });
|
|
33635
34637
|
if (directive["type"] !== "rollBackToEmbedded") return yield* new UpdateRollbackError({ message: "directiveBody.type must be \"rollBackToEmbedded\"." });
|
|
33636
34638
|
const { parameters } = directive;
|
|
33637
|
-
if (!isRecord(parameters)) return yield* new UpdateRollbackError({ message: "directiveBody.parameters must be an object." });
|
|
34639
|
+
if (!isRecord$1(parameters)) return yield* new UpdateRollbackError({ message: "directiveBody.parameters must be an object." });
|
|
33638
34640
|
const { commitTime } = parameters;
|
|
33639
34641
|
if (typeof commitTime !== "string" || Number.isNaN(Date.parse(commitTime))) return yield* new UpdateRollbackError({ message: "directiveBody.parameters.commitTime must be a valid ISO 8601 timestamp." });
|
|
33640
34642
|
if (new Date(Date.parse(commitTime)).toISOString() !== commitTime) return yield* new UpdateRollbackError({ message: "directiveBody.parameters.commitTime must be canonical ISO 8601 with milliseconds and a trailing Z (e.g. 2026-05-06T14:00:00.000Z). The expo-updates client cannot parse other forms (no milliseconds, numeric timezone offsets), so the signed rollback would be rejected on-device." });
|
|
@@ -34462,6 +35464,8 @@ const commandRegistry = {
|
|
|
34462
35464
|
init: initCommand,
|
|
34463
35465
|
status: statusCommand,
|
|
34464
35466
|
projects: projectsCommand,
|
|
35467
|
+
policies: policiesCommand,
|
|
35468
|
+
groups: groupsCommand,
|
|
34465
35469
|
branches: branchesCommand,
|
|
34466
35470
|
channels: channelsCommand,
|
|
34467
35471
|
build: buildCommand,
|