@spacefast/common 0.0.23 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brand-assets/LICENSE-Haskoy.txt +93 -0
- package/brand-assets/haskoy-latin-variable.woff2 +0 -0
- package/brand-assets/spacefast-favicon.svg +4 -4
- package/brand-assets/spacefast-sf-full-bleed.svg +3 -3
- package/brand-assets/spacefast-wordmark.svg +2 -4
- package/dist/agents/connect-targets.js +1 -1
- package/dist/agents/private-key-oauth.d.ts +0 -1
- package/dist/agents/private-key-oauth.js +46 -63
- package/dist/brand-assets-build.js +4 -6
- package/dist/brand-assets.d.ts +5 -15
- package/dist/brand-assets.js +15 -14
- package/dist/config/domains.d.ts +47 -7
- package/dist/config/domains.js +66 -10
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +0 -1
- package/dist/contracts/abuse.d.ts +2 -0
- package/dist/contracts/abuse.js +12 -0
- package/dist/contracts/access.d.ts +124 -12
- package/dist/contracts/access.js +73 -9
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +5 -12
- package/dist/contracts/api-keys.d.ts +21 -21
- package/dist/contracts/api-keys.js +2 -2
- package/dist/contracts/beta.js +1 -1
- package/dist/contracts/billing.d.ts +3 -35
- package/dist/contracts/billing.js +33 -52
- package/dist/contracts/builds.d.ts +30 -18
- package/dist/contracts/builds.js +28 -9
- package/dist/contracts/channels.js +1 -1
- package/dist/contracts/cli.d.ts +10 -0
- package/dist/contracts/cli.js +10 -0
- package/dist/contracts/collab-public.d.ts +63 -0
- package/dist/contracts/collab-public.js +46 -0
- package/dist/contracts/comments.d.ts +282 -53
- package/dist/contracts/comments.js +173 -52
- package/dist/contracts/common.d.ts +3 -3
- package/dist/contracts/common.js +5 -5
- package/dist/contracts/crons.d.ts +35 -0
- package/dist/contracts/crons.js +42 -0
- package/dist/contracts/device-auth.d.ts +6 -6
- package/dist/contracts/device-auth.js +2 -2
- package/dist/contracts/docs.js +2 -2
- package/dist/contracts/domains.d.ts +38 -27
- package/dist/contracts/domains.js +29 -2
- package/dist/contracts/enums.d.ts +34 -30
- package/dist/contracts/enums.js +62 -48
- package/dist/contracts/error-code-meta.d.ts +50 -125
- package/dist/contracts/error-code-meta.js +29 -41
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +14 -33
- package/dist/contracts/execution.d.ts +47 -66
- package/dist/contracts/execution.js +53 -49
- package/dist/contracts/feature-lifecycle-core.d.ts +0 -6
- package/dist/contracts/feature-lifecycle-core.js +2 -31
- package/dist/contracts/feature-lifecycle.d.ts +12 -52
- package/dist/contracts/feature-lifecycle.js +16 -59
- package/dist/contracts/features.d.ts +48 -60
- package/dist/contracts/features.js +59 -51
- package/dist/contracts/functions.d.ts +266 -72
- package/dist/contracts/functions.js +260 -37
- package/dist/contracts/git.d.ts +28 -34
- package/dist/contracts/git.js +10 -11
- package/dist/contracts/grants.d.ts +9 -5
- package/dist/contracts/grants.js +18 -9
- package/dist/contracts/ids.js +2 -2
- package/dist/contracts/internal.d.ts +8 -6
- package/dist/contracts/internal.js +12 -12
- package/dist/contracts/mcp.d.ts +11 -0
- package/dist/contracts/mcp.js +5 -0
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/oauth-resources.d.ts +4 -3
- package/dist/contracts/oauth-resources.js +12 -3
- package/dist/contracts/operations.d.ts +90 -5
- package/dist/contracts/operations.js +77 -3
- package/dist/contracts/pages.d.ts +12 -7
- package/dist/contracts/pages.js +24 -1
- package/dist/contracts/plan-policy.d.ts +0 -2
- package/dist/contracts/plan-policy.js +2 -3
- package/dist/contracts/platform.d.ts +61 -148
- package/dist/contracts/platform.js +70 -126
- package/dist/contracts/publish-archive.d.ts +508 -0
- package/dist/contracts/publish-archive.js +32 -0
- package/dist/contracts/push-new.d.ts +4 -4
- package/dist/contracts/quotas.js +2 -2
- package/dist/contracts/repository-connections.d.ts +43 -30
- package/dist/contracts/repository-connections.js +11 -3
- package/dist/contracts/resources.d.ts +12 -24
- package/dist/contracts/resources.js +36 -16
- package/dist/contracts/runtime-api.d.ts +433 -192
- package/dist/contracts/runtime-api.js +421 -356
- package/dist/contracts/runtime-app.d.ts +127 -120
- package/dist/contracts/runtime-app.js +19 -10
- package/dist/contracts/runtime-db.d.ts +5 -5
- package/dist/contracts/runtime-db.js +2 -2
- package/dist/contracts/runtime-purge.d.ts +8 -0
- package/dist/contracts/runtime-purge.js +7 -0
- package/dist/contracts/runtime-services.d.ts +219 -0
- package/dist/contracts/runtime-services.js +437 -0
- package/dist/contracts/runtime-storage.d.ts +46 -32
- package/dist/contracts/runtime-storage.js +31 -8
- package/dist/contracts/sf-config-v1.d.ts +48 -10
- package/dist/contracts/space-config.d.ts +69 -18
- package/dist/contracts/space-config.js +226 -27
- package/dist/contracts/spaces.d.ts +236 -450
- package/dist/contracts/spaces.js +131 -151
- package/dist/contracts/superadmin-activity.d.ts +57 -4
- package/dist/contracts/superadmin-activity.js +48 -2
- package/dist/contracts/superadmin-search.d.ts +126 -0
- package/dist/contracts/superadmin-search.js +61 -0
- package/dist/contracts/superadmin-spaces.d.ts +361 -92
- package/dist/contracts/superadmin-spaces.js +97 -24
- package/dist/contracts/superadmin-teams.d.ts +28 -10
- package/dist/contracts/superadmin-teams.js +5 -8
- package/dist/contracts/superadmin-tenants.d.ts +0 -6
- package/dist/contracts/superadmin-tenants.js +1 -7
- package/dist/contracts/superadmin.d.ts +135 -54
- package/dist/contracts/superadmin.js +46 -23
- package/dist/contracts/tags.d.ts +12 -12
- package/dist/contracts/tags.js +3 -3
- package/dist/contracts/teams.d.ts +49 -14
- package/dist/contracts/teams.js +40 -22
- package/dist/contracts/transfers.d.ts +0 -49
- package/dist/contracts/transfers.js +4 -8
- package/dist/contracts/usage.d.ts +61 -0
- package/dist/contracts/usage.js +64 -0
- package/dist/contracts/variables.d.ts +2 -7
- package/dist/contracts/variables.js +9 -8
- package/dist/contracts/webhooks.d.ts +2 -2
- package/dist/contracts/zero.d.ts +102 -73
- package/dist/contracts/zero.js +94 -32
- package/dist/dashboard-paths/index.d.ts +1 -1
- package/dist/dashboard-paths/index.js +1 -0
- package/dist/docs/agent-prose.d.ts +2 -2
- package/dist/docs/agent-prose.js +63 -70
- package/dist/docs/agent-setup.js +45 -15
- package/dist/docs/catalog.d.ts +24 -24
- package/dist/docs/catalog.js +35 -24
- package/dist/docs/error-docs.js +194 -270
- package/dist/docs/skills.d.ts +12 -12
- package/dist/docs/skills.js +4 -4
- package/dist/test-helpers/fetch-stub.d.ts +16 -0
- package/dist/test-helpers/fetch-stub.js +25 -0
- package/dist/utils/build-settings.d.ts +5 -1
- package/dist/utils/build-settings.js +91 -13
- package/dist/utils/canonical-json.d.ts +10 -0
- package/dist/utils/canonical-json.js +22 -0
- package/dist/utils/cast-transport.d.ts +28 -0
- package/dist/utils/cast-transport.js +52 -0
- package/dist/utils/claim-token.d.ts +0 -8
- package/dist/utils/claim-token.js +3 -15
- package/dist/utils/concurrency.js +1 -0
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -3
- package/dist/utils/dns-instructions.js +9 -9
- package/dist/utils/env-file.d.ts +17 -0
- package/dist/utils/env-file.js +96 -0
- package/dist/utils/error-code.d.ts +2 -0
- package/dist/utils/error-code.js +8 -0
- package/dist/utils/generate-space-name.d.ts +8 -10
- package/dist/utils/generate-space-name.js +206 -21
- package/dist/utils/grant-decision.fixtures.json +2938 -0
- package/dist/utils/grants.d.ts +2 -0
- package/dist/utils/grants.js +26 -1
- package/dist/utils/gravatar.d.ts +9 -0
- package/dist/utils/gravatar.js +31 -0
- package/dist/utils/idempotency.d.ts +6 -0
- package/dist/utils/idempotency.js +28 -0
- package/dist/utils/local-space-state.d.ts +9 -16
- package/dist/utils/local-space-state.js +20 -50
- package/dist/utils/one-shot-replay.d.ts +4 -1
- package/dist/utils/page-colors.d.ts +14 -12
- package/dist/utils/page-colors.js +15 -13
- package/dist/utils/page-fonts.d.ts +2 -2
- package/dist/utils/page-fonts.js +15 -4
- package/dist/utils/pages.d.ts +21 -3
- package/dist/utils/pages.js +42 -13
- package/dist/utils/publish-form-data.d.ts +12 -0
- package/dist/utils/publish-form-data.js +10 -0
- package/dist/utils/publish-policy.d.ts +27 -0
- package/dist/utils/publish-policy.fixtures.json +403 -0
- package/dist/utils/publish-policy.js +45 -20
- package/dist/utils/push-new.d.ts +7 -0
- package/dist/utils/push-new.js +7 -0
- package/dist/utils/query-keys.d.ts +4 -5
- package/dist/utils/query-keys.js +2 -3
- package/dist/utils/runtime-paths.d.ts +5 -0
- package/dist/utils/runtime-paths.js +5 -0
- package/dist/utils/runtime-upload-batch.d.ts +0 -5
- package/dist/utils/runtime-upload-batch.js +1 -27
- package/dist/utils/runtime-upload.d.ts +6 -11
- package/dist/utils/runtime-upload.js +10 -13
- package/dist/utils/secure-local-file.js +5 -16
- package/dist/utils/space-config.d.ts +1 -34
- package/dist/utils/space-config.js +8 -326
- package/dist/utils/static-runtime-policy.d.ts +9 -3
- package/dist/utils/static-runtime-policy.fixtures.json +38 -41
- package/dist/utils/static-runtime-policy.js +22 -56
- package/dist/utils/storage-policy.d.ts +1 -1
- package/dist/utils/storage-policy.js +3 -1
- package/dist/utils/structured-error.d.ts +14 -0
- package/dist/utils/structured-error.js +22 -0
- package/dist/utils/upload-session.d.ts +4 -1
- package/dist/utils/upload-session.js +33 -5
- package/dist/utils/version-path.js +13 -4
- package/dist/vocabulary.d.ts +13 -11
- package/dist/vocabulary.js +42 -28
- package/package.json +7 -17
- package/dist/config/postgres-budget.d.ts +0 -129
- package/dist/config/postgres-budget.js +0 -140
- package/dist/config/postgres-budget.json +0 -51
- package/dist/contracts/archives.d.ts +0 -331
- package/dist/contracts/archives.js +0 -120
- package/dist/contracts/deployments.d.ts +0 -574
- package/dist/contracts/deployments.js +0 -97
- package/dist/contracts/sites.d.ts +0 -416
- package/dist/contracts/sites.js +0 -168
- package/dist/contracts/superadmin-queues.d.ts +0 -414
- package/dist/contracts/superadmin-queues.js +0 -202
- package/dist/contracts/superadmin-runtime.d.ts +0 -27
- package/dist/contracts/superadmin-runtime.js +0 -38
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/utils/api.d.ts +0 -4
- package/dist/utils/api.js +0 -13
package/dist/utils/grants.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export type GrantEvaluation = {
|
|
|
44
44
|
matches: GrantMatch[];
|
|
45
45
|
};
|
|
46
46
|
export declare function grantResourcePatternMatches(pattern: string, resource: string, resourceKind?: "route" | "artifact"): boolean;
|
|
47
|
+
/** Does a Grant's resource set reach this path? Include, minus exclude. */
|
|
48
|
+
export declare function grantResourceSetMatches(resources: Grant["resources"], resource: string, resourceKind?: "route" | "artifact"): boolean;
|
|
47
49
|
export type GrantEvaluationExplanation = {
|
|
48
50
|
grantId: string;
|
|
49
51
|
failures: GrantCheckFailure[];
|
package/dist/utils/grants.js
CHANGED
|
@@ -27,6 +27,8 @@ function targetMatches(selector, target) {
|
|
|
27
27
|
return target.kind === "version" && target.versionId === selector.versionId;
|
|
28
28
|
case "branch":
|
|
29
29
|
return target.kind === "branch" && target.branch === selector.branch;
|
|
30
|
+
// Immutable Version hosts only. The live host is its own target: sharing
|
|
31
|
+
// "every Version" does not thereby publish the live site.
|
|
30
32
|
case "all_versions":
|
|
31
33
|
return target.kind === "version";
|
|
32
34
|
}
|
|
@@ -70,6 +72,29 @@ export function grantResourcePatternMatches(pattern, resource, resourceKind = "r
|
|
|
70
72
|
};
|
|
71
73
|
return matches(0, 0);
|
|
72
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Exclude patterns read wider than include patterns, and that asymmetry is the
|
|
77
|
+
* rule, not an accident: the route lane collapses a terminal `index.html`
|
|
78
|
+
* (normalizeAccessScopePath) while the artifact lane keeps it, so an exclude
|
|
79
|
+
* naming an index artifact must also close the route that artifact serves.
|
|
80
|
+
* Include never gets the same courtesy — granting the stripped route would hand
|
|
81
|
+
* out access nobody wrote down. The serving engine compiles the identical pair
|
|
82
|
+
* (access-rules.php _stattic_grant_exclude_pattern_variants).
|
|
83
|
+
*/
|
|
84
|
+
function grantExcludePatternMatches(pattern, resource, resourceKind) {
|
|
85
|
+
if (grantResourcePatternMatches(pattern, resource, resourceKind))
|
|
86
|
+
return true;
|
|
87
|
+
const canonical = normalizeGrantResourcePattern(pattern);
|
|
88
|
+
if (!canonical.endsWith("/index.html"))
|
|
89
|
+
return false;
|
|
90
|
+
const stripped = canonical.slice(0, -"/index.html".length);
|
|
91
|
+
return grantResourcePatternMatches(stripped === "" ? "/" : stripped, resource, resourceKind);
|
|
92
|
+
}
|
|
93
|
+
/** Does a Grant's resource set reach this path? Include, minus exclude. */
|
|
94
|
+
export function grantResourceSetMatches(resources, resource, resourceKind = "route") {
|
|
95
|
+
return (resources.include.some((pattern) => grantResourcePatternMatches(pattern, resource, resourceKind)) &&
|
|
96
|
+
!resources.exclude.some((pattern) => grantExcludePatternMatches(pattern, resource, resourceKind)));
|
|
97
|
+
}
|
|
73
98
|
function grantCheckFailures(grant, input, principal, now) {
|
|
74
99
|
const failures = [];
|
|
75
100
|
if (grant.source.kind === "config" && !input.spaceClaimed) {
|
|
@@ -120,7 +145,7 @@ function grantCheckFailures(grant, input, principal, now) {
|
|
|
120
145
|
if (!included) {
|
|
121
146
|
failures.push("resource_not_included");
|
|
122
147
|
}
|
|
123
|
-
else if (grant.resources.exclude.some((pattern) =>
|
|
148
|
+
else if (grant.resources.exclude.some((pattern) => grantExcludePatternMatches(pattern, input.resource, input.resourceKind ?? "route"))) {
|
|
124
149
|
failures.push("resource_excluded");
|
|
125
150
|
}
|
|
126
151
|
return failures;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Gravatar's canonical address form: trimmed and lowercased. */
|
|
2
|
+
export declare function normalizeGravatarEmail(email: string | null | undefined): string | null;
|
|
3
|
+
/** The canonical avatar URL for an already-computed SHA-256 hash. */
|
|
4
|
+
export declare function gravatarUrlFromHash(hashHex: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* SHA-256 hex of a normalized email — Gravatar's identifier. Uses WebCrypto so
|
|
7
|
+
* a browser can derive it locally and never send the raw address anywhere.
|
|
8
|
+
*/
|
|
9
|
+
export declare function gravatarEmailHash(email: string | null | undefined): Promise<string | null>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one Gravatar URL shape Spacefast emits. Every surface — control-plane
|
|
3
|
+
* profiles, the Comments SDK, Slack notifications — derives the same address
|
|
4
|
+
* for the same person, so an avatar is recognizable wherever it appears and
|
|
5
|
+
* Cast can validate a guest avatar against a single pattern.
|
|
6
|
+
*
|
|
7
|
+
* `d=404` is deliberate: a person without a Gravatar gets no image at all, and
|
|
8
|
+
* the consumer renders its own initials/palette fallback instead of a stock
|
|
9
|
+
* silhouette. `s=160` covers the largest avatar we render, retina included.
|
|
10
|
+
*/
|
|
11
|
+
const GRAVATAR_AVATAR_QUERY = "d=404&r=g&s=160";
|
|
12
|
+
/** Gravatar's canonical address form: trimmed and lowercased. */
|
|
13
|
+
export function normalizeGravatarEmail(email) {
|
|
14
|
+
const normalized = email?.trim().toLowerCase();
|
|
15
|
+
return normalized ? normalized : null;
|
|
16
|
+
}
|
|
17
|
+
/** The canonical avatar URL for an already-computed SHA-256 hash. */
|
|
18
|
+
export function gravatarUrlFromHash(hashHex) {
|
|
19
|
+
return `https://gravatar.com/avatar/${hashHex}?${GRAVATAR_AVATAR_QUERY}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* SHA-256 hex of a normalized email — Gravatar's identifier. Uses WebCrypto so
|
|
23
|
+
* a browser can derive it locally and never send the raw address anywhere.
|
|
24
|
+
*/
|
|
25
|
+
export async function gravatarEmailHash(email) {
|
|
26
|
+
const normalized = normalizeGravatarEmail(email);
|
|
27
|
+
if (!normalized || !globalThis.crypto?.subtle)
|
|
28
|
+
return null;
|
|
29
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(normalized));
|
|
30
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, secret-safe key for one exact logical mutation attempt. Shared by
|
|
3
|
+
* every client surface: the same publish issued through the CLI and through
|
|
4
|
+
* MCP must land on one key or the API stops deduplicating the retry.
|
|
5
|
+
*/
|
|
6
|
+
export declare function stableIdempotencyKey(prefix: string, parts: unknown): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Key-sorted JSON that treats an absent key and an `undefined` value as the
|
|
4
|
+
* same thing. Deliberately *not* `canonicalJson`: a key the CLI omits and one
|
|
5
|
+
* the MCP server sets to `undefined` describe the same mutation, so they have
|
|
6
|
+
* to hash alike. Merging the two spellings would change every minted key.
|
|
7
|
+
*/
|
|
8
|
+
function keyJson(value) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return `[${value.map(keyJson).join(",")}]`;
|
|
11
|
+
}
|
|
12
|
+
if (value && typeof value === "object") {
|
|
13
|
+
return `{${Object.entries(value)
|
|
14
|
+
.filter(([, entry]) => entry !== undefined)
|
|
15
|
+
.toSorted(([left], [right]) => left.localeCompare(right))
|
|
16
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${keyJson(entry)}`)
|
|
17
|
+
.join(",")}}`;
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify(value ?? null);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Stable, secret-safe key for one exact logical mutation attempt. Shared by
|
|
23
|
+
* every client surface: the same publish issued through the CLI and through
|
|
24
|
+
* MCP must land on one key or the API stops deduplicating the retry.
|
|
25
|
+
*/
|
|
26
|
+
export function stableIdempotencyKey(prefix, parts) {
|
|
27
|
+
return `${prefix}-${createHash("sha256").update(keyJson(parts)).digest("hex")}`;
|
|
28
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const LOCAL_SPACE_STATE_DIR_NAME = ".spacefast";
|
|
3
|
-
export declare const LEGACY_LOCAL_SPACE_STATE_DIR_NAME = ".stattic";
|
|
4
3
|
export declare const LOCAL_SPACE_LINK_FILE_NAME = "space.json";
|
|
5
4
|
export declare const LOCAL_SPACE_STATE_FILE_NAME = "state.json";
|
|
6
5
|
/**
|
|
@@ -23,6 +22,12 @@ export declare const localSpaceLinkSchema: z.ZodObject<{
|
|
|
23
22
|
apiBaseUrl: z.ZodOptional<z.ZodString>;
|
|
24
23
|
}, z.core.$loose>;
|
|
25
24
|
export declare const LOCAL_SPACE_STATE_SCHEMA_VERSION = 1;
|
|
25
|
+
export declare const creationAttemptSchema: z.ZodObject<{
|
|
26
|
+
key: z.ZodString;
|
|
27
|
+
principal: z.ZodString;
|
|
28
|
+
kind: z.ZodLiteral<"authenticated">;
|
|
29
|
+
createdAt: z.ZodString;
|
|
30
|
+
}, z.core.$strict>;
|
|
26
31
|
export declare const localSpaceStateSchema: z.ZodObject<{
|
|
27
32
|
schemaVersion: z.ZodLiteral<1>;
|
|
28
33
|
spaceId: z.ZodOptional<z.ZodString>;
|
|
@@ -42,22 +47,14 @@ export declare const localSpaceStateSchema: z.ZodObject<{
|
|
|
42
47
|
pendingPublish: z.ZodOptional<z.ZodObject<{
|
|
43
48
|
key: z.ZodString;
|
|
44
49
|
principal: z.ZodString;
|
|
45
|
-
kind: z.
|
|
46
|
-
anonymous: "anonymous";
|
|
47
|
-
authenticated: "authenticated";
|
|
48
|
-
}>;
|
|
49
|
-
intent: z.ZodOptional<z.ZodString>;
|
|
50
|
+
kind: z.ZodLiteral<"authenticated">;
|
|
50
51
|
createdAt: z.ZodString;
|
|
51
52
|
}, z.core.$strict>>;
|
|
52
53
|
/** Settled creation receipt identity retained for invocations that began before its commit. */
|
|
53
54
|
creationReplay: z.ZodOptional<z.ZodObject<{
|
|
54
55
|
key: z.ZodString;
|
|
55
56
|
principal: z.ZodString;
|
|
56
|
-
kind: z.
|
|
57
|
-
anonymous: "anonymous";
|
|
58
|
-
authenticated: "authenticated";
|
|
59
|
-
}>;
|
|
60
|
-
intent: z.ZodOptional<z.ZodString>;
|
|
57
|
+
kind: z.ZodLiteral<"authenticated">;
|
|
61
58
|
createdAt: z.ZodString;
|
|
62
59
|
}, z.core.$strict>>;
|
|
63
60
|
/** Random replay capability reserved before consuming a claim credential. */
|
|
@@ -127,10 +124,7 @@ export declare function localSpaceStateDir(root: string): string;
|
|
|
127
124
|
export declare function localSpaceStatePath(root: string): string;
|
|
128
125
|
export declare function localSpaceLinkPath(root: string): string;
|
|
129
126
|
export declare function readLocalSpaceLink(root: string): Promise<LocalSpaceLink | null>;
|
|
130
|
-
/**
|
|
131
|
-
* Like readLocalSpaceLink, but reports WHICH file matched — callers surface the
|
|
132
|
-
* legacy `.stattic/space.json` path to users so migrations stay visible.
|
|
133
|
-
*/
|
|
127
|
+
/** Like readLocalSpaceLink, but reports the file path alongside the link. */
|
|
134
128
|
export declare function readLocalSpaceLinkWithPath(root: string): Promise<{
|
|
135
129
|
link: LocalSpaceLink;
|
|
136
130
|
path: string;
|
|
@@ -151,7 +145,6 @@ export declare function mutateLocalSpaceState(root: string, mutate: (current: Lo
|
|
|
151
145
|
/** Fault-injection observer used to synchronize concurrency tests. */
|
|
152
146
|
onConflict?: (() => void | Promise<void>) | undefined;
|
|
153
147
|
}): Promise<LocalSpaceStateFile>;
|
|
154
|
-
export declare function writeLocalSpaceState(root: string, state: LocalSpaceStateWrite): Promise<string>;
|
|
155
148
|
/** Reserve random replay custody before a claim token can be consumed. */
|
|
156
149
|
export declare function reserveLocalCredentialReplay(root: string, input: {
|
|
157
150
|
operation: LocalCredentialReplayOperation;
|
|
@@ -5,7 +5,6 @@ import { z } from "zod";
|
|
|
5
5
|
import { createOneShotReplayIdentity } from "./one-shot-replay.js";
|
|
6
6
|
import { SecureLocalFileConflictError, secureReadFile, secureWriteFile, } from "./secure-local-file.js";
|
|
7
7
|
export const LOCAL_SPACE_STATE_DIR_NAME = ".spacefast";
|
|
8
|
-
export const LEGACY_LOCAL_SPACE_STATE_DIR_NAME = ".stattic";
|
|
9
8
|
export const LOCAL_SPACE_LINK_FILE_NAME = "space.json";
|
|
10
9
|
export const LOCAL_SPACE_STATE_FILE_NAME = "state.json";
|
|
11
10
|
/**
|
|
@@ -30,6 +29,17 @@ export const localSpaceLinkSchema = z
|
|
|
30
29
|
})
|
|
31
30
|
.loose();
|
|
32
31
|
export const LOCAL_SPACE_STATE_SCHEMA_VERSION = 1;
|
|
32
|
+
// One reservation, two slots: it is written as `pendingPublish` and moved to
|
|
33
|
+
// `creationReplay` once its receipt commits. Space creation is the only
|
|
34
|
+
// mutation that reserves one — credential-free publishes are not idempotent.
|
|
35
|
+
export const creationAttemptSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
key: z.string().min(1),
|
|
38
|
+
principal: z.string().regex(/^[0-9a-f]{64}$/),
|
|
39
|
+
kind: z.literal("authenticated"),
|
|
40
|
+
createdAt: z.string().datetime(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
33
43
|
export const localSpaceStateSchema = z.strictObject({
|
|
34
44
|
schemaVersion: z.literal(LOCAL_SPACE_STATE_SCHEMA_VERSION),
|
|
35
45
|
spaceId: z.string().min(1).optional(),
|
|
@@ -46,27 +56,9 @@ export const localSpaceStateSchema = z.strictObject({
|
|
|
46
56
|
/** Secret per-checkout entropy used only to namespace replay-safe first publishes. */
|
|
47
57
|
anonymousPublishNonce: z.string().min(32).optional(),
|
|
48
58
|
/** Unsettled first-publish mutation. Retries must reuse this exact key. */
|
|
49
|
-
pendingPublish:
|
|
50
|
-
.object({
|
|
51
|
-
key: z.string().min(1),
|
|
52
|
-
principal: z.string().regex(/^[0-9a-f]{64}$/),
|
|
53
|
-
kind: z.enum(["anonymous", "authenticated"]),
|
|
54
|
-
intent: z.string().min(1).optional(),
|
|
55
|
-
createdAt: z.string().datetime(),
|
|
56
|
-
})
|
|
57
|
-
.strict()
|
|
58
|
-
.optional(),
|
|
59
|
+
pendingPublish: creationAttemptSchema.optional(),
|
|
59
60
|
/** Settled creation receipt identity retained for invocations that began before its commit. */
|
|
60
|
-
creationReplay:
|
|
61
|
-
.object({
|
|
62
|
-
key: z.string().min(1),
|
|
63
|
-
principal: z.string().regex(/^[0-9a-f]{64}$/),
|
|
64
|
-
kind: z.enum(["anonymous", "authenticated"]),
|
|
65
|
-
intent: z.string().min(1).optional(),
|
|
66
|
-
createdAt: z.string().datetime(),
|
|
67
|
-
})
|
|
68
|
-
.strict()
|
|
69
|
-
.optional(),
|
|
61
|
+
creationReplay: creationAttemptSchema.optional(),
|
|
70
62
|
/** Random replay capability reserved before consuming a claim credential. */
|
|
71
63
|
pendingCredentialReplay: z
|
|
72
64
|
.object({
|
|
@@ -99,9 +91,6 @@ export function localSpaceStatePath(root) {
|
|
|
99
91
|
export function localSpaceLinkPath(root) {
|
|
100
92
|
return path.join(localSpaceStateDir(root), LOCAL_SPACE_LINK_FILE_NAME);
|
|
101
93
|
}
|
|
102
|
-
function legacyLinkPath(root) {
|
|
103
|
-
return path.join(root, LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME);
|
|
104
|
-
}
|
|
105
94
|
async function readJsonFileWithRaw(root, relativePath) {
|
|
106
95
|
const raw = await secureReadFile({ root, relativePath, encoding: "utf8" }).catch((error) => {
|
|
107
96
|
if (error.code === "ENOENT")
|
|
@@ -156,28 +145,17 @@ function linkSpaceId(link) {
|
|
|
156
145
|
export async function readLocalSpaceLink(root) {
|
|
157
146
|
return (await readLocalSpaceLinkWithPath(root))?.link ?? null;
|
|
158
147
|
}
|
|
159
|
-
/**
|
|
160
|
-
* Like readLocalSpaceLink, but reports WHICH file matched — callers surface the
|
|
161
|
-
* legacy `.stattic/space.json` path to users so migrations stay visible.
|
|
162
|
-
*/
|
|
148
|
+
/** Like readLocalSpaceLink, but reports the file path alongside the link. */
|
|
163
149
|
export async function readLocalSpaceLinkWithPath(root) {
|
|
164
150
|
const primary = parseLink(await readJsonFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
|
|
165
|
-
|
|
166
|
-
return { link: primary, path: localSpaceLinkPath(root) };
|
|
167
|
-
}
|
|
168
|
-
const legacy = parseLink(await readJsonFile(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
|
|
169
|
-
return legacy ? { link: legacy, path: legacyLinkPath(root) } : null;
|
|
151
|
+
return primary ? { link: primary, path: localSpaceLinkPath(root) } : null;
|
|
170
152
|
}
|
|
171
153
|
export async function readLocalSpaceStateAt(root) {
|
|
172
154
|
const statePath = localSpaceStatePath(root);
|
|
173
155
|
const primaryState = await readStateFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_STATE_FILE_NAME));
|
|
174
|
-
const
|
|
175
|
-
? undefined
|
|
176
|
-
: await readStateFile(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_STATE_FILE_NAME));
|
|
177
|
-
const state = primaryState?.state ?? legacyState?.state ?? null;
|
|
156
|
+
const state = primaryState?.state ?? null;
|
|
178
157
|
const linkPath = localSpaceLinkPath(root);
|
|
179
|
-
const link = parseLink(await readJsonFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)))
|
|
180
|
-
parseLink(await readJsonFile(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
|
|
158
|
+
const link = parseLink(await readJsonFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
|
|
181
159
|
if (!state && !link)
|
|
182
160
|
return null;
|
|
183
161
|
const committedSpaceId = linkSpaceId(link);
|
|
@@ -334,11 +312,7 @@ export async function mutateLocalSpaceState(root, mutate, options = {}) {
|
|
|
334
312
|
/* eslint-disable no-await-in-loop -- compare-and-swap retries are intentionally serialized against the latest committed state. */
|
|
335
313
|
for (let attempt = 1; attempt <= LOCAL_STATE_MUTATION_ATTEMPTS; attempt += 1) {
|
|
336
314
|
try {
|
|
337
|
-
const
|
|
338
|
-
const legacyState = primaryState
|
|
339
|
-
? undefined
|
|
340
|
-
: await readJsonFileWithRaw(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_STATE_FILE_NAME));
|
|
341
|
-
const source = primaryState ?? legacyState;
|
|
315
|
+
const source = await readJsonFileWithRaw(root, stateRelativePath);
|
|
342
316
|
const parsed = source ? localSpaceStateSchema.safeParse(source.value) : null;
|
|
343
317
|
if (source && !parsed?.success) {
|
|
344
318
|
throw new Error(`Local Spacefast state at ${localSpaceStatePath(root)} is invalid.`);
|
|
@@ -353,14 +327,14 @@ export async function mutateLocalSpaceState(root, mutate, options = {}) {
|
|
|
353
327
|
schemaVersion: LOCAL_SPACE_STATE_SCHEMA_VERSION,
|
|
354
328
|
});
|
|
355
329
|
const serialized = `${JSON.stringify(payload, null, 2)}\n`;
|
|
356
|
-
if (
|
|
330
|
+
if (source?.raw === serialized)
|
|
357
331
|
return payload;
|
|
358
332
|
await writeJsonAtomic({
|
|
359
333
|
root,
|
|
360
334
|
relativePath: stateRelativePath,
|
|
361
335
|
value: payload,
|
|
362
336
|
mode: 0o600,
|
|
363
|
-
expectedContent:
|
|
337
|
+
expectedContent: source?.raw ?? null,
|
|
364
338
|
...(options.beforeCommit ? { beforeCommit: options.beforeCommit } : {}),
|
|
365
339
|
...(options.afterBackup ? { afterBackup: options.afterBackup } : {}),
|
|
366
340
|
});
|
|
@@ -379,10 +353,6 @@ export async function mutateLocalSpaceState(root, mutate, options = {}) {
|
|
|
379
353
|
/* eslint-enable no-await-in-loop */
|
|
380
354
|
throw new LocalSpaceStateConflictError("Local Spacefast state stayed busy for too long.");
|
|
381
355
|
}
|
|
382
|
-
export async function writeLocalSpaceState(root, state) {
|
|
383
|
-
await mutateLocalSpaceState(root, () => state);
|
|
384
|
-
return localSpaceStatePath(root);
|
|
385
|
-
}
|
|
386
356
|
function credentialFingerprint(credential) {
|
|
387
357
|
return createHash("sha256").update(credential).digest("hex");
|
|
388
358
|
}
|
|
@@ -11,4 +11,7 @@ export type OneShotReplayIdentity = {
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function createOneShotReplayIdentity(prefix: string): OneShotReplayIdentity;
|
|
13
13
|
/** Production headers for one one-shot attempt (or one device-poll sequence). */
|
|
14
|
-
export declare function oneShotReplayHeaders(identity: OneShotReplayIdentity, sequence?: number):
|
|
14
|
+
export declare function oneShotReplayHeaders(identity: OneShotReplayIdentity, sequence?: number): {
|
|
15
|
+
"idempotency-key": string;
|
|
16
|
+
"x-spacefast-idempotency-principal": string;
|
|
17
|
+
};
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { SpacePageTheme } from "../contracts/space-config.js";
|
|
2
|
-
/** Default page palette (mirrored into the shared template CSS)
|
|
2
|
+
/** Default page palette (mirrored into the shared template CSS) — the
|
|
3
|
+
* printed-matter scheme: warm canvas, ink type, marker accent (always a
|
|
4
|
+
* ground with ink on top; the link derivation handles its text-grade). */
|
|
3
5
|
export declare const PAGE_COLOR_DEFAULTS: {
|
|
4
|
-
readonly accent: "#
|
|
5
|
-
readonly bg: "#
|
|
6
|
-
readonly fg: "#
|
|
7
|
-
readonly muted: "#
|
|
8
|
-
readonly card: "#
|
|
9
|
-
readonly border: "#
|
|
10
|
-
readonly darkBg: "#
|
|
11
|
-
readonly darkFg: "#
|
|
12
|
-
readonly darkMuted: "#
|
|
13
|
-
readonly darkCard: "#
|
|
14
|
-
readonly darkBorder: "#
|
|
6
|
+
readonly accent: "#d8f24b";
|
|
7
|
+
readonly bg: "#f7f5ef";
|
|
8
|
+
readonly fg: "#0c0c0c";
|
|
9
|
+
readonly muted: "#6e6a5e";
|
|
10
|
+
readonly card: "#f7f5ef";
|
|
11
|
+
readonly border: "#e5e1d4";
|
|
12
|
+
readonly darkBg: "#0c0c0c";
|
|
13
|
+
readonly darkFg: "#f0eee6";
|
|
14
|
+
readonly darkMuted: "#a6a193";
|
|
15
|
+
readonly darkCard: "#0c0c0c";
|
|
16
|
+
readonly darkBorder: "#2b2823";
|
|
15
17
|
};
|
|
16
18
|
export type PageSchemeColors = {
|
|
17
19
|
bg: string;
|
|
@@ -18,19 +18,21 @@
|
|
|
18
18
|
// Everything is pure and deterministic: publish finalize, dashboard previews,
|
|
19
19
|
// and tests all call the same functions and get the same pixels.
|
|
20
20
|
import { formatHex, oklch, parse, wcagContrast } from "culori";
|
|
21
|
-
/** Default page palette (mirrored into the shared template CSS)
|
|
21
|
+
/** Default page palette (mirrored into the shared template CSS) — the
|
|
22
|
+
* printed-matter scheme: warm canvas, ink type, marker accent (always a
|
|
23
|
+
* ground with ink on top; the link derivation handles its text-grade). */
|
|
22
24
|
export const PAGE_COLOR_DEFAULTS = {
|
|
23
|
-
accent: "#
|
|
24
|
-
bg: "#
|
|
25
|
-
fg: "#
|
|
26
|
-
muted: "#
|
|
27
|
-
card: "#
|
|
28
|
-
border: "#
|
|
29
|
-
darkBg: "#
|
|
30
|
-
darkFg: "#
|
|
31
|
-
darkMuted: "#
|
|
32
|
-
darkCard: "#
|
|
33
|
-
darkBorder: "#
|
|
25
|
+
accent: "#d8f24b",
|
|
26
|
+
bg: "#f7f5ef",
|
|
27
|
+
fg: "#0c0c0c",
|
|
28
|
+
muted: "#6e6a5e",
|
|
29
|
+
card: "#f7f5ef",
|
|
30
|
+
border: "#e5e1d4",
|
|
31
|
+
darkBg: "#0c0c0c",
|
|
32
|
+
darkFg: "#f0eee6",
|
|
33
|
+
darkMuted: "#a6a193",
|
|
34
|
+
darkCard: "#0c0c0c",
|
|
35
|
+
darkBorder: "#2b2823",
|
|
34
36
|
};
|
|
35
37
|
/** WCAG AA for body-size text. */
|
|
36
38
|
const TEXT_CONTRAST = 4.5;
|
|
@@ -139,7 +141,7 @@ function schemeFromBackground(background) {
|
|
|
139
141
|
/** The one entry point: raw theme in, everything the templates paint with out. */
|
|
140
142
|
export function derivePageColors(theme) {
|
|
141
143
|
const accentInput = theme.accent?.trim();
|
|
142
|
-
const accent = safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
|
|
144
|
+
const accent = accentInput && safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
|
|
143
145
|
const onAccent = onAccentColor(accent);
|
|
144
146
|
const background = theme.background?.trim();
|
|
145
147
|
if (background && safeParse(background)) {
|