@spacefast/common 0.0.6 → 0.0.8

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 (65) hide show
  1. package/brand-assets/spacefast-favicon.svg +16 -0
  2. package/brand-assets/spacefast-sf-full-bleed.svg +13 -0
  3. package/brand-assets/spacefast-wordmark.svg +5 -0
  4. package/dist/agents/connect-targets.d.ts +6 -1
  5. package/dist/agents/connect-targets.js +19 -0
  6. package/dist/brand-assets-build.d.ts +26 -0
  7. package/dist/brand-assets-build.js +81 -0
  8. package/dist/brand-assets.d.ts +42 -0
  9. package/dist/brand-assets.js +30 -0
  10. package/dist/contracts/access.d.ts +143 -0
  11. package/dist/contracts/access.js +33 -0
  12. package/dist/contracts/api-keys.d.ts +123 -0
  13. package/dist/contracts/api-keys.js +65 -0
  14. package/dist/contracts/builds.d.ts +40 -0
  15. package/dist/contracts/builds.js +17 -0
  16. package/dist/contracts/enums.js +5 -4
  17. package/dist/contracts/error-code-meta.d.ts +116 -4
  18. package/dist/contracts/error-code-meta.js +29 -1
  19. package/dist/contracts/error-codes.d.ts +1 -1
  20. package/dist/contracts/error-codes.js +28 -0
  21. package/dist/contracts/feature-lifecycle-core.d.ts +36 -0
  22. package/dist/contracts/feature-lifecycle-core.js +220 -0
  23. package/dist/contracts/feature-lifecycle.d.ts +7 -0
  24. package/dist/contracts/feature-lifecycle.js +31 -0
  25. package/dist/contracts/generated-feature-launch-entries.d.ts +2 -0
  26. package/dist/contracts/generated-feature-launch-entries.js +518 -0
  27. package/dist/contracts/ids.d.ts +4 -0
  28. package/dist/contracts/ids.js +8 -0
  29. package/dist/contracts/operations.d.ts +161 -1
  30. package/dist/contracts/operations.js +49 -1
  31. package/dist/contracts/platform.d.ts +6 -0
  32. package/dist/contracts/platform.js +6 -0
  33. package/dist/contracts/publishes.d.ts +130 -0
  34. package/dist/contracts/publishes.js +145 -0
  35. package/dist/contracts/repository-connections.d.ts +3 -0
  36. package/dist/contracts/repository-connections.js +13 -0
  37. package/dist/contracts/resources.d.ts +2 -0
  38. package/dist/contracts/resources.js +10 -2
  39. package/dist/contracts/runtime-api.d.ts +2 -0
  40. package/dist/contracts/runtime-api.js +1 -0
  41. package/dist/contracts/sf-config-v1.d.ts +75 -0
  42. package/dist/contracts/sf-config-v1.js +6 -0
  43. package/dist/contracts/spaces.d.ts +12 -3
  44. package/dist/contracts/spaces.js +22 -2
  45. package/dist/contracts/superadmin-spaces.d.ts +2 -0
  46. package/dist/contracts/superadmin-tenants.d.ts +6 -6
  47. package/dist/contracts/superadmin-tenants.js +6 -11
  48. package/dist/contracts/teams.d.ts +8 -11
  49. package/dist/contracts/teams.js +11 -16
  50. package/dist/docs/agent-setup.d.ts +51 -0
  51. package/dist/docs/agent-setup.js +168 -3
  52. package/dist/docs/error-docs.js +113 -1
  53. package/dist/slug-policy/blocklist.d.ts +4 -0
  54. package/dist/slug-policy/blocklist.js +122 -0
  55. package/dist/slug-policy/index.d.ts +17 -0
  56. package/dist/slug-policy/index.js +86 -0
  57. package/dist/utils/concurrency.d.ts +1 -0
  58. package/dist/utils/concurrency.js +22 -0
  59. package/dist/utils/email.d.ts +1 -0
  60. package/dist/utils/email.js +3 -0
  61. package/dist/utils/query-keys.d.ts +1 -0
  62. package/dist/utils/query-keys.js +4 -0
  63. package/dist/utils/sf-config-v1.d.ts +2 -0
  64. package/dist/utils/sf-config-v1.js +542 -0
  65. package/package.json +20 -1
