@spacefast/common 0.0.10 → 0.0.12

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.
Files changed (122) hide show
  1. package/dist/agents/connect-targets.d.ts +4 -4
  2. package/dist/agents/connect-targets.js +3 -3
  3. package/dist/agents/private-key-oauth.d.ts +37 -0
  4. package/dist/agents/private-key-oauth.js +158 -0
  5. package/dist/config/domains.js +1 -1
  6. package/dist/config/index.d.ts +1 -0
  7. package/dist/config/index.js +1 -0
  8. package/dist/config/postgres-budget.d.ts +126 -0
  9. package/dist/config/postgres-budget.js +135 -0
  10. package/dist/config/postgres-budget.json +50 -0
  11. package/dist/contracts/access.d.ts +42 -24
  12. package/dist/contracts/access.js +73 -34
  13. package/dist/contracts/activity.d.ts +1 -1
  14. package/dist/contracts/activity.js +5 -5
  15. package/dist/contracts/api-keys.d.ts +53 -45
  16. package/dist/contracts/api-keys.js +1 -0
  17. package/dist/contracts/archives.d.ts +8 -8
  18. package/dist/contracts/builds.d.ts +8 -8
  19. package/dist/contracts/builds.js +28 -48
  20. package/dist/contracts/channels.js +1 -2
  21. package/dist/contracts/comments.d.ts +55 -19
  22. package/dist/contracts/comments.js +56 -17
  23. package/dist/contracts/common.d.ts +4 -3
  24. package/dist/contracts/common.js +2 -0
  25. package/dist/contracts/deployments.d.ts +3 -3
  26. package/dist/contracts/device-auth.d.ts +14 -12
  27. package/dist/contracts/docs.d.ts +5 -5
  28. package/dist/contracts/domains.d.ts +261 -1330
  29. package/dist/contracts/domains.js +39 -301
  30. package/dist/contracts/email-preferences.d.ts +50 -0
  31. package/dist/contracts/email-preferences.js +44 -0
  32. package/dist/contracts/enums.d.ts +1 -1
  33. package/dist/contracts/error-code-meta.d.ts +84 -15
  34. package/dist/contracts/error-code-meta.js +28 -4
  35. package/dist/contracts/error-codes.d.ts +1 -1
  36. package/dist/contracts/error-codes.js +24 -6
  37. package/dist/contracts/events.d.ts +13 -13
  38. package/dist/contracts/feature-lifecycle.d.ts +644 -2
  39. package/dist/contracts/feature-lifecycle.js +685 -2
  40. package/dist/contracts/features.d.ts +236 -24
  41. package/dist/contracts/features.js +3 -272
  42. package/dist/contracts/ids.d.ts +1 -2
  43. package/dist/contracts/ids.js +1 -2
  44. package/dist/contracts/internal.d.ts +3 -3
  45. package/dist/contracts/mcp.d.ts +6 -6
  46. package/dist/contracts/me.js +1 -1
  47. package/dist/contracts/mounts.d.ts +142 -0
  48. package/dist/contracts/mounts.js +56 -0
  49. package/dist/contracts/oauth-resources.d.ts +5 -5
  50. package/dist/contracts/oauth-resources.js +1 -0
  51. package/dist/contracts/operations.d.ts +2 -2
  52. package/dist/contracts/pages.d.ts +2 -2
  53. package/dist/contracts/pages.js +8 -3
  54. package/dist/contracts/plan-policy.d.ts +1 -1
  55. package/dist/contracts/platform.d.ts +3 -3
  56. package/dist/contracts/resources.d.ts +12 -20
  57. package/dist/contracts/resources.js +13 -37
  58. package/dist/contracts/runtime-api.d.ts +330 -16
  59. package/dist/contracts/runtime-api.js +58 -14
  60. package/dist/contracts/sf-config-v1.js +1 -1
  61. package/dist/contracts/sites.d.ts +12 -12
  62. package/dist/contracts/sites.js +18 -11
  63. package/dist/contracts/space-config.d.ts +2 -3
  64. package/dist/contracts/space-config.js +3 -1
  65. package/dist/contracts/spaces.d.ts +143 -102
  66. package/dist/contracts/spaces.js +109 -43
  67. package/dist/contracts/superadmin-emails.d.ts +25 -1
  68. package/dist/contracts/superadmin-emails.js +10 -15
  69. package/dist/contracts/superadmin-runtime.d.ts +2 -2
  70. package/dist/contracts/superadmin-spaces.d.ts +6 -14
  71. package/dist/contracts/tags.d.ts +43 -13
  72. package/dist/contracts/tags.js +195 -11
  73. package/dist/contracts/transfers.d.ts +1 -1
  74. package/dist/contracts/variables.d.ts +4 -4
  75. package/dist/contracts/variables.js +4 -3
  76. package/dist/contracts/webhooks.d.ts +2 -2
  77. package/dist/contracts/zero.d.ts +329 -7
  78. package/dist/contracts/zero.js +121 -2
  79. package/dist/dashboard-paths/index.d.ts +1 -1
  80. package/dist/docs/agent-setup.d.ts +4 -2
  81. package/dist/docs/agent-setup.js +15 -7
  82. package/dist/docs/agent-solutions.js +1 -1
  83. package/dist/docs/catalog.d.ts +8 -10
  84. package/dist/docs/catalog.js +8 -10
  85. package/dist/docs/error-docs.js +86 -18
  86. package/dist/utils/access-grant.d.ts +14 -0
  87. package/dist/utils/access-grant.js +88 -0
  88. package/dist/utils/access-match.d.ts +7 -0
  89. package/dist/utils/access-match.fixtures.json +513 -0
  90. package/dist/utils/access-match.js +32 -16
  91. package/dist/utils/access-verdict.fixtures.json +421 -0
  92. package/dist/utils/credential-policy.d.ts +1 -1
  93. package/dist/utils/dns-instructions.d.ts +29 -0
  94. package/dist/utils/dns-instructions.js +78 -0
  95. package/dist/utils/domain-diagnostics.d.ts +29 -0
  96. package/dist/utils/domain-diagnostics.js +63 -0
  97. package/dist/utils/egress-policy.fixtures.json +51 -0
  98. package/dist/utils/id-hints.d.ts +1 -0
  99. package/dist/utils/id-hints.js +55 -13
  100. package/dist/utils/page-colors.d.ts +50 -0
  101. package/dist/utils/page-colors.js +167 -0
  102. package/dist/utils/page-fonts.d.ts +2 -0
  103. package/dist/utils/page-fonts.js +4 -0
  104. package/dist/utils/pages.d.ts +8 -1
  105. package/dist/utils/pages.js +123 -37
  106. package/dist/utils/production-runtime.d.ts +18 -0
  107. package/dist/utils/production-runtime.js +28 -0
  108. package/dist/utils/query-keys.d.ts +6 -7
  109. package/dist/utils/query-keys.js +11 -12
  110. package/dist/utils/sf-config-v1.fixtures.json +9 -0
  111. package/dist/utils/sf-config-v1.js +28 -19
  112. package/dist/utils/source-archive-policy.d.ts +2 -0
  113. package/dist/utils/source-archive-policy.js +2 -0
  114. package/dist/utils/static-runtime-policy.fixtures.json +57 -0
  115. package/dist/utils/static-runtime-policy.js +1 -1
  116. package/package.json +5 -4
  117. package/dist/contracts/annotations.d.ts +0 -327
  118. package/dist/contracts/annotations.js +0 -235
  119. package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
  120. package/dist/contracts/generated-feature-launch-entries.js +0 -518
  121. package/dist/contracts/publishes.d.ts +0 -135
  122. package/dist/contracts/publishes.js +0 -162
