@spacefast/common 0.0.12 → 0.0.13

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 (113) hide show
  1. package/dist/contracts/access.d.ts +386 -1201
  2. package/dist/contracts/access.js +240 -1057
  3. package/dist/contracts/activity.d.ts +1 -1
  4. package/dist/contracts/activity.js +6 -14
  5. package/dist/contracts/api-keys.d.ts +16 -0
  6. package/dist/contracts/api-keys.js +2 -0
  7. package/dist/contracts/archives.d.ts +1 -2
  8. package/dist/contracts/archives.js +4 -6
  9. package/dist/contracts/comments.d.ts +58 -85
  10. package/dist/contracts/comments.js +86 -67
  11. package/dist/contracts/countries.js +2 -7
  12. package/dist/contracts/data-source-routes.d.ts +20 -0
  13. package/dist/contracts/data-source-routes.js +15 -0
  14. package/dist/contracts/device-auth.d.ts +7 -3
  15. package/dist/contracts/enums.d.ts +2 -4
  16. package/dist/contracts/enums.js +3 -8
  17. package/dist/contracts/error-code-meta.d.ts +17 -20
  18. package/dist/contracts/error-code-meta.js +8 -5
  19. package/dist/contracts/error-codes.d.ts +1 -1
  20. package/dist/contracts/error-codes.js +4 -5
  21. package/dist/contracts/events.d.ts +13 -13
  22. package/dist/contracts/execution.d.ts +212 -0
  23. package/dist/contracts/execution.js +288 -0
  24. package/dist/contracts/feature-lifecycle.d.ts +21 -167
  25. package/dist/contracts/feature-lifecycle.js +27 -173
  26. package/dist/contracts/features.d.ts +13 -115
  27. package/dist/contracts/features.js +1 -31
  28. package/dist/contracts/functions.d.ts +729 -0
  29. package/dist/contracts/functions.js +485 -0
  30. package/dist/contracts/git.d.ts +1 -1
  31. package/dist/contracts/grants.d.ts +1158 -0
  32. package/dist/contracts/grants.js +696 -0
  33. package/dist/contracts/ids.d.ts +5 -3
  34. package/dist/contracts/ids.js +5 -3
  35. package/dist/contracts/mcp.d.ts +6 -6
  36. package/dist/contracts/oauth-resources.d.ts +5 -5
  37. package/dist/contracts/oauth-resources.js +1 -0
  38. package/dist/contracts/operations.d.ts +2 -2
  39. package/dist/contracts/pages.d.ts +6 -14
  40. package/dist/contracts/pages.js +6 -23
  41. package/dist/contracts/plan-policy.d.ts +1 -8
  42. package/dist/contracts/plan-policy.js +1 -4
  43. package/dist/contracts/platform.d.ts +78 -30
  44. package/dist/contracts/platform.js +59 -12
  45. package/dist/contracts/push-new.d.ts +26 -2
  46. package/dist/contracts/push-new.js +8 -0
  47. package/dist/contracts/quotas.d.ts +0 -1
  48. package/dist/contracts/quotas.js +0 -5
  49. package/dist/contracts/repository-connections.d.ts +1 -1
  50. package/dist/contracts/resources.d.ts +10 -66
  51. package/dist/contracts/resources.js +4 -10
  52. package/dist/contracts/runtime-api.d.ts +558 -265
  53. package/dist/contracts/runtime-api.js +178 -91
  54. package/dist/contracts/runtime-app.d.ts +138 -0
  55. package/dist/contracts/runtime-app.js +21 -0
  56. package/dist/contracts/sf-config-v1.d.ts +5 -0
  57. package/dist/contracts/sites.d.ts +5 -0
  58. package/dist/contracts/sites.js +12 -1
  59. package/dist/contracts/space-config.d.ts +46 -116
  60. package/dist/contracts/space-config.js +320 -24
  61. package/dist/contracts/spaces.d.ts +253 -158
  62. package/dist/contracts/spaces.js +68 -54
  63. package/dist/contracts/superadmin-emails.d.ts +2 -2
  64. package/dist/contracts/superadmin-spaces.d.ts +15 -130
  65. package/dist/contracts/superadmin-spaces.js +7 -0
  66. package/dist/contracts/tags.d.ts +23 -23
  67. package/dist/contracts/teams.d.ts +9 -9
  68. package/dist/contracts/transfers.d.ts +0 -1
  69. package/dist/contracts/zero.d.ts +195 -273
  70. package/dist/contracts/zero.js +78 -206
  71. package/dist/dashboard-paths/index.d.ts +2 -2
  72. package/dist/dashboard-paths/index.js +1 -0
  73. package/dist/docs/agent-prose.js +10 -8
  74. package/dist/docs/agent-setup.d.ts +2 -2
  75. package/dist/docs/agent-setup.js +11 -9
  76. package/dist/docs/agent-solutions.js +1 -1
  77. package/dist/docs/catalog.d.ts +28 -28
  78. package/dist/docs/catalog.js +29 -29
  79. package/dist/docs/error-docs.js +23 -27
  80. package/dist/utils/asset-fingerprint.d.ts +11 -0
  81. package/dist/utils/asset-fingerprint.js +46 -0
  82. package/dist/utils/auth-redirect.d.ts +1 -3
  83. package/dist/utils/auth-redirect.js +1 -21
  84. package/dist/utils/build-settings.d.ts +2 -0
  85. package/dist/utils/build-settings.js +61 -9
  86. package/dist/utils/credential-policy.d.ts +1 -1
  87. package/dist/utils/credential-policy.js +4 -0
  88. package/dist/utils/egress-policy.fixtures.json +1 -1
  89. package/dist/utils/grants.d.ts +53 -0
  90. package/dist/utils/grants.js +160 -0
  91. package/dist/utils/local-space-state.js +1 -0
  92. package/dist/utils/pages.d.ts +11 -5
  93. package/dist/utils/pages.js +19 -32
  94. package/dist/utils/publish-policy.d.ts +34 -4
  95. package/dist/utils/publish-policy.js +132 -61
  96. package/dist/utils/query-keys.d.ts +1 -6
  97. package/dist/utils/query-keys.js +1 -6
  98. package/dist/utils/space-config.d.ts +5 -1
  99. package/dist/utils/space-config.js +35 -0
  100. package/dist/utils/static-runtime-policy.d.ts +5 -0
  101. package/dist/utils/static-runtime-policy.js +54 -10
  102. package/dist/utils/upload-session.d.ts +3 -0
  103. package/dist/utils/upload-session.js +31 -0
  104. package/package.json +2 -1
  105. package/dist/utils/access-grant.d.ts +0 -14
  106. package/dist/utils/access-grant.js +0 -88
  107. package/dist/utils/access-match.d.ts +0 -46
  108. package/dist/utils/access-match.fixtures.json +0 -513
  109. package/dist/utils/access-match.js +0 -312
  110. package/dist/utils/access-verdict.fixtures.json +0 -421
  111. package/dist/utils/sf-config-v1.d.ts +0 -2
  112. package/dist/utils/sf-config-v1.fixtures.json +0 -9
  113. package/dist/utils/sf-config-v1.js +0 -551