@@ -0,0 +1,75 @@
1
+ export declare const SF_CONFIG_V1_CANONICAL_FILE = "sf.jsonc";
2
+ export declare const SF_CONFIG_V1_FILES: readonly ["sf.jsonc", "spacefast.jsonc", "spacefast.json", "sf.json", ".sf/sf.json", ".sf/config.jsonc", ".sf/config.json"];
3
+ /** Published JSON Schema URL for the strict `sf.jsonc` v1 contract. */
4
+ export declare const SF_CONFIG_V1_SCHEMA_URL: `${string}/schemas/sf.v1.json`;
5
+ export type SfConfigV1 = {
6
+ $schema?: typeof SF_CONFIG_V1_SCHEMA_URL;
7
+ version: 1;
8
+ build?: {
9
+ installRoot?: string;
10
+ installCommand?: string;
11
+ command?: string;
12
+ output?: string;
13
+ timeoutSeconds?: number;
14
+ };
15
+ serve?: {
16
+ index?: string | false;
17
+ spaFallback?: string;
18
+ cleanUrls?: boolean;
19
+ directoryListing?: boolean;
20
+ };
21
+ metadata?: {
22
+ title?: string;
23
+ description?: string;
24
+ image?: string;
25
+ };
26
+ substitute?: {
27
+ files: string[];
28
+ };
29
+ };
30
+ export type EffectiveConfigV1 = {
31
+ version: 1;
32
+ build?: NonNullable<SfConfigV1["build"]> & {
33
+ installRoot: string;
34
+ timeoutSeconds: number;
35
+ };
36
+ serve: {
37
+ index: string | false;
38
+ spaFallback?: string;
39
+ cleanUrls: boolean;
40
+ directoryListing: boolean;
41
+ };
42
+ metadata?: SfConfigV1["metadata"];
43
+ substitute?: SfConfigV1["substitute"];
44
+ };
45
+ export type SfConfigV1Issue = {
46
+ code: "config_alias" | "config_conflict" | "config_identity_moved" | "config_invalid" | "config_key_removed" | "config_unknown_key" | "serve_index_required" | "template_syntax_retired";
47
+ severity: "error" | "warning";
48
+ path: string;
49
+ line: number;
50
+ column: number;
51
+ message: string;
52
+ suggestion?: string;
53
+ };
54
+ export type SfConfigV1Location = Pick<SfConfigV1Issue, "line" | "column">;
55
+ type SfConfigV1CompileResultBase = {
56
+ issues: SfConfigV1Issue[];
57
+ locations: ReadonlyMap<string, SfConfigV1Location>;
58
+ };
59
+ export type SfConfigV1CompileResult = SfConfigV1CompileResultBase & ({
60
+ success: true;
61
+ config: SfConfigV1;
62
+ effective: EffectiveConfigV1;
63
+ } | {
64
+ success: false;
65
+ config: null;
66
+ effective: null;
67
+ });
68
+ export type CompileSfConfigV1Options = {
69
+ artifactPaths?: readonly string[];
70
+ templateSources?: readonly {
71
+ path: string;
72
+ source: string;
73
+ }[];
74
+ };
75
+ export {};
@@ -0,0 +1,6 @@
1
+ import { BRAND } from "../brand.js";
2
+ import { SPACE_CONFIG_ACCEPTED_FILES, SPACE_CONFIG_CANONICAL_FILE, } from "./space-config.js";
3
+ export const SF_CONFIG_V1_CANONICAL_FILE = SPACE_CONFIG_CANONICAL_FILE;
4
+ export const SF_CONFIG_V1_FILES = SPACE_CONFIG_ACCEPTED_FILES;
5
+ /** Published JSON Schema URL for the strict `sf.jsonc` v1 contract. */
6
+ export const SF_CONFIG_V1_SCHEMA_URL = `${BRAND.websiteOrigin}/schemas/sf.v1.json`;
@@ -189,6 +189,7 @@ export declare const SpaceCreateResponseSchema: z.ZodObject<{
189
189
  noscript: z.ZodOptional<z.ZodArray<z.ZodString>>;
190
190
  }, z.core.$strip>>;
191
191
  }, z.core.$strip>>;
192
+ settingsDigest: z.ZodString;
192
193
  noindex: z.ZodBoolean;
193
194
  policy: z.ZodNullable<z.ZodObject<{
194
195
  rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -586,6 +587,8 @@ export declare const spacePatchSchema: z.ZodObject<{
586
587
  noscript: z.ZodOptional<z.ZodArray<z.ZodString>>;
587
588
  }, z.core.$strip>>;
588
589
  }, z.core.$strip>>>;
590
+ baseSettingsDigest: z.ZodOptional<z.ZodString>;
591
+ force: z.ZodOptional<z.ZodBoolean>;
589
592
  dataLocation: z.ZodOptional<z.ZodString>;