@@ -1,7 +1,7 @@
1
1
  export declare const LOCAL_MCP_PACKAGE = "spacefast";
2
2
  export declare const LOCAL_MCP_COMMAND = "npx";
3
- export declare const LOCAL_MCP_ARGS: readonly ["-y", "spacefast", "mcp"];
4
- /** The copy-paste one-liner form, e.g. `npx -y spacefast mcp`. */
3
+ export declare const LOCAL_MCP_ARGS: readonly ["-y", "spacefast", "mcp", "--preset", "essential"];
4
+ /** The copy-paste one-liner form with the lean default tool surface. */
5
5
  export declare const LOCAL_MCP_COMMAND_LINE: string;
6
6
  /**
7
7
  * base64 of a UTF-8 string, without Buffer. Byte-identical to
@@ -14,7 +14,7 @@ export declare function localMcpServersJson(): {
14
14
  mcpServers: {
15
15
  spacefast: {
16
16
  command: string;
17
- args: ("mcp" | "spacefast" | "-y")[];
17
+ args: ("mcp" | "essential" | "spacefast" | "-y" | "--preset")[];
18
18
  env: {};
19
19
  };
20
20
  };
@@ -30,7 +30,7 @@ export declare function vscodeMcpServersJson(): {
30
30
  spacefast: {
31
31
  type: string;
32
32
  command: string;
33
- args: ("mcp" | "spacefast" | "-y")[];
33
+ args: ("mcp" | "essential" | "spacefast" | "-y" | "--preset")[];
34
34
  };
35
35
  };
36
36
  };
@@ -15,8 +15,8 @@
15
15
  import { spacefastClaudePluginAddCommand, spacefastClaudePluginInstallCommand, spacefastCodexPluginAddCommand, spacefastCodexPluginInstallCommand, } from "../docs/skill-distribution.js";
16
16
  export const LOCAL_MCP_PACKAGE = "spacefast";
17
17
  export const LOCAL_MCP_COMMAND = "npx";
18
- export const LOCAL_MCP_ARGS = ["-y", LOCAL_MCP_PACKAGE, "mcp"];
19
- /** The copy-paste one-liner form, e.g. `npx -y spacefast mcp`. */
18
+ export const LOCAL_MCP_ARGS = ["-y", LOCAL_MCP_PACKAGE, "mcp", "--preset", "essential"];
19
+ /** The copy-paste one-liner form with the lean default tool surface. */
20
20
  export const LOCAL_MCP_COMMAND_LINE = `${LOCAL_MCP_COMMAND} ${LOCAL_MCP_ARGS.join(" ")}`;