@@ -1,3 +1,4 @@
1
+ import { load as parseYaml } from "js-yaml";
1
2
  import { parse as parseToml } from "smol-toml";
2
3
  export const BUILD_ANALYZER_CANDIDATE_ROOTS = ["apps", "packages", "sites", "docs"];
3
4
  const PACKAGE_MANAGER_ONLY_FILES = new Set([
@@ -7,6 +8,8 @@ const PACKAGE_MANAGER_ONLY_FILES = new Set([
7
8
  "pnpm-lock.yaml",
8
9
  "yarn.lock",
9
10
  "package-lock.json",
11
+ "Gemfile",
12
+ "Gemfile.lock",
10
13
  ]);
11
14
  export function hasWorkspaceSignal(fileNames, packageJsonText) {
12
15
  const names = new Set(fileNames);
@@ -102,6 +105,20 @@ function parseTomlObject(value) {
102
105
  return null;
103
106
  }
104
107
  }
108
+ function parseYamlObject(value) {
109
+ if (!value) {
110
+ return null;
111
+ }
112
+ try {
113
+ const parsed = parseYaml(value);
114
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
115
+ ? parsed
116
+ : null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
105
122
  export function stringField(value, key) {
106
123
  const field = value?.[key];
107
124
  return typeof field === "string" && field.trim() ? field.trim() : null;
@@ -628,9 +645,8 @@ const FRAMEWORKS = [
628
645
  },
629
646
  {
630
647
  preset: "jekyll",
631
- configFiles: ["_config.yml", "_config.yaml"],
648
+ configFiles: ["_config.yml", "_config.yaml", "_config.toml"],
632
649
  outputDirectory: "_site",
633
- defaultBuildCommand: "bundle exec jekyll build",
634
650
  },
635
651
  {
636
652
  preset: "mkdocs",
@@ -675,6 +691,26 @@ function outputDirectoryForFramework(frameworkPreset, files) {
675
691
  return framework.outputDirectory;
676
692
  return files.has("dist") ? "dist" : null;
677
693
  }
694
+ const JEKYLL_BUNDLE_PATH = ".spacefast/cache/jekyll-bundle";
695
+ const JEKYLL_GEM_HOME = ".spacefast/cache/jekyll-gems";
696
+ function jekyllInstallCommand(files) {
697
+ return files.has("Gemfile")
698
+ ? `BUNDLE_PATH=${JEKYLL_BUNDLE_PATH} bundle install`
699
+ : `GEM_HOME=${JEKYLL_GEM_HOME} gem install jekyll --no-document`;
700
+ }
701
+ function jekyllConfigFile(files) {
702
+ return ["_config.yml", "_config.yaml", "_config.toml"].find((file) => files.has(file));
703
+ }
704
+ function jekyllBuildCommand(files) {
705
+ const command = files.has("Gemfile")
706
+ ? `JEKYLL_ENV=production BUNDLE_PATH=${JEKYLL_BUNDLE_PATH} bundle exec jekyll build`
707
+ : `JEKYLL_ENV=production GEM_HOME=${JEKYLL_GEM_HOME} ${JEKYLL_GEM_HOME}/bin/jekyll build`;
708
+ const configFile = jekyllConfigFile(files);
709
+ return configFile ? `${command} --config ${configFile}` : command;
710
+ }
711
+ function frameworkInstallCommand(frameworkPreset, files) {
712
+ return frameworkPreset === "jekyll" ? jekyllInstallCommand(files) : null;
713
+ }
678
714
  function installCommandForManager(packageManager, packageJson) {
679
715
  switch (packageManager) {
680
716
  case "bun":
@@ -687,13 +723,15 @@ function installCommandForManager(packageManager, packageJson) {
687
723
  return packageJson ? "npm ci" : null;
688
724
  }
689
725
  }
690
- function buildCommandForDefaults(packageJson, run, frameworkPreset) {
726
+ function buildCommandForDefaults(packageJson, run, frameworkPreset, files) {
691
727
  if (packageJson?.scripts?.build)
692
728
  return `${run} build`;
729
+ if (frameworkPreset === "jekyll")
730
+ return jekyllBuildCommand(files);
693
731
  return frameworkForPreset(frameworkPreset)?.defaultBuildCommand ?? null;
694
732
  }
695
- function packageDefaults(files, packageJsonText) {
696
- const packageJson = parsePackageJson(packageJsonText);
733
+ function packageDefaults(input, files) {
734
+ const packageJson = parsePackageJson(input.packageJson);
697
735
  const dependencies = {
698
736
  ...packageJson?.dependencies,
699
737
  ...packageJson?.devDependencies,
@@ -701,17 +739,26 @@ function packageDefaults(files, packageJsonText) {
701
739
  const packageManager = packageManagerForFiles(files);
702
740
  const run = packageManager === "npm" ? "npm run" : packageManager === "bun" ? "bun run" : packageManager;
703
741
  const frameworkPreset = detectFrameworkPreset(files, dependencies);
742
+ const installCommands = [
743
+ installCommandForManager(packageManager, packageJson),
744
+ frameworkInstallCommand(frameworkPreset, files),
745
+ ].filter((command) => Boolean(command));
746
+ const jekyllConfig = input.jekyllYaml
747
+ ? parseYamlObject(input.jekyllYaml)
748
+ : parseTomlObject(input.jekyllToml);
704
749
  return {
705
750
  frameworkPreset,
706
- installCommand: installCommandForManager(packageManager, packageJson),
707
- buildCommand: buildCommandForDefaults(packageJson, run, frameworkPreset),
708
- outputDirectory: outputDirectoryForFramework(frameworkPreset, files),
751
+ installCommand: installCommands.length > 0 ? installCommands.join(" && ") : null,
752
+ buildCommand: buildCommandForDefaults(packageJson, run, frameworkPreset, files),
753
+ outputDirectory: frameworkPreset === "jekyll"
754
+ ? (stringField(jekyllConfig, "destination") ?? "_site")
755
+ : outputDirectoryForFramework(frameworkPreset, files),
709
756
  };
710
757
  }
711
758
  export function detectBuildSettings(input) {
712
759
  const files = new Set(input.fileNames);
713
760
  const defaults = {
714
- ...packageDefaults(files, input.packageJson),
761
+ ...packageDefaults(input, files),
715
762
  rootDirectory: input.rootDirectory ?? null,
716
763
  platformPreset: null,
717
764
  detectedFiles: [
@@ -721,6 +768,11 @@ export function detectBuildSettings(input) {
721
768
  files.has("pnpm-lock.yaml") ? "pnpm-lock.yaml" : null,
722
769
  files.has("yarn.lock") ? "yarn.lock" : null,
723
770
  files.has("package-lock.json") ? "package-lock.json" : null,
771
+ files.has("Gemfile") ? "Gemfile" : null,
772
+ files.has("Gemfile.lock") ? "Gemfile.lock" : null,
773
+ files.has("_config.yml") ? "_config.yml" : null,
774
+ files.has("_config.yaml") ? "_config.yaml" : null,
775
+ files.has("_config.toml") ? "_config.toml" : null,
724
776
  ].filter((file) => Boolean(file)),
725
777
  };
726
778
  const vercelFramework = input.vercelJson
@@ -16,7 +16,7 @@ export type PolicyAuthorizeInput = {
16
16
  resource: string | readonly string[];
17
17
  conditions?: RequestCondition;
18
18
  };
19
- export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:create")[];
19
+ export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:trigger" | "builds:create")[];
20
20
  export declare function compileApiKeyPreset(preset: ApiKeyPreset, input: PolicyCompileInput): ApiCredentialPolicy;
21
21
  /** Expand a named API-key preset into the finite permissions sent on the public wire contract. */
22
22
  export declare function apiKeyPresetPermissions(preset: ApiKeyPreset): StatticAction[];
@@ -8,10 +8,12 @@ export const publishActions = [
8
8
  "versions:promote",
9
9
  "versions:download",
10
10
  "builds:read",
11
+ "builds:trigger",
11
12
  "builds:create",
12
13
  ];
13
14
  const spaceAdminActions = [
14
15
  ...publishActions,
16
+ "spaces:rename",
15
17
  "spaces:delete",
16
18
  "spaces:transfer",
17
19
  "spaces:export",
@@ -81,12 +83,14 @@ export function compileTeamRolePolicy(input) {
81
83
  "spaces:read",
82
84
  "spaces:create",
83
85
  "spaces:write",
86
+ "spaces:rename",
84
87
  "spaces:publish",
85
88
  "versions:read",
86
89
  "versions:promote",
87
90
  "versions:download",
88
91
  "domains:read",
89
92
  "builds:read",
93
+ "builds:trigger",
90
94
  "builds:create",
91
95
  "teams:read",
92
96
  "billing:read",
@@ -1,5 +1,5 @@
1
1
  {
2
- "comment": "Shared egress-policy corpus (spec: one proxy-egress-safety invariant). Exercised by BOTH enforcers: apps/control-plane/src/http/egress-guard.ts (control-plane SSRF guard) and runtime/engine/shared/egress.php (runtime proxy egress). Every case must produce the same verdict in TypeScript and PHP; parity is asserted by apps/control-plane/src/runtime/php-policy-parity.test.ts and src/http/egress-guard.test.ts.",
2
+ "comment": "Shared egress-policy corpus (spec: one proxy-egress-safety invariant). Exercised by THREE enforcers: apps/control-plane/src/http/egress-guard.ts (control-plane SSRF guard), runtime/engine/shared/egress.php (runtime proxy egress), and crates/stattic-runtime-core (Rust finalizer policy). Every case must produce the same verdict in TypeScript, PHP, and Rust; parity is asserted by apps/control-plane/src/runtime/php-policy-parity.test.ts, src/http/egress-guard.test.ts, and crates/stattic-runtime-core/src/policy.rs::proxy_destinations_match_the_shared_egress_corpus.",
3
3
  "ips": [
4
4
  { "ip": "127.0.0.1", "public": false, "reason": "loopback" },
5
5
  { "ip": "127.8.9.10", "public": false, "reason": "loopback /8" },
@@ -0,0 +1,53 @@
1
+ import { type Grant, type GrantCapability, type GrantCheckFailure, type GrantSource } from "../contracts/grants.js";
2
+ export type GrantPrincipal = {
3
+ teamIds?: readonly string[];
4
+ personIds?: readonly string[];
5
+ linkIds?: readonly string[];
6
+ passwordCredentialIds?: readonly string[];
7
+ machineIds?: readonly string[];
8
+ externalIdentities?: readonly {
9
+ issuer: string;
10
+ subject: string;
11
+ }[];
12
+ verifiedEmail?: boolean;
13
+ ipAddress?: string;
14
+ country?: string;
15
+ userAgent?: string;
16
+ };
17
+ export type GrantEvaluationTarget = {
18
+ kind: "live";
19
+ } | {
20
+ kind: "version";
21
+ versionId: string;
22
+ } | {
23
+ kind: "branch";
24
+ branch: string;
25
+ };
26
+ export type GrantEvaluationInput = {
27
+ grants: readonly Grant[];
28
+ resource: string;
29
+ resourceKind?: "route" | "artifact";
30
+ target: GrantEvaluationTarget;
31
+ principal?: GrantPrincipal;
32
+ spaceClaimed: boolean;
33
+ now?: Date;
34
+ useCounts?: Readonly<Record<string, number>>;
35
+ acquiredGrantIds?: readonly string[];
36
+ };
37
+ export type GrantMatch = {
38
+ grantId: string;
39
+ capabilities: GrantCapability[];
40
+ source: GrantSource;
41
+ };
42
+ export type GrantEvaluation = {
43
+ capabilities: GrantCapability[];
44
+ matches: GrantMatch[];
45
+ };
46
+ export declare function grantResourcePatternMatches(pattern: string, resource: string, resourceKind?: "route" | "artifact"): boolean;
47
+ export type GrantEvaluationExplanation = {
48
+ grantId: string;
49
+ failures: GrantCheckFailure[];
50
+ };
51
+ export declare function explainGrants(input: GrantEvaluationInput): GrantEvaluationExplanation[];
52
+ export declare function evaluateGrants(input: GrantEvaluationInput): GrantEvaluation;
53
+ export declare function grantAllows(input: GrantEvaluationInput, capability: GrantCapability): boolean;
@@ -0,0 +1,160 @@
1
+ import { normalizeAccessScopePath } from "../contracts/access.js";
2
+ import { GRANT_CAPABILITIES, grantNetworkCidrMatches, normalizeGrantResourcePattern, } from "../contracts/grants.js";
3
+ function audienceMatches(audience, principal) {
4
+ switch (audience.kind) {
5
+ case "public":
6
+ return true;
7
+ case "team":
8
+ return principal.teamIds?.includes(audience.teamId) ?? false;
9
+ case "person":
10
+ return principal.personIds?.includes(audience.personId) ?? false;
11
+ case "link":
12
+ return principal.linkIds?.includes(audience.linkId) ?? false;
13
+ case "password":
14
+ return principal.passwordCredentialIds?.includes(audience.credentialId) ?? false;
15
+ case "machine":
16
+ return principal.machineIds?.includes(audience.machineId) ?? false;
17
+ case "external":
18
+ return (principal.externalIdentities?.some((identity) => identity.issuer === audience.issuer && identity.subject === audience.subject) ?? false);
19
+ }
20
+ throw new Error("Unsupported Grant audience.");
21
+ }
22
+ function targetMatches(selector, target) {
23
+ switch (selector.kind) {
24
+ case "live":
25
+ return target.kind === "live";
26
+ case "version":
27
+ return target.kind === "version" && target.versionId === selector.versionId;
28
+ case "branch":
29
+ return target.kind === "branch" && target.branch === selector.branch;
30
+ case "all_versions":
31
+ return target.kind === "version";
32
+ }
33
+ throw new Error("Unsupported Grant target.");
34
+ }
35
+ function normalizeGrantEvaluationResource(resource, resourceKind) {
36
+ if (resourceKind === "route")
37
+ return normalizeAccessScopePath(resource);
38
+ if (!resource.startsWith("/")) {
39
+ throw new TypeError("artifact resource must be an absolute path");
40
+ }
41
+ const canonical = normalizeGrantResourcePattern(resource);
42
+ if (canonical.includes("*")) {
43
+ throw new TypeError("artifact resource cannot contain wildcards");
44
+ }
45
+ return canonical;
46
+ }
47
+ export function grantResourcePatternMatches(pattern, resource, resourceKind = "route") {
48
+ const canonicalPattern = normalizeGrantResourcePattern(pattern);
49
+ const canonicalResource = normalizeGrantEvaluationResource(resource, resourceKind);
50
+ const patternSegments = canonicalPattern === "/" ? [] : canonicalPattern.slice(1).split("/");
51
+ const resourceSegments = canonicalResource === "/" ? [] : canonicalResource.slice(1).split("/");
52
+ const matches = (patternIndex, resourceIndex) => {
53
+ const patternSegment = patternSegments[patternIndex];
54
+ if (patternSegment === undefined)
55
+ return resourceIndex === resourceSegments.length;
56
+ if (patternSegment === "**") {
57
+ if (patternIndex === patternSegments.length - 1)
58
+ return true;
59
+ for (let next = resourceIndex; next <= resourceSegments.length; next++) {
60
+ if (matches(patternIndex + 1, next))
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ if (resourceIndex >= resourceSegments.length)
66
+ return false;
67
+ if (patternSegment !== "*" && patternSegment !== resourceSegments[resourceIndex])
68
+ return false;
69
+ return matches(patternIndex + 1, resourceIndex + 1);
70
+ };
71
+ return matches(0, 0);
72
+ }
73
+ function grantCheckFailures(grant, input, principal, now) {
74
+ const failures = [];
75
+ if (grant.source.kind === "config" && !input.spaceClaimed) {
76
+ failures.push("space_unclaimed_config");
77
+ }
78
+ if (!audienceMatches(grant.audience, principal))
79
+ failures.push("audience");
80
+ if (!targetMatches(grant.target, input.target))
81
+ failures.push("target");
82
+ const timestamp = now.getTime();
83
+ if (!Number.isFinite(timestamp)) {
84
+ failures.push("invalid_time");
85
+ }
86
+ else {
87
+ const notBefore = grant.constraints.notBefore;
88
+ if (notBefore !== undefined && timestamp < Date.parse(notBefore))
89
+ failures.push("not_before");
90
+ const expiresAt = grant.constraints.expiresAt;
91
+ if (expiresAt !== undefined && timestamp >= Date.parse(expiresAt))
92
+ failures.push("expired");
93
+ }
94
+ const maxUses = grant.constraints.maxUses;
95
+ if (maxUses !== undefined && !input.acquiredGrantIds?.includes(grant.id)) {
96
+ const uses = input.useCounts?.[grant.id];
97
+ if (!Number.isInteger(uses) || (uses ?? 0) < 0 || (uses ?? 0) >= maxUses) {
98
+ failures.push("max_uses");
99
+ }
100
+ }
101
+ if (grant.constraints.requireVerifiedEmail && principal.verifiedEmail !== true) {
102
+ failures.push("verified_email");
103
+ }
104
+ if (grant.constraints.network &&
105
+ ((grant.constraints.network.ipCidrs !== undefined &&
106
+ (!principal.ipAddress ||
107
+ !grant.constraints.network.ipCidrs.some((cidr) => grantNetworkCidrMatches(cidr, principal.ipAddress ?? "")))) ||
108
+ (grant.constraints.network.countries !== undefined &&
109
+ (!principal.country ||
110
+ !grant.constraints.network.countries.includes(principal.country.toUpperCase()))) ||
111
+ (grant.constraints.network.excludedCountries !== undefined &&
112
+ (!principal.country ||
113
+ grant.constraints.network.excludedCountries.includes(principal.country.toUpperCase()))) ||
114
+ (grant.constraints.network.excludedUserAgentSubstrings !== undefined &&
115
+ (!principal.userAgent ||
116
+ grant.constraints.network.excludedUserAgentSubstrings.some((needle) => principal.userAgent?.toLowerCase().includes(needle.toLowerCase())))))) {
117
+ failures.push("network");
118
+ }
119
+ const included = grant.resources.include.some((pattern) => grantResourcePatternMatches(pattern, input.resource, input.resourceKind));
120
+ if (!included) {
121
+ failures.push("resource_not_included");
122
+ }
123
+ else if (grant.resources.exclude.some((pattern) => grantResourcePatternMatches(pattern, input.resource, input.resourceKind))) {
124
+ failures.push("resource_excluded");
125
+ }
126
+ return failures;
127
+ }
128
+ export function explainGrants(input) {
129
+ const principal = input.principal ?? {};
130
+ const now = input.now ?? new Date();
131
+ return input.grants.map((grant) => ({
132
+ grantId: grant.id,
133
+ failures: grantCheckFailures(grant, input, principal, now),
134
+ }));
135
+ }
136
+ export function evaluateGrants(input) {
137
+ const principal = input.principal ?? {};
138
+ const now = input.now ?? new Date();
139
+ const matches = [];
140
+ const capabilities = new Set();
141
+ for (const grant of input.grants) {
142
+ if (grantCheckFailures(grant, input, principal, now).length > 0)
143
+ continue;
144
+ const matchedCapabilities = [...grant.capabilities];
145
+ matches.push({
146
+ grantId: grant.id,
147
+ capabilities: matchedCapabilities,
148
+ source: grant.source,
149
+ });
150
+ for (const capability of matchedCapabilities)
151
+ capabilities.add(capability);
152
+ }
153
+ return {
154
+ capabilities: GRANT_CAPABILITIES.filter((capability) => capabilities.has(capability)),
155
+ matches,
156
+ };
157
+ }
158
+ export function grantAllows(input, capability) {
159
+ return evaluateGrants(input).capabilities.includes(capability);
160
+ }
@@ -131,6 +131,7 @@ export async function findUpward(probe, startDir = process.cwd(), boundary) {
131
131
  current = resolvedBoundary;
132
132
  }
133
133
  for (;;) {
134
+ // oxlint-disable-next-line no-await-in-loop -- each parent probe depends on the child being absent
134
135
  const found = await probe(current);
135
136
  if (found)
136
137
  return found;
@@ -16,17 +16,23 @@ export type PageFileEntry = {
16
16
  export type PagePreviewFile = {
17
17
  name: string;
18
18
  href: string;
19
- downloadHref?: string;
20
19
  size?: string;
21
20
  mime?: string;
22
21
  };
22
+ /**
23
+ * One URL, one representation. A file's bare path always serves its bytes;
24
+ * the preview page is reachable only under this query flag. The alternative —
25
+ * negotiating on `Accept` at the bare path — depends on `Vary`, which the edge
26
+ * ignores, so whichever client arrived first pinned that representation for
27
+ * everyone (an `<img>` could poison the human's link for a year).
28
+ */
29
+ export declare const PAGE_PREVIEW_QUERY = "spacefast_view";
30
+ /** The preview page for a file whose bytes live at `fileHref`. */
31
+ export declare function previewPageHref(fileHref: string): string;
23
32
  export type PageExpansionContext = {
24
33
  page: SitePageId | "layout";
25
34
  theme?: PageTheme;
26
35
  content?: string;
27
- passwordAction?: string;
28
- passwordError?: string;
29
- loginHref?: string;
30
36
  displayPath?: string;
31
37
  displayTitle?: string;
32
38
  itemCount?: number;
@@ -38,7 +44,7 @@ export type PageExpansionContext = {
38
44
  };
39
45
  export type PageDiagnostic = {
40
46
  severity: "warning" | "error";
41
- code: "page_source_too_large" | "unknown_page_template" | "unknown_page_element" | "page_element_not_allowed" | "page_document_required" | "layout_missing_content" | "password_form_missing" | "index_files_missing" | "page_indexable";
47
+ code: "page_source_too_large" | "unknown_page_template" | "unknown_page_element" | "page_element_not_allowed" | "page_document_required" | "layout_missing_content" | "index_files_missing" | "page_indexable";
42
48
  message: string;
43
49
  path: string;
44
50
  };
@@ -3,6 +3,18 @@ import { CUSTOM_PAGE_IDS, PAGE_CATALOG, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SO
3
3
  import { SPACE_CONFIG_ACCEPTED_FILES } from "../contracts/space-config.js";
4
4
  import { derivePageColors, PAGE_COLOR_DEFAULTS } from "./page-colors.js";
5
5
  import { PAGE_EMBEDDED_FONT_FACES, PAGE_FONT_FACES } from "./page-fonts.js";
6
+ /**
7
+ * One URL, one representation. A file's bare path always serves its bytes;
8
+ * the preview page is reachable only under this query flag. The alternative —
9
+ * negotiating on `Accept` at the bare path — depends on `Vary`, which the edge
10
+ * ignores, so whichever client arrived first pinned that representation for
11
+ * everyone (an `<img>` could poison the human's link for a year).
12
+ */
13
+ export const PAGE_PREVIEW_QUERY = "spacefast_view";
14
+ /** The preview page for a file whose bytes live at `fileHref`. */
15
+ export function previewPageHref(fileHref) {
16
+ return `${fileHref}${fileHref.includes("?") ? "&" : "?"}${PAGE_PREVIEW_QUERY}=1`;
17
+ }
6
18
  const encoder = new TextEncoder();
7
19
  const knownElements = new Set(PAGE_ELEMENTS);
8
20
  const customPageIds = new Set(CUSTOM_PAGE_IDS);
@@ -69,16 +81,14 @@ function renderFiles(files) {
69
81
  function renderFile(file) {
70
82
  if (!file)
71
83
  return '<p class="sf-empty">File unavailable.</p>';
72
- const rawHref = `${file.href}${file.href.includes("?") ? "&" : "?"}spacefast_raw=1`;
73
- const href = escapePageHtml(rawHref);
84
+ // This page is itself the `?spacefast_view=1` representation, so the embed,
85
+ // the download and the raw link all point straight at the canonical path.
86
+ const href = escapePageHtml(file.href);
74
87
  const name = escapePageHtml(file.name);
75
88
  const media = file.mime?.startsWith("image/")
76
89
  ? `<img class="sf-preview" src="${href}" alt="">`
77
90
  : `<iframe class="sf-preview" src="${href}" title="Preview of ${name}" loading="lazy" sandbox></iframe>`;
78
- const downloadHref = file.downloadHref
79
- ? `${file.downloadHref}${file.downloadHref.includes("?") ? "&" : "?"}spacefast_raw=1`
80
- : rawHref;
81
- return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${escapePageHtml(downloadHref)}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
91
+ return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${href}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
82
92
  }
83
93
  function renderSpacefastBranding(hidden) {
84
94
  const wordmark = hidden
@@ -90,18 +100,8 @@ function renderElement(element, context) {
90
100
  switch (element) {
91
101
  case "sf-content":
92
102
  return context.content ?? "";
93
- case "sf-password-form": {
94
- const hasError = Boolean(context.passwordError);
95
- const error = hasError
96
- ? `<p class="sf-error" id="sf-password-error" role="alert">${escapePageHtml(context.passwordError ?? "")}</p>`
97
- : "";
98
- const form = `<form class="sf-form" method="post" action="${escapePageHtml(context.passwordAction ?? "")}"><label for="sf-password">Password</label><div class="sf-form-row"><input id="sf-password" name="_pw" type="password" required autocomplete="current-password"${hasError ? ' aria-invalid="true" aria-describedby="sf-password-error"' : ""}><button type="submit">Continue</button></div>${error}</form>`;
99
- return runtimeSlot("challenge", form);
100
- }
101
- case "sf-login":
102
- return runtimeSlot("challenge", `<div class="sf-actions"><a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Continue with Spacefast</a></div>`);
103
103
  case "sf-denial-context":
104
- return runtimeSlot("denial", '<p class="sf-copy">Access to this space is restricted.</p>');
104
+ return runtimeSlot("denial", '<p class="sf-copy">This page is private.</p>');
105
105
  case "sf-page-path":
106
106
  return context.displayPath
107
107
  ? escapePageHtml(context.displayPath)
@@ -192,7 +192,6 @@ export function validatePageSources(input) {
192
192
  element === "sf-name" ||
193
193
  element === "sf-spacefast-branding" ||
194
194
  (id === "404" && element === "sf-page-path") ||
195
- (id === "password" && element === "sf-password-form") ||
196
195
  (id === "denied" && element === "sf-denial-context") ||
197
196
  (id === "index" &&
198
197
  (element === "sf-files" ||
@@ -209,16 +208,6 @@ export function validatePageSources(input) {
209
208
  path,
210
209
  });
211
210
  }
212
- if (id === "password" &&
213
- !/<\s*sf-password-form\b/i.test(source) &&
214
- !/<\s*input\b[^>]*\bname\s*=\s*["']_pw["']/i.test(source)) {
215
- diagnostics.push({
216
- severity: "error",
217
- code: "password_form_missing",
218
- message: "The password page needs <sf-password-form> or a form input named _pw.",
219
- path,
220
- });
221
- }
222
211
  if (id === "index" && !/<\s*sf-files\b/i.test(source))
223
212
  diagnostics.push({
224
213
  severity: "warning",
@@ -226,7 +215,7 @@ export function validatePageSources(input) {
226
215
  message: "The index page does not render <sf-files>.",
227
216
  path,
228
217
  });
229
- if ((id === "password" || id === "denied") &&
218
+ if (id === "denied" &&
230
219
  /<\s*meta\b[^>]*\bname\s*=\s*["']robots["'][^>]*\bcontent\s*=\s*["'][^"']*\bindex\b/i.test(source))
231
220
  diagnostics.push({
232
221
  severity: "warning",
@@ -332,7 +321,7 @@ export function defaultPageBody(id) {
332
321
  // The built-in palette runs through the same readability engine as pushed
333
322
  // themes, so buttons and links remain readable for every accepted accent.
334
323
  const defaultPageColors = derivePageColors({});
335
- const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button,.sf-form button,.sf-login{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover,.sf-form button:hover,.sf-login:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-form{width:100%;max-width:360px;margin-top:22px;text-align:left}.sf-form label,.sf-password label{display:block;margin-bottom:8px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.1em;text-transform:uppercase}.sf-password label{flex-basis:100%}.sf-form-row{display:flex;gap:10px}.sf-password form{display:flex;flex-wrap:wrap;gap:10px}.sf-form input,.sf-pw{min-width:0;flex:1;border:1px solid var(--sf-border);background:transparent;color:var(--sf-fg);padding:9px 11px;font:14px/1.5 var(--sf-font)}.sf-form input:focus-visible,.sf-pw:focus-visible{outline:2px solid var(--sf-link);outline-offset:-2px}.sf-form input[aria-invalid=true],.sf-pw[aria-invalid=true]{border-color:var(--sf-error)}.sf-form button,.sf-password button{margin:0}.sf-error{margin:9px 0 0;color:var(--sf-error);font-size:14px}.sf-password .sf-error{flex-basis:100%;margin:0}.sf-block{width:100%}.sf-login-block{display:flex;justify-content:center;margin-top:20px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:11px}.sf-wordmark-shadow{fill:var(--sf-accent)}.sf-wordmark-plate{fill:#141419}.sf-wordmark-paper{fill:#fbf8f1}@media(max-width:520px){.sf-page{padding:20px}.sf-form-row,.sf-password form{flex-direction:column}.sf-button,.sf-form button,.sf-login{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button,.sf-form button,.sf-login{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
324
+ const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:11px}.sf-wordmark-shadow{fill:var(--sf-accent)}.sf-wordmark-plate{fill:#141419}.sf-wordmark-paper{fill:#fbf8f1}@media(max-width:520px){.sf-page{padding:20px}.sf-button{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
336
325
  const pageHeader = '<header class="sf-header"><a class="sf-brand" href="/" aria-label="Homepage"><span class="sf-brand-mark" aria-hidden="true"></span><sf-logo></sf-logo><strong><sf-name></sf-name></strong></a></header>';
337
326
  const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
338
327
  function document(id, title, body, layout) {
@@ -341,9 +330,7 @@ function document(id, title, body, layout) {
341
330
  }
342
331
  export const DEFAULT_PAGE_TEMPLATES = {
343
332
  "404": document("404", "Page not found", '<p class="sf-eyebrow">404 · Not Found</p><h1>Page not found</h1><p class="sf-copy">There’s nothing at <span class="sf-inline-path"><sf-page-path></sf-page-path></span>.</p><div class="sf-actions"><a class="sf-button" href="/">Go to the homepage</a></div>', "center"),
344
- password: document("password", "Password required", '<h1>This space is protected</h1><p class="sf-sub">Enter the password to continue.</p><sf-password-form></sf-password-form>', "center"),
345
333
  denied: document("denied", "Access denied", "<h1>Access denied</h1><sf-denial-context></sf-denial-context>", "center"),
346
- login: document("login", "Sign in", '<h1>This space is for members</h1><p class="sf-sub">Sign in to continue — you’ll come right back here.</p><sf-login></sf-login>', "center"),
347
334
  index: document("index", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><p class="sf-sub"><sf-page-summary></sf-page-summary></p><sf-files></sf-files>', "plain"),
348
335
  preview: document("preview", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><sf-file></sf-file>', "plain"),
349
336
  };
@@ -46,6 +46,40 @@ export type PublishManifestResult<T extends {
46
46
  files: T[];
47
47
  warnings: PublishManifestWarning[];
48
48
  };
49
+ export type PublishRouteAnalysis = {
50
+ collisions: Array<{
51
+ route: string;
52
+ paths: string[];
53
+ }>;
54
+ caseCollisions: Array<{
55
+ route: string;
56
+ siblingRoute: string;
57
+ paths: string[];
58
+ }>;
59
+ routeOwnership: PublishRouteOwnership[];
60
+ };
61
+ export type PublishRouteOwnership = {
62
+ requestedRoute: string;
63
+ artifactPath: string;
64
+ };
65
+ /**
66
+ * Canonicalizes a route expanded from an already-canonical manifest path.
67
+ * Manifest intake has decoded percent escapes exactly once, so this must not
68
+ * decode again. It applies only the route-identity aliases shared with access.
69
+ */
70
+ export declare function canonicalizePublishRequestedRoute(route: string): string;
71
+ /**
72
+ * Expands artifact paths into the request routes they can serve. The runtime
73
+ * resolves literal files, directory indexes, and clean `.html` aliases; two
74
+ * different artifacts owning one request route is rejected at publish/finalize
75
+ * instead of depending on lookup order. `routeOwnership` exposes the same
76
+ * expansion in canonical access-route form so authorization and diagnostics
77
+ * never have to infer a route from an artifact path.
78
+ */
79
+ export declare function analyzePublishRoutes(paths: readonly string[], options?: {
80
+ index?: string | false;
81
+ cleanUrls?: boolean;
82
+ }): PublishRouteAnalysis;
49
83
  export declare function resolveManifestFileCap(maxFiles?: number | null): number;
50
84
  export declare function validatePublishManifest<T extends {
51
85
  path: string;
@@ -64,7 +98,3 @@ export declare function filterFreePlanManifest<T extends {
64
98
  allowed: T[];
65
99
  ignored: string[];
66
100
  };
67
- export declare function filterFreePlanHeaders(content: string): {
68
- headers: string | undefined;
69
- droppedBasicAuthCount: number;
70
- };