@builder.io/ai-utils 0.91.1 → 0.93.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/src/projects.js CHANGED
@@ -25,6 +25,16 @@ export function getGitProviderFromUrl(url) {
25
25
  return "unknown";
26
26
  }
27
27
  }
28
+ export const GIT_PROVIDERS = [
29
+ "github",
30
+ "selfHostedGithub",
31
+ "bitbucket",
32
+ "gitlab",
33
+ "azure",
34
+ "internalHost",
35
+ "custom",
36
+ ];
37
+ export const GitProviderSchema = z.enum(GIT_PROVIDERS);
28
38
  export const EXAMPLE_REPOS = [
29
39
  "steve8708/mui-vite",
30
40
  "steve8708/carbon-vite",
@@ -970,6 +980,14 @@ export const ProjectHostingBuildConfigSchema = z.object({
970
980
  buildOutputDir: z.string().optional(),
971
981
  nodeVersion: z.string().optional(),
972
982
  });
983
+ /**
984
+ * Who pays for AI usage on a project's published site.
985
+ *
986
+ * `undefined` is reserved for headless publishes that never reach
987
+ * `POST /projects/hosting/ai-usage`: it is the signal that makes the publish
988
+ * step fire, so every interactive path writes a value, `"skip"` included.
989
+ */
990
+ export const AiPaymentModeSchema = z.enum(["builder-credits", "byok", "skip"]);
973
991
  /**
974
992
  * Hosting configuration for a project, stored at `projects/{projectId}.hosting`.
975
993
  */
@@ -1015,6 +1033,8 @@ export const ProjectHostingSchema = z.object({
1015
1033
  lastDeployId: z.string().optional(),
1016
1034
  lastDeployAt: z.number().optional(),
1017
1035
  lastDeployStatus: DeployStatusSchema.optional(),
1036
+ /** Kubernetes branch route currently serving; absent means the default route. */
1037
+ publishedBranchId: z.string().optional(),
1018
1038
  /** Git commit currently live at the project's hosting URL (set on go-live). */
1019
1039
  publishedCommit: z.string().optional(),
1020
1040
  autoDeploy: z
@@ -1042,6 +1062,24 @@ export const ProjectHostingSchema = z.object({
1042
1062
  domainIds: z.array(z.string()).optional().meta({
1043
1063
  description: "Doc keys (normalized domains) in `hosting-domains` attached to this project. The domain docs are the source of truth; this is only the back-pointer.",
1044
1064
  }),
1065
+ /**
1066
+ * Who pays for AI usage on the published site. Written only by
1067
+ * `POST /projects/hosting/ai-usage`, which gates on `fusionHostingPublish`;
1068
+ * deliberately kept out of {@link PROJECT_UPDATABLE_HOSTING_FIELDS_MASK} so
1069
+ * the generic `PATCH` (gated on `modifyProjectSettings`) cannot write it.
1070
+ */
1071
+ aiPaymentMode: AiPaymentModeSchema.optional().meta({
1072
+ description: "Who pays for AI usage on the published site. Absent means the first-publish step has never run for this project.",
1073
+ }),
1074
+ /**
1075
+ * When a hosting field that is baked into the deployed site last changed
1076
+ * (env vars, build config, AI payment mode). Compared against
1077
+ * `lastDeployAt` to decide whether the live site is stale: server-side so a
1078
+ * change made in one browser session is visible in every other.
1079
+ */
1080
+ configUpdatedAt: z.number().optional().meta({
1081
+ description: "Epoch ms of the last change to a hosting field that requires a redeploy. Compare against `lastDeployAt`.",
1082
+ }),
1045
1083
  });
1046
1084
  /**
1047
1085
  * Mask of the hosting sub-fields a client may write via `PATCH`. Everything
@@ -1052,7 +1090,7 @@ export const ProjectHostingSchema = z.object({
1052
1090
  * new field added to `ProjectHostingSchema` must be opted *in* to be writable,
1053
1091
  * so the default for anything we add later is "not client-writable".
1054
1092
  */
1055
- const PROJECT_UPDATABLE_HOSTING_FIELDS_MASK = {
1093
+ export const PROJECT_UPDATABLE_HOSTING_FIELDS_MASK = {
1056
1094
  enabled: true,
1057
1095
  buildConfig: true,
1058
1096
  environment: true,
@@ -1184,6 +1222,18 @@ export const DeploySchema = z.object({
1184
1222
  devBranchId: z.string().optional().meta({
1185
1223
  description: "Fusion dev branch's id (unset if we are deploying project default branch)",
1186
1224
  }),
1225
+ supersededRouteBranchId: z.string().nullable().optional().meta({
1226
+ description: "Prior published route awaiting cleanup after a branch switch; null identifies the default route.",
1227
+ }),
1228
+ supersededRouteResourceVersions: z
1229
+ .object({
1230
+ route: z.string().optional(),
1231
+ handshake: z.string().optional(),
1232
+ })
1233
+ .optional()
1234
+ .meta({
1235
+ description: "Kubernetes versions that fence deletion of the superseded route resources.",
1236
+ }),
1187
1237
  status: DeployStatusSchema,
1188
1238
  netlifyDeployId: z.string().optional(),
1189
1239
  url: z.string().optional(),
@@ -1461,6 +1511,92 @@ export const DeleteHostingRequestSchema = z.object({
1461
1511
  export const DeleteHostingResponseSchema = z.object({
1462
1512
  success: z.literal(true),
1463
1513
  });
1514
+ // ---------------------------------------------------------------------------
1515
+ // AI usage (who pays for AI on the published site)
1516
+ // ---------------------------------------------------------------------------
1517
+ /** Query for `GET /projects/hosting/ai-usage`. */
1518
+ export const GetHostingAiUsageRequestSchema = z.object({
1519
+ projectId: z.string().min(1),
1520
+ });
1521
+ /**
1522
+ * Status of the published site's gateway token. Recomputed from the
1523
+ * authoritative PAT doc on every read; the plaintext token is never included.
1524
+ */
1525
+ export const HostingAiUsageTokenStatusSchema = z.object({
1526
+ active: z.boolean(),
1527
+ lastUsed: z.number().optional(),
1528
+ createdBy: z.string().optional(),
1529
+ });
1530
+ export const GetHostingAiUsageResponseSchema = z.object({
1531
+ /**
1532
+ * Whether the project has ever been deployed. Server-derived from
1533
+ * `hosting.netlifySiteId`, which the client can never see.
1534
+ */
1535
+ hasEverPublished: z.boolean(),
1536
+ aiPaymentMode: AiPaymentModeSchema.optional(),
1537
+ /**
1538
+ * Whether `"builder-credits"` can be honoured for this project: the LD flag
1539
+ * AND a `coreVersion` floor at or above the minimum supporting release.
1540
+ * Per-project, because core is pinned in the project's own lockfile.
1541
+ */
1542
+ coreSupportsGateway: z.boolean(),
1543
+ token: HostingAiUsageTokenStatusSchema.nullable(),
1544
+ });
1545
+ /** Providers a customer may bring their own key for. */
1546
+ export const AiUsageByokProviderSchema = z.enum(["anthropic", "openai"]);
1547
+ /**
1548
+ * Body for `POST /projects/hosting/ai-usage`.
1549
+ *
1550
+ * Deliberately closed (`strictObject`): this route is gated on
1551
+ * `fusionHostingPublish` rather than `modifyProjectSettings`, so the request
1552
+ * shape is the only thing stopping it becoming a general prod-env write
1553
+ * primitive. The handler builds the env entries itself from `provider`,
1554
+ * `apiKey` and `baseUrl` — an env var *name* is never caller-supplied.
1555
+ */
1556
+ export const SetHostingAiUsageRequestSchema = z.strictObject({
1557
+ projectId: z.string().min(1),
1558
+ mode: AiPaymentModeSchema,
1559
+ provider: AiUsageByokProviderSchema.optional(),
1560
+ apiKey: z.string().min(1).optional(),
1561
+ // https only: this value is written into a published site's prod env as
1562
+ // `OPENAI_BASE_URL`, so an `http://` endpoint would send the customer's
1563
+ // provider key and every prompt in plaintext from the deployed site.
1564
+ baseUrl: z
1565
+ .url()
1566
+ .refine((value) => value.startsWith("https://"), {
1567
+ message: "baseUrl must use https",
1568
+ })
1569
+ .optional(),
1570
+ });
1571
+ export const SetHostingAiUsageResponseSchema = z.object({
1572
+ aiPaymentMode: AiPaymentModeSchema,
1573
+ /**
1574
+ * `hosting.configUpdatedAt` after the write — compare against
1575
+ * `hosting.lastDeployAt` to decide whether a redeploy is needed. Absent when
1576
+ * the write changed nothing deploy-relevant (e.g. re-selecting the current
1577
+ * mode), which is exactly the case where no redeploy is needed.
1578
+ */
1579
+ configUpdatedAt: z.number().optional(),
1580
+ });
1581
+ /** Body for `POST /projects/hosting/ai-usage/revoke`. */
1582
+ export const RevokeHostingAiTokenRequestSchema = z.strictObject({
1583
+ projectId: z.string().min(1),
1584
+ });
1585
+ export const RevokeHostingAiTokenResponseSchema = z.object({
1586
+ success: z.literal(true),
1587
+ });
1588
+ /**
1589
+ * The three prod env vars the AI-usage endpoint owns. The write path sets only
1590
+ * these; every non-`byok` mode deletes exactly these. Without the delete, a
1591
+ * BYOK→credits switch is a silent no-op — the stale provider key keeps winning
1592
+ * the framework's engine auto-detection, and the gateway injection gate keys on
1593
+ * its absence.
1594
+ */
1595
+ export const AI_USAGE_MANAGED_PROD_ENV_KEYS = [
1596
+ "ANTHROPIC_API_KEY",
1597
+ "OPENAI_API_KEY",
1598
+ "OPENAI_BASE_URL",
1599
+ ];
1464
1600
  export const GetDeploysRequestSchema = z.object({
1465
1601
  projectId: z.string(),
1466
1602
  deployId: z.string().optional(),
@@ -1703,3 +1839,71 @@ export const EnsureMainBranchExistsOptionsSchema = z.object({
1703
1839
  description: "Branch whose head ref seeds the main branch when it doesn't exist yet. No-op for non-internalHost projects.",
1704
1840
  }),
1705
1841
  });
1842
+ /**
1843
+ * `settings.environmentVariables` is stored with **plaintext** secret values;
1844
+ * secrets are masked before this shape leaves ai-services, same as
1845
+ * `Project.settings`.
1846
+ */
1847
+ export const ProjectTemplateSchema = z.object({
1848
+ id: z.string(),
1849
+ name: z.string(),
1850
+ ownerId: z.string(),
1851
+ description: z.string(),
1852
+ repoFullName: z.string(),
1853
+ repoProvider: GitProviderSchema,
1854
+ repoUrl: z.string(),
1855
+ repoPrivate: z.boolean(),
1856
+ screenshot: z.string().nullish(),
1857
+ order: z.number(),
1858
+ hidden: z.boolean(),
1859
+ isDefault: z.boolean(),
1860
+ createdDate: z.string(),
1861
+ updatedAt: z.string(),
1862
+ createdBy: z.string(),
1863
+ lastUpdateBy: z.string(),
1864
+ // Only set when the template is seeded from an unconnected built-in
1865
+ // template's container backup; repo-connected sources clone from their repo.
1866
+ sourceProjectId: z.string().optional(),
1867
+ sourceBranchName: z.string().optional(),
1868
+ settings: ProjectSettingsSchema.partial(),
1869
+ hosting: ProjectHostingSchema.optional(),
1870
+ });
1871
+ /**
1872
+ * The id, ownership, audit, and source fields are server-assigned, so they can't
1873
+ * be overridden here.
1874
+ *
1875
+ * A masked-secret placeholder in `settings.environmentVariables` keeps the
1876
+ * source project's real value, so a client that never saw a secret can still
1877
+ * round-trip it.
1878
+ */
1879
+ export const ProjectTemplateOverridesSchema = z.object({
1880
+ ...ProjectTemplateSchema.omit({
1881
+ id: true,
1882
+ ownerId: true,
1883
+ createdDate: true,
1884
+ updatedAt: true,
1885
+ createdBy: true,
1886
+ lastUpdateBy: true,
1887
+ sourceProjectId: true,
1888
+ sourceBranchName: true,
1889
+ }).partial().shape,
1890
+ hosting: ProjectHostingExternalSchema.optional(),
1891
+ });
1892
+ export const CreateProjectTemplateFromProjectOptionsSchema = z.object({
1893
+ sourceProjectId: z.string().min(1).meta({
1894
+ description: "Project whose settings, repo fields, and hosting config seed the template.",
1895
+ }),
1896
+ sourceBranchName: z.string().min(1).meta({
1897
+ description: "Branch of the source project that projects created from this template are seeded from.",
1898
+ }),
1899
+ overrides: ProjectTemplateOverridesSchema.optional().meta({
1900
+ description: "Fields to override on top of the values copied from the source project.",
1901
+ }),
1902
+ });
1903
+ /**
1904
+ * Deliberately minimal: the stored template carries plaintext secret env values,
1905
+ * so nothing beyond the identity of the new template goes back to the caller.
1906
+ */
1907
+ export const CreateProjectTemplateFromProjectResponseSchema = z.object({
1908
+ template: ProjectTemplateSchema.pick({ id: true, name: true }),
1909
+ });
@@ -1,5 +1,16 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { AGENT_NATIVE_STARTER_REPO, matchesAgentNativeStarter, } from "./projects";
2
+ import { AGENT_NATIVE_STARTER_REPO, matchesAgentNativeStarter, ProjectHostingExternalSchema, ProjectHostingSchema, } from "./projects";
3
+ describe("project hosting schemas", () => {
4
+ it("accepts the canonical published branch route", () => {
5
+ expect(ProjectHostingSchema.parse({ publishedBranchId: "branch-live" })).toEqual({ publishedBranchId: "branch-live" });
6
+ });
7
+ it("does not allow clients to write the canonical published branch route", () => {
8
+ expect(ProjectHostingExternalSchema.parse({
9
+ enabled: true,
10
+ publishedBranchId: "branch-client",
11
+ })).toEqual({ enabled: true });
12
+ });
13
+ });
3
14
  describe("matchesAgentNativeStarter", () => {
4
15
  it("matches when repoName is the starter", () => {
5
16
  expect(matchesAgentNativeStarter({ repoName: AGENT_NATIVE_STARTER_REPO })).toBe(true);