21
21
  /**
22
22
  * base64 of a UTF-8 string, without Buffer. Byte-identical to
@@ -65,7 +65,7 @@ export function vscodeMcpServersJson() {
65
65
  * on-device for local ones — but a one-click install must never depend on the
66
66
  * user having npm/npx. The hosted URL server needs no runtime at all (the
67
67
  * client speaks HTTP + OAuth), so every deeplink and every primary action
68
- * carries the hosted endpoint. On-device stdio (`npx -y spacefast mcp`) stays
68
+ * carries the hosted endpoint. On-device stdio (`npx -y spacefast mcp --preset essential`) stays
69
69
  * available as the explicitly-labeled alternative for people who run the CLI —
70
70
  * their environment already provides npx.
71
71
  */
@@ -0,0 +1,37 @@
1
+ import { webcrypto } from "node:crypto";
2
+ export type AgentPrivateJwk = webcrypto.JsonWebKey & {
3
+ kid?: string;
4
+ };
5
+ export type AgentAuthConfig = {
6
+ client_id: string;
7
+ token_endpoint: string;
8
+ private_jwk: AgentPrivateJwk;
9
+ scope?: string | undefined;
10
+ resources?: string[] | undefined;
11
+ };
12
+ export type AgentAuthEnvironment = Readonly<Record<string, string | undefined>>;
13
+ export type AgentTokenExchangeErrorInput = {
14
+ status: number;
15
+ code: "agent_token_exchange_failed";
16
+ message: string;
17
+ };
18
+ export type AgentTokenExchangeResult = {
19
+ accessToken: string;
20
+ expiresIn: number;
21
+ };
22
+ /** Parse private-key agent credentials using the established bundled-over-split env precedence. */
23
+ export declare function readAgentAuthConfig(env: AgentAuthEnvironment, apiUrl: string): AgentAuthConfig | null;
24
+ export declare function resolveAgentApiAuthorization(config: AgentAuthConfig, apiUrl: string): {
25
+ resources: string[];
26
+ scope: string | undefined;
27
+ };
28
+ export declare function createPrivateKeyJwtAssertion(config: AgentAuthConfig, createInvalidConfigError?: (message: string) => Error): Promise<string>;
29
+ export declare function exchangeAgentAccessToken(input: {
30
+ apiUrl: string;
31
+ client: string;
32
+ config: AgentAuthConfig;
33
+ signal: AbortSignal;
34
+ createError: (input: AgentTokenExchangeErrorInput) => Error;
35
+ createInvalidConfigError?: ((message: string) => Error) | undefined;
36
+ fetch?: typeof globalThis.fetch | undefined;
37
+ }): Promise<AgentTokenExchangeResult>;
@@ -0,0 +1,158 @@
1
+ import { randomUUID, webcrypto } from "node:crypto";
2
+ import { OAUTH_API_RESOURCE_SCOPES, oauthApiResourceUrl } from "../contracts/oauth-resources.js";
3
+ const PRIVATE_KEY_JWT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
4
+ const API_RESOURCE_SCOPES = new Set(OAUTH_API_RESOURCE_SCOPES);
5
+ const INVALID_PRIVATE_KEY_MESSAGE = "SPACEFAST_AGENT_CONFIG private_jwk must be an ES256 P-256 JWK.";
6
+ function isRecord(value) {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ function isJwkObject(value) {
10
+ // Keep parsing permissive here. Web Crypto remains the authority on the JWK's
11
+ // individual fields, matching the callers' previous behavior.
12
+ return value !== null && typeof value === "object";
13
+ }
14
+ function readJsonObject(value) {
15
+ if (!value?.trim()) {
16
+ return null;
17
+ }
18
+ const parsed = JSON.parse(value);
19
+ return isRecord(parsed) ? parsed : null;
20
+ }
21
+ function stringArray(value) {
22
+ return Array.isArray(value)
23
+ ? value.filter((entry) => typeof entry === "string")
24
+ : undefined;
25
+ }
26
+ /** Parse private-key agent credentials using the established bundled-over-split env precedence. */
27
+ export function readAgentAuthConfig(env, apiUrl) {
28
+ const bundled = readJsonObject(env.SPACEFAST_AGENT_CONFIG);
29
+ const bundledPrivateJwk = bundled?.private_jwk;
30
+ const privateJwk = isJwkObject(bundledPrivateJwk)
31
+ ? bundledPrivateJwk
32
+ : readJsonObject(env.SPACEFAST_AGENT_PRIVATE_JWK);
33
+ const clientId = (typeof bundled?.client_id === "string" ? bundled.client_id : undefined) ??
34
+ env.SPACEFAST_AGENT_CLIENT_ID;
35
+ if (!clientId || !privateJwk) {
36
+ return null;
37
+ }
38
+ const tokenEndpoint = (typeof bundled?.token_endpoint === "string" ? bundled.token_endpoint : undefined) ??
39
+ env.SPACEFAST_AGENT_TOKEN_ENDPOINT ??
40
+ `${apiUrl}/v1/auth/oauth2/token`;
41
+ const scope = (typeof bundled?.scope === "string" ? bundled.scope : undefined) ?? env.SPACEFAST_AGENT_SCOPE;
42
+ const resources = stringArray(bundled?.resources) ??
43
+ (env.SPACEFAST_AGENT_RESOURCES
44
+ ? env.SPACEFAST_AGENT_RESOURCES.split(",")
45
+ .map((entry) => entry.trim())
46
+ .filter(Boolean)
47
+ : undefined);
48
+ return {
49
+ client_id: clientId,
50
+ token_endpoint: tokenEndpoint,
51
+ private_jwk: privateJwk,
52
+ ...(scope ? { scope } : {}),
53
+ ...(resources?.length ? { resources } : {}),
54
+ };
55
+ }
56
+ export function resolveAgentApiAuthorization(config, apiUrl) {
57
+ const configuredResources = config.resources;
58
+ let resources;
59
+ if (!configuredResources?.length) {
60
+ resources = [];
61
+ }
62
+ else {
63
+ const apiResource = oauthApiResourceUrl(apiUrl);
64
+ resources = configuredResources.includes(apiResource) ? [apiResource] : configuredResources;
65
+ }
66
+ if (!config.scope || resources.length !== 1) {
67
+ return { resources, scope: config.scope };
68
+ }
69
+ const apiScopes = config.scope
70
+ .split(/\s+/)
71
+ .map((entry) => entry.trim())
72
+ .filter((entry) => API_RESOURCE_SCOPES.has(entry));
73
+ return {
74
+ resources,
75
+ scope: apiScopes.length > 0 ? apiScopes.join(" ") : undefined,
76
+ };
77
+ }
78
+ function base64urlJson(value) {
79
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
80
+ }
81
+ export async function createPrivateKeyJwtAssertion(config, createInvalidConfigError = (message) => new Error(message)) {
82
+ if (config.private_jwk.kty !== "EC") {
83
+ throw createInvalidConfigError(INVALID_PRIVATE_KEY_MESSAGE);
84
+ }
85
+ const now = Math.floor(Date.now() / 1000);
86
+ const key = await webcrypto.subtle.importKey("jwk", config.private_jwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
87
+ const header = base64urlJson({
88
+ alg: "ES256",
89
+ typ: "JWT",
90
+ ...(config.private_jwk.kid ? { kid: config.private_jwk.kid } : {}),
91
+ });
92
+ const payload = base64urlJson({
93
+ iss: config.client_id,
94
+ sub: config.client_id,
95
+ aud: config.token_endpoint,
96
+ jti: randomUUID(),
97
+ iat: now,
98
+ exp: now + 300,
99
+ });
100
+ const signingInput = `${header}.${payload}`;
101
+ const signature = await webcrypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, new TextEncoder().encode(signingInput));
102
+ return `${signingInput}.${Buffer.from(signature).toString("base64url")}`;
103
+ }
104
+ function oauthErrorDescription(payload) {
105
+ return isRecord(payload) && "error_description" in payload
106
+ ? String(payload.error_description)
107
+ : undefined;
108
+ }
109
+ function oauthAccessToken(payload) {
110
+ return isRecord(payload) && typeof payload.access_token === "string"
111
+ ? payload.access_token
112
+ : undefined;
113
+ }
114
+ function oauthExpiresIn(payload) {
115
+ return isRecord(payload) && typeof payload.expires_in === "number" ? payload.expires_in : 300;
116
+ }
117
+ export async function exchangeAgentAccessToken(input) {
118
+ const { resources, scope } = resolveAgentApiAuthorization(input.config, input.apiUrl);
119
+ const body = new URLSearchParams({
120
+ grant_type: "client_credentials",
121
+ client_id: input.config.client_id,
122
+ client_assertion_type: PRIVATE_KEY_JWT_ASSERTION_TYPE,
123
+ client_assertion: await createPrivateKeyJwtAssertion(input.config, input.createInvalidConfigError),
124
+ });
125
+ if (scope) {
126
+ body.set("scope", scope);
127
+ }
128
+ for (const resource of resources) {
129
+ body.append("resource", resource);
130
+ }
131
+ const response = await (input.fetch ?? globalThis.fetch)(input.config.token_endpoint, {
132
+ method: "POST",
133
+ headers: {
134
+ accept: "application/json",
135
+ "content-type": "application/x-www-form-urlencoded",
136
+ "x-spacefast-client": input.client,
137
+ },
138
+ body,
139
+ signal: input.signal,
140
+ });
141
+ const payload = await response.json().catch(() => null);
142
+ if (!response.ok) {
143
+ throw input.createError({
144
+ status: response.status,
145
+ code: "agent_token_exchange_failed",
146
+ message: oauthErrorDescription(payload) ?? response.statusText,
147
+ });
148
+ }
149
+ const accessToken = oauthAccessToken(payload);
150
+ if (accessToken === undefined) {
151
+ throw input.createError({
152
+ status: response.status,
153
+ code: "agent_token_exchange_failed",
154
+ message: "OAuth token endpoint did not return an access_token.",
155
+ });
156
+ }
157
+ return { accessToken, expiresIn: oauthExpiresIn(payload) };
158
+ }
@@ -29,7 +29,7 @@ export function envDomainsFor(domain) {
29
29
  superadmin: `superadmin.${domain}`,
30
30
  cast: `cast.${domain}`,
31
31
  mcp: `mcp.${domain}`,
32
- fieldguide: prod ? "fieldguide.view.fast" : `fieldguide.${domain}`,
32
+ fieldguide: prod ? "pitch.view.fast" : `fieldguide.${domain}`,
33
33
  },
34
34
  };