590
593
  placement: z.ZodOptional<z.ZodObject<{
591
594
  region: z.ZodOptional<z.ZodString>;
@@ -756,6 +759,11 @@ export declare const spaceVersionCreateSchema: z.ZodObject<{
756
759
  }>>;
757
760
  sourceMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
758
761
  buildLog: z.ZodOptional<z.ZodString>;
762
+ conventionFiles: z.ZodOptional<z.ZodObject<{
763
+ redirects: z.ZodOptional<z.ZodString>;
764
+ headers: z.ZodOptional<z.ZodString>;
765
+ routes: z.ZodOptional<z.ZodString>;
766
+ }, z.core.$strip>>;
759
767
  files: z.ZodOptional<z.ZodArray<z.ZodObject<{
760
768
  path: z.ZodString;
761
769
  size: z.ZodNumber;
@@ -1203,6 +1211,7 @@ export declare const spaceListSchema: z.ZodObject<{
1203
1211
  noscript: z.ZodOptional<z.ZodArray<z.ZodString>>;
1204
1212
  }, z.core.$strip>>;
1205
1213
  }, z.core.$strip>>;
1214
+ settingsDigest: z.ZodString;
1206
1215
  noindex: z.ZodBoolean;
1207
1216
  policy: z.ZodNullable<z.ZodObject<{
1208
1217
  rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -2009,11 +2018,11 @@ export declare const spaceVersionComputedRoutingSummarySchema: z.ZodObject<{
2009
2018
  }, z.core.$strip>;
2010
2019
  export declare const spaceVersionFileDiffStatusSchema: z.ZodEnum<{
2011
2020
  deleted: "deleted";
2021
+ unchanged: "unchanged";
2012
2022
  binary: "binary";
2013
2023
  added: "added";
2014
2024
  modified: "modified";
2015
2025
  renamed: "renamed";
2016
- unchanged: "unchanged";
2017
2026
  too_large: "too_large";
2018
2027
  }>;
2019
2028
  export declare const spaceVersionFileDiffEntrySchema: z.ZodObject<{
@@ -2021,11 +2030,11 @@ export declare const spaceVersionFileDiffEntrySchema: z.ZodObject<{
2021
2030
  oldPath: z.ZodNullable<z.ZodString>;
2022
2031
  status: z.ZodEnum<{
2023
2032
  deleted: "deleted";
2033
+ unchanged: "unchanged";
2024
2034
  binary: "binary";
2025
2035
  added: "added";
2026
2036
  modified: "modified";
2027
2037
  renamed: "renamed";
2028
- unchanged: "unchanged";
2029
2038
  too_large: "too_large";
2030
2039
  }>;
2031
2040
  before: z.ZodNullable<z.ZodObject<{
@@ -2091,11 +2100,11 @@ export declare const spaceVersionFileDiffResponseSchema: z.ZodObject<{
2091
2100
  oldPath: z.ZodNullable<z.ZodString>;
2092
2101
  status: z.ZodEnum<{
2093
2102
  deleted: "deleted";
2103
+ unchanged: "unchanged";
2094
2104
  binary: "binary";
2095
2105
  added: "added";
2096
2106
  modified: "modified";
2097
2107
  renamed: "renamed";
2098
- unchanged: "unchanged";
2099
2108
  too_large: "too_large";
2100
2109
  }>;
2101
2110
  before: z.ZodNullable<z.ZodObject<{
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { AsyncOperationSchema, baseDiagnosticSchema, CursorPaginationSchema, cursorListQuerySchema, } from "./common.js";
3
3
  import { versionStatus, spaceStatus } from "./enums.js";
4
4
  import { conventionFilesSchema, uploadInstructionsSchema, manifestFileSchema } from "./internal.js";
5
- import { ChannelPointerSchema, VersionSchema, PublishSourceSchema, SpaceSchema, } from "./resources.js";
5
+ import { ChannelPointerSchema, VersionSchema, PublishSourceSchema, SpaceSchema, spaceSettingsDigestSchema, } from "./resources.js";
6
6
  import { spaceConfigSchema, spacePlacementConfigSchema } from "./space-config.js";
7
7
  import { principalIdSchema } from "./transfers.js";
8
8
  import { zeroRuntimeKindSchema } from "./zero.js";
@@ -67,12 +67,20 @@ export const spaceCreateSchema = z
67
67
  })
68
68
  .optional()
69
69
  .default({});
70
- export const spacePatchSchema = z.object({
70
+ export const spacePatchSchema = z
71
+ .object({
71
72
  title: z.string().min(1).max(255).optional(),
72
73
  config: spaceConfigSchema
73
74
  .nullable()
74
75
  .optional()
75
76
  .describe("Replaces the stored SpaceConfig overlay; null clears it."),
77
+ baseSettingsDigest: spaceSettingsDigestSchema
78
+ .optional()
79
+ .describe("Optimistic-concurrency base for versioned settings (title/config): the `settingsDigest` read from the Space resource when the form was loaded. When provided and the space's current settings digest no longer matches, the PATCH fails with 409 publish_base_changed and changes nothing."),
80
+ force: z
81
+ .boolean()
82
+ .optional()
83
+ .describe("With baseSettingsDigest: acknowledge a changed base and overwrite anyway. The forced overwrite is recorded in the space activity feed. Never the default."),
76
84
  dataLocation: z.string().min(1).optional(),
77
85
  placement: spacePlacementConfigSchema.optional(),
78
86
  noindex: z
@@ -86,6 +94,15 @@ export const spacePatchSchema = z.object({
86
94
  .nullable()
87
95
  .optional()
88
96
  .describe("Sets (string) or removes (null) space password protection."),
97
+ })
98
+ .superRefine((value, ctx) => {
99
+ if (value.force === true && value.baseSettingsDigest === undefined) {
100
+ ctx.addIssue({
101
+ code: z.ZodIssueCode.custom,
102
+ message: "force requires baseSettingsDigest.",
103
+ path: ["force"],
104
+ });
105
+ }
89
106
  });
90
107
  export const spaceClaimSchema = z
91
108
  .object({
@@ -221,6 +238,9 @@ export const spaceVersionCreateSchema = z
221
238
  .max(64_000)
222
239
  .optional()
223
240
  .describe("Bounded build transcript for deploys that ran a local build before publishing."),
241
+ conventionFiles: conventionFilesSchema
242
+ .optional()
243
+ .describe("Routing convention contents used when deciding whether this publish is unchanged. The same values are supplied again when the version is finalized."),
224
244
  files: z
225
245
  .array(manifestFileSchema)
226
246
  .optional()
@@ -298,6 +298,7 @@ export declare const SuperadminSpaceDetailSchema: z.ZodObject<{
298
298
  noscript: z.ZodOptional<z.ZodArray<z.ZodString>>;
299
299
  }, z.core.$strip>>;
300
300
  }, z.core.$strip>>;
301
+ settingsDigest: z.ZodString;
301
302
  noindex: z.ZodBoolean;
302
303
  policy: z.ZodNullable<z.ZodObject<{
303
304
  rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -706,6 +707,7 @@ export declare const SuperadminAnonymousSpaceAssignResponseSchema: z.ZodObject<{
706
707
  noscript: z.ZodOptional<z.ZodArray<z.ZodString>>;
707
708
  }, z.core.$strip>>;
708
709
  }, z.core.$strip>>;
710
+ settingsDigest: z.ZodString;
709
711
  noindex: z.ZodBoolean;
710
712
  policy: z.ZodNullable<z.ZodObject<{
711
713
  rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  export declare const SuperadminProviderClientSchema: z.ZodObject<{
3
3
  id: z.ZodString;
4
4
  provider: z.ZodLiteral<"wpcloud">;
5
- tenantId: z.ZodNullable<z.ZodString>;
5
+ tenantId: z.ZodString;
6
6
  name: z.ZodString;
7
7
  label: z.ZodString;
8
8
  apiBase: z.ZodNullable<z.ZodString>;
@@ -20,7 +20,7 @@ export declare const SuperadminProviderClientListSchema: z.ZodObject<{
20
20
  clients: z.ZodArray<z.ZodObject<{
21
21
  id: z.ZodString;
22
22
  provider: z.ZodLiteral<"wpcloud">;
23
- tenantId: z.ZodNullable<z.ZodString>;
23
+ tenantId: z.ZodString;
24
24
  name: z.ZodString;
25
25
  label: z.ZodString;
26
26
  apiBase: z.ZodNullable<z.ZodString>;
@@ -42,7 +42,7 @@ export declare const superadminProviderClientCreateSchema: z.ZodObject<{
42
42
  apiBase: z.ZodOptional<z.ZodString>;
43
43
  defaultDatacenter: z.ZodOptional<z.ZodString>;
44
44
  defaultPhpVersion: z.ZodOptional<z.ZodString>;
45
- tenantId: z.ZodOptional<z.ZodString>;
45
+ tenantId: z.ZodString;
46
46
  }, z.core.$strip>;
47
47
  export declare const superadminProviderClientPatchSchema: z.ZodObject<{
48
48
  label: z.ZodOptional<z.ZodString>;
@@ -54,7 +54,7 @@ export declare const superadminProviderClientPatchSchema: z.ZodObject<{
54
54
  active: "active";
55
55
  disabled: "disabled";
56
56
  }>>;
57
- tenantId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
57
+ tenantId: z.ZodOptional<z.ZodString>;
58
58
  }, z.core.$strip>;
59
59
  export declare const superadminProviderClientTestResponseSchema: z.ZodObject<{
60
60
  ok: z.ZodBoolean;
@@ -83,7 +83,7 @@ export declare const SuperadminTenantSchema: z.ZodObject<{
83
83
  providerClient: z.ZodNullable<z.ZodObject<{
84
84
  id: z.ZodString;
85
85
  provider: z.ZodLiteral<"wpcloud">;
86
- tenantId: z.ZodNullable<z.ZodString>;
86
+ tenantId: z.ZodString;
87
87
  name: z.ZodString;
88
88
  label: z.ZodString;
89
89
  apiBase: z.ZodNullable<z.ZodString>;
@@ -125,7 +125,7 @@ export declare const SuperadminTenantListSchema: z.ZodObject<{
125
125
  providerClient: z.ZodNullable<z.ZodObject<{
126
126
  id: z.ZodString;
127
127
  provider: z.ZodLiteral<"wpcloud">;
128
- tenantId: z.ZodNullable<z.ZodString>;
128
+ tenantId: z.ZodString;
129
129
  name: z.ZodString;
130
130
  label: z.ZodString;
131
131
  apiBase: z.ZodNullable<z.ZodString>;
@@ -4,16 +4,13 @@ import { providerClientStatus, tenantKind, tenantStatus } from "./enums.js";
4
4
  import { superadminPaginationSchema } from "./superadmin.js";
5
5
  // ProviderClient (`pvc_`) = one WP.Cloud API client identity sites are
6
6
  // provisioned under (internal-docs/platform.md "ProviderClient"). Operator-only
7
- // resource. `tenantId` is null for the Spacefast first-party client; a tenant
8
- // bringing its own WP.Cloud account owns exactly one row. The API token is
9
- // write-only; responses expose only a masked hint, never the secret.
7
+ // resource. Each row is a tenant-owned BYO account; the first-party client is
8
+ // configured separately as the platform context. The API token is write-only;
9
+ // responses expose only a masked hint, never the secret.
10
10
  export const SuperadminProviderClientSchema = z.object({
11
11
  id: z.string().describe("ProviderClient id (`pvc_`)."),
12
12
  provider: z.literal("wpcloud").describe("Hosting provider this client identity belongs to."),
13
- tenantId: z
14
- .string()
15
- .nullable()
16
- .describe("Owning tenant; null for the Spacefast first-party client."),
13
+ tenantId: z.string().describe("Tenant that owns this WP.Cloud account."),
17
14
  name: z.string().describe("The WP.Cloud client/account name used as the {client} path param."),
18
15
  label: z.string().describe("Human-readable operator label."),
19
16
  apiBase: z.string().nullable().describe("API base URL override; null uses the default."),
@@ -51,8 +48,7 @@ export const superadminProviderClientCreateSchema = z.object({
51
48
  apiBase: z.string().url().optional(),
52
49
  defaultDatacenter: z.string().trim().min(1).optional(),
53
50
  defaultPhpVersion: z.string().trim().min(1).optional(),
54
- // Connect the client to a tenant at creation; omit for the first-party client.
55
- tenantId: z.string().trim().min(1).optional(),
51
+ tenantId: z.string().trim().min(1),
56
52
  });
57
53
  export const superadminProviderClientPatchSchema = z.object({
58
54
  label: z.string().trim().min(1).max(255).optional(),
@@ -62,8 +58,7 @@ export const superadminProviderClientPatchSchema = z.object({
62
58
  defaultDatacenter: z.string().trim().min(1).nullable().optional(),
63
59
  defaultPhpVersion: z.string().trim().min(1).nullable().optional(),
64
60
  status: providerClientStatus.optional(),
65
- // Connect/disconnect the owning tenant (null disconnects → first-party pool).
66
- tenantId: z.string().trim().min(1).nullable().optional(),
61
+ tenantId: z.string().trim().min(1).optional(),
67
62
  });
68
63
  // Live connectivity check against the client's WP.Cloud account.
69
64
  export const superadminProviderClientTestResponseSchema = z.object({
@@ -197,17 +197,14 @@ export declare const teamSettingsResponseSchema: z.ZodObject<{
197
197
  label: z.ZodString;
198
198
  }, z.core.$strip>>;
199
199
  wpCloudCapabilities: z.ZodObject<{
200
- status: z.ZodEnum<{
201
- failed: "failed";
202
- ready: "ready";
203
- running: "running";
204
- idle: "idle";
205
- }>;
206
- degraded: z.ZodBoolean;
207
- lastSyncedAt: z.ZodNullable<z.ZodString>;
208
- lastError: z.ZodNullable<z.ZodString>;
209
- regionCount: z.ZodNumber;
210
- phpVersionCount: z.ZodNumber;
200
+ datacenters: z.ZodArray<z.ZodObject<{
201
+ id: z.ZodString;
202
+ label: z.ZodString;
203
+ }, z.core.$strip>>;
204
+ phpVersions: z.ZodArray<z.ZodObject<{
205
+ id: z.ZodString;
206
+ label: z.ZodString;
207
+ }, z.core.$strip>>;
211
208
  }, z.core.$strip>;
212
209
  }, z.core.$strip>;
213
210
  export declare const teamUsageResponseSchema: z.ZodObject<{
@@ -107,29 +107,24 @@ export const teamDataLocationSchema = z
107
107
  export const teamSettingsUpdateSchema = z.object({
108
108
  defaultDataLocation: teamDataLocationSchema,
109
109
  });
110
+ const wpCloudCapabilityOptionSchema = z.object({
111
+ id: z.string(),
112
+ label: z.string(),
113
+ });
110
114
  // Served by `GET /v1/teams/{teamId}/settings` (and echoed by the PATCH):
111
115
  // the default data location plus the placement options it may take.
112
116
  export const teamSettingsResponseSchema = z.object({
113
117
  teamId: z.string().describe("Team the settings belong to."),
114
118
  defaultDataLocation: teamDataLocationSchema,
115
119
  regionOptions: z
116
- .array(z.object({
117
- id: z.string().describe("Placement region id."),
118
- label: z.string().describe("Human-readable region label."),
119
- }))
120
+ .array(wpCloudCapabilityOptionSchema)
120
121
  .describe("Placement regions currently available for new spaces."),
121
122
  wpCloudCapabilities: z
122
123
  .object({
123
- status: z
124
- .enum(["idle", "running", "ready", "failed"])
125
- .describe("State of the provider capability sync."),
126
- degraded: z.boolean().describe("Whether the last sync finished with an error."),
127
- lastSyncedAt: z.string().nullable().describe("When capabilities last synced."),
128
- lastError: z.string().nullable().describe("Last sync error, if any."),
129
- regionCount: z.number().int().describe("Known placement regions."),
130
- phpVersionCount: z.number().int().describe("Known runtime PHP versions."),
124
+ datacenters: z.array(wpCloudCapabilityOptionSchema),
125
+ phpVersions: z.array(wpCloudCapabilityOptionSchema),
131
126
  })
132
- .describe("Provider capability sync status backing the region options."),
127
+ .describe("Live capabilities for the team's WP.Cloud provider account."),
133
128
  });
134
129
  // Served by `GET /v1/teams/{teamId}/usage`: counts and metered usage against
135
130
  // the named plan limits.
@@ -180,9 +175,9 @@ export const teamUpdateSchema = z
180
175
  .trim()
181
176
  .min(1)
182
177
  .max(255)
183
- .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/, {
184
- message: "Team slugs must contain lowercase letters, numbers, and hyphens.",
185
- })
178
+ // Keep canonical guidance in OpenAPI without rejecting an exact legacy
179
+ // no-op before the shared policy can preserve its stored bytes.
180
+ .meta({ pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$" })
186
181
  .optional(),
187
182
  logo: z.string().trim().max(2048).nullable().optional(),
188
183
  metadata: z.string().nullable().optional(),
@@ -1,6 +1,57 @@
1
+ import { type ConnectTargetId } from "../agents/connect-targets.js";
1
2
  export declare const agentSetupUrl: string;
2
3
  export declare const agentSetupTextUrl: string;
3
4
  export declare const agentSetupHostLabel: string;
4
5
  export declare const agentSetupPrompt: string;
6
+ /**
7
+ * Typed, safe decline states for the app-private redemption exchange.
8
+ * `invalid_handoff` is the single answer for both an unknown document ID and a wrong secret, so a
9
+ * decline never becomes an existence oracle; state-specific reasons are returned only after the
10
+ * supplied secret authenticates the handoff. `already_redeemed` makes one-use explicit: after the
11
+ * first exchange succeeds, the second fails with this reason. Declines are terminal and never
12
+ * reveal whether a secret was close to correct.
13
+ */
14
+ export declare const AGENT_HANDOFF_DECLINE_REASONS: readonly ["invalid_handoff", "expired", "already_redeemed", "revoked", "client_mismatch", "origin_mismatch"];
15
+ export type AgentHandoffDeclineReason = (typeof AGENT_HANDOFF_DECLINE_REASONS)[number];
16
+ export type AgentHandoffAction = Readonly<{
17
+ kind: "copy";
18
+ label: string;
19
+ sensitive: boolean;
20
+ value: string;
21
+ }> | Readonly<{
22
+ kind: "open";
23
+ label: string;
24
+ href: string;
25
+ }>;
26
+ export type AgentHandoff = Readonly<{
27
+ actions: readonly AgentHandoffAction[];
28
+ documentUrl: string;
29
+ instructions: string;
30
+ prompt: string;
31
+ }>;
32
+ /**
33
+ * The single shared grammar for handoff links:
34
+ * `https://<app-origin>/agent/<documentId>#<secret>`. The document ID must match
35
+ * `[A-Za-z0-9_-]{1,512}` and the secret `[A-Za-z0-9_-]{16,512}` (the minimum keeps secrets
36
+ * high-entropy and makes the public-projection leak check meaningful); the two must differ;
37
+ * queries and embedded credentials are forbidden; HTTPS is required except on loopback hosts.
38
+ * Normalization is part of the contract: the input is WHATWG-URL-normalized first (backslashes,
39
+ * dot segments, stray tab/CR/LF, empty userinfo, case, default ports), the grammar is enforced on
40
+ * the normalized URL, and only the reconstructed canonical URL ever leaves this function.
41
+ * Dashboard minting, CLI ingestion, and control-plane validation must all use this parser so the
42
+ * grammar can never fork.
43
+ */
44
+ export declare function parseAgentHandoffUrl(value: string, expectedOrigin: string): Readonly<{
45
+ documentId: string;
46
+ documentUrl: string;
47
+ secret: string;
48
+ }>;
49
+ /** Builds every copy-ready handoff artifact from one validated URL and target catalog row. */
50
+ export declare function buildAgentHandoff(input: {
51
+ expectedOrigin: string;
52
+ mcpEndpoint: string;
53
+ targetId: ConnectTargetId;
54
+ url: string;
55
+ }): AgentHandoff;
5
56
  export declare function renderAgentSetupTemplate(value: string): string;
6
57
  export declare function aiInstructionsText(): string;
@@ -1,11 +1,176 @@
1
- import { BRAND } from "@spacefast/common/brand";
2
- import { CHECK_BEFORE_PUBLISH_PROSE, CONTINUE_AFTER_CLAIM_PROSE, ENV_NOTES_PROSE, FAILURE_CONDUCT_PROSE, HOW_TO_RESEARCH_PROSE, LANE_PICKER_PROSE, POSITIONING_PROSE, PUBLISH_SEQUENCE_PROSE, RECEIPT_SHAPE_PROSE, SAVE_STATE_PROSE, SECRET_RULES_PROSE, SIGNED_UPLOAD_PROSE, UPDATE_SPACE_PROSE, } from "@spacefast/common/docs/agent-prose";
3
- import { spacefastInstallCurlCommand, spacefastInstallPowerShellCommand, spacefastSkillFetchCommand, spacefastSkillInstallCommand, spacefastSkillUrl, } from "@spacefast/common/docs/skill-distribution";
1
+ import { getConnectTarget, } from "../agents/connect-targets.js";
2
+ import { BRAND } from "../brand.js";
3
+ import { CHECK_BEFORE_PUBLISH_PROSE, CONTINUE_AFTER_CLAIM_PROSE, ENV_NOTES_PROSE, FAILURE_CONDUCT_PROSE, HOW_TO_RESEARCH_PROSE, LANE_PICKER_PROSE, POSITIONING_PROSE, PUBLISH_SEQUENCE_PROSE, RECEIPT_SHAPE_PROSE, SAVE_STATE_PROSE, SECRET_RULES_PROSE, SIGNED_UPLOAD_PROSE, UPDATE_SPACE_PROSE, } from "./agent-prose.js";
4
+ import { spacefastInstallCurlCommand, spacefastInstallPowerShellCommand, spacefastSkillFetchCommand, spacefastSkillInstallCommand, spacefastSkillUrl, } from "./skill-distribution.js";
4
5
  export const agentSetupUrl = `${BRAND.agentSetupOrigin}/ai`;
5
6
  export const agentSetupTextUrl = `${BRAND.agentSetupOrigin}/ai.txt`;
6
7
  const agentSetupParsedUrl = new URL(agentSetupUrl);
7
8
  export const agentSetupHostLabel = agentSetupParsedUrl.host + agentSetupParsedUrl.pathname;
8
9
  export const agentSetupPrompt = `Fetch ${agentSetupUrl} and follow it to publish this project — or update it if it's already on ${BRAND.productName}. Ask me what I want to publish if it isn't obvious.`;
10
+ /*
11
+ * Agent handoff contract: links use https://<app-origin>/agent/<documentId>#<secret>, with
12
+ * opaque document IDs and secrets. The fragment is the secret, so ordinary document fetches
13
+ * never send or reveal it. Fetching never consumes a handoff; exactly one successful redemption
14
+ * does. Redemption is app-private, reuses the existing device/OAuth approval service, and returns
15
+ * the already-approved finite Space-scoped credential: there is no public AgentGrant resource,
16
+ * durable key table, or team-wide publisher key. The CLI accepts links only through stdin or a
17
+ * browser callback and redacts them from JSON output, telemetry, shell history, doctor, and
18
+ * receipts. The dashboard can revoke an unused handoff.
19
+ */
20
+ const opaqueHandoffDocumentId = /^[A-Za-z0-9_-]{1,512}$/;
21
+ const opaqueHandoffSecret = /^[A-Za-z0-9_-]{16,512}$/;
22
+ const shellSafeMcpEndpoint = /^[A-Za-z0-9%+,\-./:=_[\]~]+$/;
23
+ /**
24
+ * Typed, safe decline states for the app-private redemption exchange.
25
+ * `invalid_handoff` is the single answer for both an unknown document ID and a wrong secret, so a
26
+ * decline never becomes an existence oracle; state-specific reasons are returned only after the
27
+ * supplied secret authenticates the handoff. `already_redeemed` makes one-use explicit: after the
28
+ * first exchange succeeds, the second fails with this reason. Declines are terminal and never
29
+ * reveal whether a secret was close to correct.
30
+ */
31
+ export const AGENT_HANDOFF_DECLINE_REASONS = [
32
+ "invalid_handoff",
33
+ "expired",
34
+ "already_redeemed",
35
+ "revoked",
36
+ "client_mismatch",
37
+ "origin_mismatch",
38
+ ];
39
+ function isSafeHttpUrl(url) {
40
+ return ((url.protocol === "https:" ||
41
+ (url.protocol === "http:" &&
42
+ (url.hostname === "localhost" ||
43
+ url.hostname === "127.0.0.1" ||
44
+ url.hostname === "[::1]"))) &&
45
+ !url.username &&
46
+ !url.password);
47
+ }
48
+ /**
49
+ * The single shared grammar for handoff links:
50
+ * `https://<app-origin>/agent/<documentId>#<secret>`. The document ID must match
51
+ * `[A-Za-z0-9_-]{1,512}` and the secret `[A-Za-z0-9_-]{16,512}` (the minimum keeps secrets
52
+ * high-entropy and makes the public-projection leak check meaningful); the two must differ;
53
+ * queries and embedded credentials are forbidden; HTTPS is required except on loopback hosts.
54
+ * Normalization is part of the contract: the input is WHATWG-URL-normalized first (backslashes,
55
+ * dot segments, stray tab/CR/LF, empty userinfo, case, default ports), the grammar is enforced on
56
+ * the normalized URL, and only the reconstructed canonical URL ever leaves this function.
57
+ * Dashboard minting, CLI ingestion, and control-plane validation must all use this parser so the
58
+ * grammar can never fork.
59
+ */
60
+ export function parseAgentHandoffUrl(value, expectedOrigin) {
61
+ let canonicalExpectedOrigin;
62
+ try {
63
+ canonicalExpectedOrigin = new URL(expectedOrigin).origin;
64
+ }
65
+ catch {
66
+ throw new TypeError("Agent handoff expectedOrigin must be a valid URL origin.");
67
+ }
68
+ let url;
69
+ try {
70
+ url = new URL(value);
71
+ }
72
+ catch {
73
+ throw new TypeError("Agent handoff URL must be a valid HTTP(S) URL.");
74
+ }
75
+ const documentId = /^\/agent\/([^/]+)$/.exec(url.pathname)?.[1];
76
+ const secret = url.hash.slice(1);
77
+ if (!isSafeHttpUrl(url) ||
78
+ url.origin !== canonicalExpectedOrigin ||
79
+ value.includes("?") ||
80
+ !documentId ||
81
+ !opaqueHandoffDocumentId.test(documentId) ||
82
+ !opaqueHandoffSecret.test(secret) ||
83
+ documentId === secret) {
84
+ throw new TypeError("Agent handoff URL must be an HTTPS /agent/<id> URL with one opaque fragment secret (16-512 chars, distinct from the id) and no credentials or query (HTTP is allowed on loopback hosts).");
85
+ }
86
+ return { documentId, documentUrl: `${url.origin}${url.pathname}`, secret };
87
+ }
88
+ function validateMcpEndpoint(value) {
89
+ let url;
90
+ try {
91
+ url = new URL(value);
92
+ }
93
+ catch {
94
+ throw new TypeError("MCP endpoint must be a valid HTTP(S) URL.");
95
+ }
96
+ if (!shellSafeMcpEndpoint.test(value) || !isSafeHttpUrl(url)) {
97
+ throw new TypeError("MCP endpoint must be a shell-safe HTTPS URL with no credentials, query, fragment, or control characters (HTTP is allowed on loopback hosts).");
98
+ }
99
+ return url.href.replace(/\/$/, "");
100
+ }
101
+ function renderAgentHandoffInstructions(documentUrl, target) {
102
+ const setupSections = [target.primary, target.config].flatMap((setup) => setup ? [[setup.label, `\`\`\`${setup.language}`, setup.value, "```"].join("\n")] : []);
103
+ const connectSections = [
104
+ target.blurb,
105
+ ...setupSections,
106
+ target.deeplink ? `${target.deeplink.label}: ${target.deeplink.href}` : null,
107
+ target.download ? `${target.download.label}: ${target.download.href}` : null,
108
+ ].filter((section) => section !== null);
109
+ const approvalGuidance = target.id === "claude-code" || target.id === "codex"
110
+ ? `Run \`sf setup agent\` to initiate the handoff exchange; if \`sf\` is missing, install it first (macOS/Linux: \`${spacefastInstallCurlCommand}\`; Windows PowerShell: \`${spacefastInstallPowerShellCommand}\`). When it asks for the handoff link on stdin, paste the original link into that prompt; never pass the link as an argv value. Ask the user to approve in the browser, then wait for the CLI's approval result before publishing.`
111
+ : "When the client opens browser approval, ask the user to approve there before publishing.";
112
+ const sections = [
113
+ `# Set up ${BRAND.productName} with ${target.name}`,
114
+ `This is the public setup document for ${documentUrl}.`,
115
+ "Talk to the user before installing tools or publishing. Confirm the file or folder they want to make public.",
116
+ ["## Connect", ...connectSections].join("\n\n"),
117
+ [
118
+ "## Continue the approved handoff",
119
+ "Keep the original handoff link private. Fetching this document did not use the link or send its fragment secret; the secret is spent only by one successful exchange. An expired, used, or revoked link fails safely, so ask the user for a fresh one from the dashboard.",
120
+ approvalGuidance,
121
+ "Keep its fragment out of commands, request URLs, logs, and replies.",
122
+ ].join("\n\n"),
123
+ [
124
+ "## Publish",
125
+ "After approval, confirm the selected Space and permissions, then publish only the files the user approved. Share the live URL and immutable version URL from the receipt.",
126
+ ].join("\n\n"),
127
+ `General agent instructions: ${agentSetupTextUrl}`,
128
+ ];
129
+ return `${sections.join("\n\n")}\n`;
130
+ }
131
+ /** Builds every copy-ready handoff artifact from one validated URL and target catalog row. */
132
+ export function buildAgentHandoff(input) {
133
+ const { documentUrl, secret } = parseAgentHandoffUrl(input.url, input.expectedOrigin);
134
+ const mcpEndpoint = validateMcpEndpoint(input.mcpEndpoint);
135
+ const target = getConnectTarget(input.targetId, {
136
+ mcpEndpoint,
137
+ });
138
+ const prompt = `Fetch ${documentUrl}#${secret} and follow the ${target.name} setup instructions to publish with ${BRAND.productName}. Ask me what I want to publish if it isn't obvious.`;
139
+ const instructions = renderAgentHandoffInstructions(documentUrl, target);
140
+ const actions = [
141
+ { kind: "copy", label: "Copy for my agent", sensitive: true, value: prompt },
142
+ { kind: "copy", label: "Copy Markdown", sensitive: false, value: instructions },
143
+ ];
144
+ if (target.deeplink) {
145
+ actions.push({
146
+ kind: "open",
147
+ label: target.deeplink.label,
148
+ href: target.deeplink.href,
149
+ });
150
+ }
151
+ if (target.download) {
152
+ actions.push({
153
+ kind: "open",
154
+ label: target.download.label,
155
+ href: target.download.href,
156
+ });
157
+ }
158
+ // The scanner-safe invariant is total, not statistical: no public projection may contain the
159
+ // secret, no matter what the caller fed in (a pathological mint, a poisoned MCP endpoint).
160
+ const publicProjections = [
161
+ documentUrl,
162
+ instructions,
163
+ ...actions.flatMap((action) => {
164
+ if (action.kind === "open")
165
+ return [action.href];
166
+ return action.sensitive ? [] : [action.value];
167
+ }),
168
+ ];
169
+ if (publicProjections.some((projection) => projection.includes(secret))) {
170
+ throw new TypeError("Agent handoff inputs would leak the fragment secret into a public projection.");
171
+ }
172
+ return { actions, documentUrl, instructions, prompt };
173
+ }
9
174
  export function renderAgentSetupTemplate(value) {
10
175
  return value
11
176
  .replaceAll("{{AGENT_SETUP_URL}}", agentSetupUrl)