35
35
  }
@@ -1 +1,2 @@
1
1
  export * from "./domains.js";
2
+ export * from "./postgres-budget.js";
@@ -1,3 +1,4 @@
1
1
  // Public entry for the centralized domain/URL registry.
2
2
  // Consumers import from "@spacefast/common/config".
3
3
  export * from "./domains.js";
4
+ export * from "./postgres-budget.js";
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+ declare const postgresConnectionBudgetSchema: z.ZodObject<{
3
+ server: z.ZodObject<{
4
+ maxConnections: z.ZodNumber;
5
+ superuserReservedConnections: z.ZodNumber;
6
+ }, z.core.$strip>;
7
+ operationalReserveConnections: z.ZodNumber;
8
+ transactionPools: z.ZodRecord<z.ZodString, z.ZodObject<{
9
+ serverConnections: z.ZodNumber;
10
+ }, z.core.$strip>>;
11
+ services: z.ZodObject<{
12
+ api: z.ZodObject<{
13
+ enabledInProduction: z.ZodBoolean;
14
+ replicas: z.ZodNumber;
15
+ poolConnectionsPerReplica: z.ZodNumber;
16
+ fixedConnectionsPerReplica: z.ZodNumber;
17
+ maxPinnedTransactionsPerReplica: z.ZodNumber;
18
+ rolloutOverlapReplicas: z.ZodNumber;
19
+ connectionBudget: z.ZodString;
20
+ }, z.core.$strip>;
21
+ worker: z.ZodObject<{
22
+ enabledInProduction: z.ZodBoolean;
23
+ replicas: z.ZodNumber;
24
+ poolConnectionsPerReplica: z.ZodNumber;
25
+ fixedConnectionsPerReplica: z.ZodNumber;
26
+ maxPinnedTransactionsPerReplica: z.ZodNumber;
27
+ rolloutOverlapReplicas: z.ZodNumber;
28
+ connectionBudget: z.ZodString;
29
+ }, z.core.$strip>;
30
+ cast: z.ZodObject<{
31
+ enabledInProduction: z.ZodBoolean;
32
+ replicas: z.ZodNumber;
33
+ poolConnectionsPerReplica: z.ZodNumber;
34
+ fixedConnectionsPerReplica: z.ZodNumber;
35
+ maxPinnedTransactionsPerReplica: z.ZodNumber;
36
+ rolloutOverlapReplicas: z.ZodNumber;
37
+ connectionBudget: z.ZodString;
38
+ }, z.core.$strip>;
39
+ scanWorker: z.ZodObject<{
40
+ enabledInProduction: z.ZodBoolean;
41
+ replicas: z.ZodNumber;
42
+ poolConnectionsPerReplica: z.ZodNumber;
43
+ fixedConnectionsPerReplica: z.ZodNumber;
44
+ maxPinnedTransactionsPerReplica: z.ZodNumber;
45
+ rolloutOverlapReplicas: z.ZodNumber;
46
+ connectionBudget: z.ZodString;
47
+ }, z.core.$strip>;
48
+ }, z.core.$strip>;
49
+ }, z.core.$strip>;
50
+ export type PostgresConnectionBudget = z.infer<typeof postgresConnectionBudgetSchema>;
51
+ export type PostgresBudgetService = keyof PostgresConnectionBudget["services"];
52
+ export type PostgresConnectionBudgetSummary = {
53
+ usableServerConnections: number;
54
+ directSteadyConnections: number;
55
+ transactionPoolConnections: number;
56
+ transactionPoolPinnedConnections: number;
57
+ transactionPoolQueryHeadroom: number;
58
+ steadyConnections: number;
59
+ rollingDeploySurgeConnections: number;
60
+ operationalReserveConnections: number;
61
+ plannedConnections: number;
62
+ unallocatedConnections: number;
63
+ };
64
+ export declare const POSTGRES_CONNECTION_BUDGET: {
65
+ server: {
66
+ maxConnections: number;
67
+ superuserReservedConnections: number;
68
+ };
69
+ operationalReserveConnections: number;
70
+ transactionPools: Record<string, {
71
+ serverConnections: number;
72
+ }>;
73
+ services: {
74
+ api: {
75
+ enabledInProduction: boolean;
76
+ replicas: number;
77
+ poolConnectionsPerReplica: number;
78
+ fixedConnectionsPerReplica: number;
79
+ maxPinnedTransactionsPerReplica: number;
80
+ rolloutOverlapReplicas: number;
81
+ connectionBudget: string;
82
+ };
83
+ worker: {
84
+ enabledInProduction: boolean;
85
+ replicas: number;
86
+ poolConnectionsPerReplica: number;
87
+ fixedConnectionsPerReplica: number;
88
+ maxPinnedTransactionsPerReplica: number;
89
+ rolloutOverlapReplicas: number;
90
+ connectionBudget: string;
91
+ };
92
+ cast: {
93
+ enabledInProduction: boolean;
94
+ replicas: number;
95
+ poolConnectionsPerReplica: number;
96
+ fixedConnectionsPerReplica: number;
97
+ maxPinnedTransactionsPerReplica: number;
98
+ rolloutOverlapReplicas: number;
99
+ connectionBudget: string;
100
+ };
101
+ scanWorker: {
102
+ enabledInProduction: boolean;
103
+ replicas: number;
104
+ poolConnectionsPerReplica: number;
105
+ fixedConnectionsPerReplica: number;
106
+ maxPinnedTransactionsPerReplica: number;
107
+ rolloutOverlapReplicas: number;
108
+ connectionBudget: string;
109
+ };
110
+ };
111
+ };
112
+ export declare function calculatePostgresConnectionBudget(plan: PostgresConnectionBudget, server?: {
113
+ maxConnections: number;
114
+ superuserReservedConnections: number;
115
+ }): PostgresConnectionBudgetSummary;
116
+ export declare function assertPostgresServiceConnectionUrl(service: PostgresBudgetService, rawUrl: string, options?: {
117
+ allowDirectDevelopmentConnection?: boolean;
118
+ }): void;
119
+ export declare function assertPostgresServicePoolBudget(service: PostgresBudgetService, configuredPoolConnections: number): void;
120
+ export declare function assertPostgresServiceNamedLockBudget(service: PostgresBudgetService, configuredNamedLockTransactions: number): void;
121
+ export declare function assertPostgresServerBudget(server: {
122
+ maxConnections: number;
123
+ superuserReservedConnections: number;
124
+ }): PostgresConnectionBudgetSummary;
125
+ export declare const POSTGRES_CONNECTION_BUDGET_SUMMARY: PostgresConnectionBudgetSummary;
126
+ export {};
@@ -0,0 +1,135 @@
1
+ import { z } from "zod";
2
+ import budgetJson from "./postgres-budget.json" with { type: "json" };
3
+ const serviceBudgetSchema = z.object({
4
+ enabledInProduction: z.boolean(),
5
+ replicas: z.number().int().nonnegative(),
6
+ poolConnectionsPerReplica: z.number().int().positive(),
7
+ fixedConnectionsPerReplica: z.number().int().nonnegative(),
8
+ maxPinnedTransactionsPerReplica: z.number().int().nonnegative(),
9
+ rolloutOverlapReplicas: z.number().int().nonnegative(),
10
+ connectionBudget: z.string().min(1),
11
+ });
12
+ const postgresConnectionBudgetSchema = z
13
+ .object({
14
+ server: z.object({
15
+ maxConnections: z.number().int().positive(),
16
+ superuserReservedConnections: z.number().int().nonnegative(),
17
+ }),
18
+ operationalReserveConnections: z.number().int().nonnegative(),
19
+ transactionPools: z.record(z.string().min(1), z.object({
20
+ serverConnections: z.number().int().positive(),
21
+ })),
22
+ services: z.object({
23
+ api: serviceBudgetSchema,
24
+ worker: serviceBudgetSchema,
25
+ cast: serviceBudgetSchema,
26
+ scanWorker: serviceBudgetSchema,
27
+ }),
28
+ })
29
+ .superRefine((plan, context) => {
30
+ for (const [serviceName, service] of Object.entries(plan.services)) {
31
+ if (service.connectionBudget !== "direct" &&
32
+ !Object.hasOwn(plan.transactionPools, service.connectionBudget)) {
33
+ context.addIssue({
34
+ code: "custom",
35
+ message: `service ${serviceName} references unknown transaction pool ${service.connectionBudget}`,
36
+ path: ["services", serviceName, "connectionBudget"],
37
+ });
38
+ }
39
+ }
40
+ for (const [poolName, pool] of Object.entries(plan.transactionPools)) {
41
+ const maxPinnedTransactions = Object.values(plan.services)
42
+ .filter((service) => service.enabledInProduction && service.connectionBudget === poolName)
43
+ .reduce((total, service) => total +
44
+ (service.replicas + service.rolloutOverlapReplicas) *
45
+ service.maxPinnedTransactionsPerReplica, 0);
46
+ if (maxPinnedTransactions >= pool.serverConnections) {
47
+ context.addIssue({
48
+ code: "custom",
49
+ message: `transaction pool ${poolName} can pin ${maxPinnedTransactions}/${pool.serverConnections} server connections, leaving no callback query capacity`,
50
+ path: ["transactionPools", poolName, "serverConnections"],
51
+ });
52
+ }
53
+ }
54
+ });
55
+ export const POSTGRES_CONNECTION_BUDGET = postgresConnectionBudgetSchema.parse(budgetJson);
56
+ function serviceFootprint(service) {
57
+ return service.poolConnectionsPerReplica + service.fixedConnectionsPerReplica;
58
+ }
59
+ export function calculatePostgresConnectionBudget(plan, server = plan.server) {
60
+ const usableServerConnections = server.maxConnections - server.superuserReservedConnections;
61
+ if (usableServerConnections <= 0) {
62
+ throw new Error(`postgres_connection_budget_invalid: maxConnections=${server.maxConnections} must exceed superuserReservedConnections=${server.superuserReservedConnections}`);
63
+ }
64
+ const directServices = Object.values(plan.services).filter((service) => service.enabledInProduction && service.connectionBudget === "direct");
65
+ const directSteadyConnections = directServices.reduce((total, service) => total + service.replicas * serviceFootprint(service), 0);
66
+ const transactionPoolConnections = Object.values(plan.transactionPools).reduce((total, pool) => total + pool.serverConnections, 0);
67
+ const transactionPoolPinnedConnections = Object.values(plan.services)
68
+ .filter((service) => service.enabledInProduction && service.connectionBudget !== "direct")
69
+ .reduce((total, service) => total +
70
+ (service.replicas + service.rolloutOverlapReplicas) *
71
+ service.maxPinnedTransactionsPerReplica, 0);
72
+ const transactionPoolQueryHeadroom = transactionPoolConnections - transactionPoolPinnedConnections;
73
+ const steadyConnections = directSteadyConnections + transactionPoolConnections;
74
+ const rollingDeploySurgeConnections = directServices.reduce((total, service) => total + service.rolloutOverlapReplicas * serviceFootprint(service), 0);
75
+ const plannedConnections = steadyConnections + rollingDeploySurgeConnections + plan.operationalReserveConnections;
76
+ return {
77
+ usableServerConnections,
78
+ directSteadyConnections,
79
+ transactionPoolConnections,
80
+ transactionPoolPinnedConnections,
81
+ transactionPoolQueryHeadroom,
82
+ steadyConnections,
83
+ rollingDeploySurgeConnections,
84
+ operationalReserveConnections: plan.operationalReserveConnections,
85
+ plannedConnections,
86
+ unallocatedConnections: usableServerConnections - plannedConnections,
87
+ };
88
+ }
89
+ export function assertPostgresServiceConnectionUrl(service, rawUrl, options = {}) {
90
+ const parsed = new URL(rawUrl);
91
+ const hostname = parsed.hostname.toLowerCase();
92
+ const isLocalDevelopmentHost = hostname === "localhost" ||
93
+ hostname === "127.0.0.1" ||
94
+ hostname === "::1" ||
95
+ hostname === "[::1]" ||
96
+ (!hostname.includes(".") && !hostname.includes(":"));
97
+ if (options.allowDirectDevelopmentConnection && isLocalDevelopmentHost) {
98
+ return;
99
+ }
100
+ const serviceBudget = POSTGRES_CONNECTION_BUDGET.services[service];
101
+ if (!serviceBudget.enabledInProduction) {
102
+ throw new Error(`postgres_connection_budget_service_disabled: service=${service}; add its replicas and rollout overlap to the shared budget before production startup`);
103
+ }
104
+ const connectionBudget = serviceBudget.connectionBudget;
105
+ const port = parsed.port || "5432";
106
+ if (connectionBudget === "direct") {
107
+ if (port !== "5432") {
108
+ throw new Error(`postgres_connection_budget_url_mismatch: service=${service} planned=direct port=${port}`);
109
+ }
110
+ return;
111
+ }
112
+ if (port !== "6432") {
113
+ throw new Error(`postgres_connection_budget_url_mismatch: service=${service} planned=transaction_pool pool=${connectionBudget} port=${port}`);
114
+ }
115
+ }
116
+ export function assertPostgresServicePoolBudget(service, configuredPoolConnections) {
117
+ const expected = POSTGRES_CONNECTION_BUDGET.services[service].poolConnectionsPerReplica;
118
+ if (configuredPoolConnections !== expected) {
119
+ throw new Error(`postgres_connection_budget_pool_mismatch: service=${service} configured=${configuredPoolConnections} planned=${expected}; update the shared budget and every affected service in one change`);
120
+ }
121
+ }
122
+ export function assertPostgresServiceNamedLockBudget(service, configuredNamedLockTransactions) {
123
+ const expected = POSTGRES_CONNECTION_BUDGET.services[service].maxPinnedTransactionsPerReplica;
124
+ if (configuredNamedLockTransactions !== expected) {
125
+ throw new Error(`postgres_connection_budget_named_lock_mismatch: service=${service} configured=${configuredNamedLockTransactions} planned=${expected}`);
126
+ }
127
+ }
128
+ export function assertPostgresServerBudget(server) {
129
+ const summary = calculatePostgresConnectionBudget(POSTGRES_CONNECTION_BUDGET, server);
130
+ if (summary.unallocatedConnections < 0) {
131
+ throw new Error(`postgres_connection_budget_exceeded: planned=${summary.plannedConnections} usable=${summary.usableServerConnections} max=${server.maxConnections} superuserReserved=${server.superuserReservedConnections}`);
132
+ }
133
+ return summary;
134
+ }
135
+ export const POSTGRES_CONNECTION_BUDGET_SUMMARY = assertPostgresServerBudget(POSTGRES_CONNECTION_BUDGET.server);
@@ -0,0 +1,50 @@
1
+ {
2
+ "server": {
3
+ "maxConnections": 25,
4
+ "superuserReservedConnections": 3
5
+ },
6
+ "operationalReserveConnections": 4,
7
+ "transactionPools": {
8
+ "controlPlaneRuntime": {
9
+ "serverConnections": 8
10
+ }
11
+ },
12
+ "services": {
13
+ "api": {
14
+ "enabledInProduction": true,
15
+ "replicas": 2,
16
+ "poolConnectionsPerReplica": 4,
17
+ "fixedConnectionsPerReplica": 0,
18
+ "maxPinnedTransactionsPerReplica": 1,
19
+ "rolloutOverlapReplicas": 1,
20
+ "connectionBudget": "controlPlaneRuntime"
21
+ },
22
+ "worker": {
23
+ "enabledInProduction": true,
24
+ "replicas": 1,
25
+ "poolConnectionsPerReplica": 4,
26
+ "fixedConnectionsPerReplica": 0,
27
+ "maxPinnedTransactionsPerReplica": 1,
28
+ "rolloutOverlapReplicas": 1,
29
+ "connectionBudget": "controlPlaneRuntime"
30
+ },
31
+ "cast": {
32
+ "enabledInProduction": true,
33
+ "replicas": 1,
34
+ "poolConnectionsPerReplica": 4,
35
+ "fixedConnectionsPerReplica": 1,
36
+ "maxPinnedTransactionsPerReplica": 0,
37
+ "rolloutOverlapReplicas": 1,
38
+ "connectionBudget": "direct"
39
+ },
40
+ "scanWorker": {
41
+ "enabledInProduction": false,
42
+ "replicas": 0,
43
+ "poolConnectionsPerReplica": 2,
44
+ "fixedConnectionsPerReplica": 0,
45
+ "maxPinnedTransactionsPerReplica": 1,
46
+ "rolloutOverlapReplicas": 0,
47
+ "connectionBudget": "controlPlaneRuntime"
48
+ }
49
+ }
50
+ }