@spacefast/common 0.0.5 → 0.0.6
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/dist/agents/connect-targets.d.ts +133 -0
- package/dist/agents/connect-targets.js +224 -0
- package/dist/brand.d.ts +1 -0
- package/dist/brand.js +5 -0
- package/dist/config/domains.d.ts +12 -6
- package/dist/config/domains.js +30 -38
- package/dist/contracts/access.d.ts +825 -125
- package/dist/contracts/access.js +771 -148
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +28 -2
- package/dist/contracts/annotations.d.ts +2 -4
- package/dist/contracts/api-keys.d.ts +3 -4
- package/dist/contracts/api-keys.js +9 -6
- package/dist/contracts/archives.d.ts +8 -2
- package/dist/contracts/archives.js +11 -4
- package/dist/contracts/billing.d.ts +0 -9
- package/dist/contracts/builds.d.ts +25 -21
- package/dist/contracts/builds.js +12 -0
- package/dist/contracts/common.d.ts +2 -0
- package/dist/contracts/common.js +4 -0
- package/dist/contracts/continuation.d.ts +0 -3
- package/dist/contracts/countries.d.ts +2 -0
- package/dist/contracts/countries.js +261 -0
- package/dist/contracts/deployments.d.ts +34 -34
- package/dist/contracts/device-auth.d.ts +4 -11
- package/dist/contracts/device-auth.js +4 -0
- package/dist/contracts/docs.d.ts +119 -0
- package/dist/contracts/docs.js +51 -0
- package/dist/contracts/domains.d.ts +96 -54
- package/dist/contracts/domains.js +26 -4
- package/dist/contracts/enums.d.ts +15 -16
- package/dist/contracts/enums.js +25 -14
- package/dist/contracts/error-code-meta.d.ts +72 -0
- package/dist/contracts/error-code-meta.js +96 -10
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +14 -0
- package/dist/contracts/events.d.ts +1 -1
- package/dist/contracts/features.d.ts +76 -6
- package/dist/contracts/features.js +178 -13
- package/dist/contracts/git.d.ts +2 -8
- package/dist/contracts/ids.d.ts +5 -0
- package/dist/contracts/ids.js +5 -0
- package/dist/contracts/mcp.d.ts +8 -10
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/oauth-resources.d.ts +19 -8
- package/dist/contracts/oauth-resources.js +44 -13
- package/dist/contracts/platform.d.ts +0 -102
- package/dist/contracts/platform.js +0 -50
- package/dist/contracts/push-new.d.ts +4 -4
- package/dist/contracts/quotas.d.ts +2 -0
- package/dist/contracts/quotas.js +8 -0
- package/dist/contracts/repository-connections.d.ts +6 -8
- package/dist/contracts/resources.d.ts +97 -13
- package/dist/contracts/resources.js +26 -2
- package/dist/contracts/routes.d.ts +0 -2
- package/dist/contracts/runtime-api.d.ts +361 -39
- package/dist/contracts/runtime-api.js +275 -6
- package/dist/contracts/sites.d.ts +19 -2
- package/dist/contracts/sites.js +21 -0
- package/dist/contracts/space-config.d.ts +201 -2
- package/dist/contracts/space-config.js +135 -5
- package/dist/contracts/spaces.d.ts +364 -193
- package/dist/contracts/spaces.js +40 -8
- package/dist/contracts/superadmin-emails.d.ts +4 -2
- package/dist/contracts/superadmin-emails.js +8 -0
- package/dist/contracts/superadmin-queues.d.ts +26 -26
- package/dist/contracts/superadmin-spaces.d.ts +170 -42
- package/dist/contracts/superadmin.d.ts +6 -93
- package/dist/contracts/superadmin.js +6 -14
- package/dist/contracts/tags.d.ts +10 -9
- package/dist/contracts/tags.js +2 -1
- package/dist/contracts/teams.d.ts +21 -22
- package/dist/contracts/teams.js +0 -1
- package/dist/contracts/theme-json.d.ts +30 -0
- package/dist/contracts/theme-json.js +48 -0
- package/dist/contracts/variables.d.ts +0 -2
- package/dist/contracts/variables.js +0 -5
- package/dist/contracts/webhooks.d.ts +2 -0
- package/dist/contracts/webhooks.js +1 -1
- package/dist/contracts/zero.d.ts +5 -10
- package/dist/docs/agent-prose.d.ts +43 -3
- package/dist/docs/agent-prose.js +146 -7
- package/dist/docs/agent-setup.js +45 -13
- package/dist/docs/agent-solutions.d.ts +5 -5
- package/dist/docs/agent-solutions.js +2 -2
- package/dist/docs/catalog.d.ts +650 -0
- package/dist/docs/catalog.js +758 -0
- package/dist/docs/error-docs.d.ts +0 -2
- package/dist/docs/error-docs.js +77 -35
- package/dist/docs/index-build.d.ts +17 -0
- package/dist/docs/index-build.js +24 -0
- package/dist/docs/search.d.ts +54 -0
- package/dist/docs/search.js +96 -0
- package/dist/docs/skill-distribution.d.ts +5 -3
- package/dist/docs/skill-distribution.js +8 -8
- package/dist/utils/access-match.d.ts +39 -0
- package/dist/utils/access-match.js +296 -0
- package/dist/utils/auth-redirect.d.ts +3 -1
- package/dist/utils/auth-redirect.js +21 -1
- package/dist/utils/build-settings.d.ts +6 -0
- package/dist/utils/build-settings.js +429 -46
- package/dist/utils/claim-token.d.ts +8 -0
- package/dist/utils/claim-token.js +23 -2
- package/dist/utils/content-type.d.ts +1 -0
- package/dist/utils/content-type.js +1 -1
- package/dist/utils/credential-policy.d.ts +54 -0
- package/dist/utils/{access-policy.js → credential-policy.js} +28 -2
- package/dist/utils/gate-theme.d.ts +61 -0
- package/dist/utils/gate-theme.js +217 -0
- package/dist/utils/git-repository.d.ts +0 -1
- package/dist/utils/git-repository.js +0 -3
- package/dist/utils/id-hints.d.ts +8 -0
- package/dist/utils/id-hints.js +61 -0
- package/dist/utils/local-space-state.d.ts +91 -0
- package/dist/utils/local-space-state.js +251 -0
- package/dist/utils/oauth-signed-query.d.ts +8 -0
- package/dist/utils/oauth-signed-query.js +26 -0
- package/dist/utils/publish-detection.d.ts +0 -8
- package/dist/utils/publish-detection.js +0 -93
- package/dist/utils/publish-policy.d.ts +6 -5
- package/dist/utils/publish-policy.js +71 -7
- package/dist/utils/query-keys.d.ts +15 -4
- package/dist/utils/query-keys.js +27 -4
- package/dist/utils/runtime-paths.d.ts +0 -1
- package/dist/utils/runtime-paths.js +0 -1
- package/dist/utils/runtime-upload-batch.d.ts +18 -0
- package/dist/utils/runtime-upload-batch.js +90 -0
- package/dist/utils/runtime-upload.d.ts +2 -0
- package/dist/utils/runtime-upload.js +5 -0
- package/dist/utils/space-config.d.ts +4 -0
- package/dist/utils/space-config.js +10 -1
- package/dist/utils/space-theme.d.ts +2 -0
- package/dist/utils/space-theme.js +9 -0
- package/dist/utils/static-runtime-policy.d.ts +0 -21
- package/dist/utils/static-runtime-policy.js +19 -103
- package/dist/utils/upload-session.d.ts +1 -1
- package/dist/utils/wpcom-auth-redirects.d.ts +32 -2
- package/dist/utils/wpcom-auth-redirects.js +26 -10
- package/dist/vocabulary.d.ts +130 -30
- package/dist/vocabulary.js +77 -36
- package/package.json +16 -1
- package/dist/contracts/account.d.ts +0 -28
- package/dist/contracts/account.js +0 -54
- package/dist/contracts/intercom.d.ts +0 -6
- package/dist/contracts/intercom.js +0 -5
- package/dist/contracts/telemetry.d.ts +0 -2
- package/dist/contracts/telemetry.js +0 -1
- package/dist/utils/access-policy.d.ts +0 -39
- package/dist/utils/error-display.d.ts +0 -7
- package/dist/utils/error-display.js +0 -42
package/dist/contracts/access.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
// Unified access contracts (internal-docs/
|
|
3
|
-
//
|
|
2
|
+
// Unified access contracts (internal-docs/access-plan.html — the only access
|
|
3
|
+
// document).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// The one access & policy lane. The headline simplification: **firewall ⊂
|
|
6
|
+
// access** — there is exactly one Rule schema, and an anonymous firewall rule
|
|
7
|
+
// is simply a Rule whose `auth` is absent. There is exactly ONE satisfaction
|
|
8
|
+
// test: token grants ∩ requiredGrants (X-32); `auth.acquire` only configures
|
|
9
|
+
// how the challenge page lets a visitor obtain a token. The runtime verifies
|
|
10
|
+
// fully offline; the cloud resolves identity and mints signed tokens (plus
|
|
11
|
+
// exactly one non-cloud minter: the runtime's space-local `pw:` mint). Field
|
|
12
|
+
// names are the spec contract — do not rename.
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
12
14
|
// 1. Grants
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
@@ -19,35 +21,41 @@ import { z } from "zod";
|
|
|
19
21
|
// `requiredGrants` so a rule can name a class of identities:
|
|
20
22
|
//
|
|
21
23
|
// email:alice@acme.com a single verified email
|
|
22
|
-
// email:*@acme.com any verified email on a domain (glob
|
|
24
|
+
// email:*@acme.com any verified email on a domain (glob — this IS
|
|
25
|
+
// the "anyone at a domain" audience; there is no
|
|
26
|
+
// separate email-domain namespace)
|
|
23
27
|
// team:team_123:member top-level team membership at a role
|
|
24
28
|
// sub-team:sub_123:member enterprise group membership inside a team
|
|
25
29
|
// user:<handle> a specific platform person
|
|
26
|
-
//
|
|
27
|
-
//
|
|
30
|
+
// space:{spaceId}:{level} a leveled space grant (SPACE_LEVELS)
|
|
31
|
+
// link:lnk_x an active share link (structural revocation)
|
|
32
|
+
// invite:sin_x an accepted invite (structural revocation)
|
|
33
|
+
// svc:stk_x a service token (CI, agents; structural)
|
|
34
|
+
// sso:acn_x an OIDC connection identity
|
|
35
|
+
// ext:{connectionId}:<tag> an owner-defined external (BYO issuer) grant
|
|
36
|
+
// pw:{ruleId} a satisfied password wall (runtime-local mint —
|
|
37
|
+
// the ONLY namespace the space-local key signs)
|
|
28
38
|
//
|
|
29
|
-
// Tokens signed by an external (BYO) key
|
|
30
|
-
//
|
|
31
|
-
// before
|
|
39
|
+
// Namespaces exist iff a minter exists. Tokens signed by an external (BYO) key
|
|
40
|
+
// may only carry grants namespaced to that key (see
|
|
41
|
+
// `accessKeySchema.grantNamespaces`); everything else is dropped before
|
|
42
|
+
// intersection.
|
|
32
43
|
export const grantSchema = z.string().min(1).max(255);
|
|
33
|
-
// Known grant namespace prefixes
|
|
34
|
-
// every future namespace, but covers the documented vocabulary.
|
|
44
|
+
// Known grant namespace prefixes — final vocabulary (access-plan §1).
|
|
35
45
|
export const GRANT_NAMESPACES = [
|
|
36
46
|
"email:",
|
|
37
47
|
"team:",
|
|
38
48
|
"sub-team:",
|
|
39
49
|
"user:",
|
|
40
|
-
"svc:",
|
|
41
|
-
"ext:",
|
|
42
|
-
// Mint-time-only namespaces (sources the cloud resolves into the above, plus
|
|
43
|
-
// transient grants the runtime mints itself).
|
|
44
|
-
"email-domain:",
|
|
45
50
|
"space:",
|
|
46
51
|
"link:",
|
|
52
|
+
"invite:",
|
|
53
|
+
"svc:",
|
|
47
54
|
"sso:",
|
|
48
|
-
"
|
|
55
|
+
"ext:",
|
|
56
|
+
"pw:",
|
|
49
57
|
];
|
|
50
|
-
const GRANT_PATTERN = /^(email|
|
|
58
|
+
const GRANT_PATTERN = /^(email|team|sub-team|user|space|link|invite|svc|sso|ext|pw):.+$/;
|
|
51
59
|
// Validator for grant globs like "email:*@acme.com", "team:team_123:member",
|
|
52
60
|
// "sub-team:sub_123:member", "user:<h>", "svc:…", "ext:{conn}:…". Returns true when the grant string is a
|
|
53
61
|
// well-formed `namespace:value`. This is advisory (the runtime intersects
|
|
@@ -61,6 +69,12 @@ export function isWellFormedGrant(grant) {
|
|
|
61
69
|
export const requiredGrantSchema = grantSchema.refine(isWellFormedGrant, {
|
|
62
70
|
message: "grant must be namespaced, e.g. email:*@acme.com, team:team_123:member, sub-team:sub_123:member, user:<h>, svc:…, ext:{conn}:…",
|
|
63
71
|
});
|
|
72
|
+
// The ONE space-level vocabulary (access-plan X-38): consumed by
|
|
73
|
+
// `space:{id}:{level}` grants, Cast's capability table, invitee levels, and the
|
|
74
|
+
// future editor role. No parallel level enum may exist anywhere. v1 surfaces
|
|
75
|
+
// mint only `viewer` and `commenter`.
|
|
76
|
+
export const SPACE_LEVELS = ["viewer", "commenter", "editor", "admin"];
|
|
77
|
+
export const spaceLevelSchema = z.enum(SPACE_LEVELS);
|
|
64
78
|
// ---------------------------------------------------------------------------
|
|
65
79
|
// 2. Issuer registry
|
|
66
80
|
// ---------------------------------------------------------------------------
|
|
@@ -74,29 +88,227 @@ export const accessKeySchema = z.object({
|
|
|
74
88
|
publicKey: z.string().min(1).describe("base64url-encoded raw Ed25519 public key."),
|
|
75
89
|
// Grants in a token signed by this key are DROPPED unless prefixed by one of
|
|
76
90
|
// these. Platform keys list the platform prefixes
|
|
77
|
-
// (["team:","sub-team:","user:","email:","
|
|
91
|
+
// (["team:","sub-team:","user:","email:","space:","link:","svc:","sso:"]);
|
|
78
92
|
// external keys list exactly ["ext:{connectionId}:"], bounding a leaked key's
|
|
79
93
|
// blast radius to the rules the owner pointed at it.
|
|
80
94
|
grantNamespaces: z.array(z.string().min(1)).min(1),
|
|
81
95
|
});
|
|
82
96
|
// ---------------------------------------------------------------------------
|
|
97
|
+
// 2b. Identity connections (access-plan §4.4/§6.2/§6.3, X-18/X-19)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// A team-level identity connection (`acn_` row): either a company OIDC
|
|
100
|
+
// relying-party config (§6.2 — the authorize endpoint runs the code flow and
|
|
101
|
+
// mints the visitor token; no Spacefast account is created) or an external
|
|
102
|
+
// BYO issuer (§6.3 — the shop holds the private key and mints its own
|
|
103
|
+
// Ed25519 tokens; Spacefast is not in the login path).
|
|
104
|
+
export const ACCESS_CONNECTION_TYPES = ["oidc", "external"];
|
|
105
|
+
export const accessConnectionTypeSchema = z.enum(ACCESS_CONNECTION_TYPES);
|
|
106
|
+
// Session TTL defaults (§4.4): 8h for company SSO (re-auth is a transparent
|
|
107
|
+
// bounce while the IdP session lives), 30min for external issuers (the short
|
|
108
|
+
// TTL bounds entitlement drift — a cancelled subscriber survives at most one
|
|
109
|
+
// TTL).
|
|
110
|
+
export const OIDC_CONNECTION_DEFAULT_TTL_SECONDS = 8 * 60 * 60; // 28800
|
|
111
|
+
export const EXTERNAL_CONNECTION_DEFAULT_TTL_SECONDS = 30 * 60; // 1800
|
|
112
|
+
// Hard ceiling on an EXTERNAL-issued token's exp (§6.3: exp ≤ 30min). The
|
|
113
|
+
// connection's sessionTtlSeconds may be shorter, never longer.
|
|
114
|
+
export const EXTERNAL_TOKEN_MAX_TTL_SECONDS = 30 * 60;
|
|
115
|
+
// The grant a company-SSO visitor carries (§6.2): `sso:{connectionId}`.
|
|
116
|
+
export function ssoGrant(connectionId) {
|
|
117
|
+
return `sso:${connectionId}`;
|
|
118
|
+
}
|
|
119
|
+
// The grant namespace an external connection's keys are bound to (§6.3):
|
|
120
|
+
// tokens signed by a shop key are dropped unless prefixed `ext:{acn}:` — a
|
|
121
|
+
// leaked shop key can forge exactly the rules the owner pointed at it, never
|
|
122
|
+
// team:/user: grants (corpus-pinned).
|
|
123
|
+
export function externalGrantNamespace(connectionId) {
|
|
124
|
+
return `ext:${connectionId}:`;
|
|
125
|
+
}
|
|
126
|
+
// An owner-defined external grant (§6.3): `ext:{acn}:{tag}`, e.g.
|
|
127
|
+
// `ext:acn_x:wc-subscriber`.
|
|
128
|
+
export function externalGrant(connectionId, tag) {
|
|
129
|
+
return `ext:${connectionId}:${tag}`;
|
|
130
|
+
}
|
|
131
|
+
// STORED form of a connection login acquire (`loginAcquireSchema.url`): the
|
|
132
|
+
// compiler stores the symbolic `connection:{acn}` reference and the resolver
|
|
133
|
+
// rewrites it to the live URL on every policy resolution (the OIDC leg URL or
|
|
134
|
+
// the shop's authorizeUrl with the CURRENT space/sv/host baked in). Nothing
|
|
135
|
+
// stored ever carries a stale sessionVersion; a revoked connection's acquire
|
|
136
|
+
// resolves to nothing (the login block disappears — fail closed).
|
|
137
|
+
export function connectionAcquireRef(connectionId) {
|
|
138
|
+
return `connection:${connectionId}`;
|
|
139
|
+
}
|
|
140
|
+
export function parseConnectionAcquireRef(url) {
|
|
141
|
+
if (!url.startsWith("connection:")) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const id = url.slice("connection:".length);
|
|
145
|
+
return id.length > 0 && !id.includes("/") && !id.includes(":") ? id : null;
|
|
146
|
+
}
|
|
147
|
+
// The connection ids a rule references — through `sso:{acn}` / `ext:{acn}:…`
|
|
148
|
+
// requiredGrants or symbolic `connection:{acn}` acquire refs. The resolver
|
|
149
|
+
// embeds these connections' keys/acquire legs at hydration time.
|
|
150
|
+
export function referencedConnectionIds(input) {
|
|
151
|
+
const ids = new Set();
|
|
152
|
+
for (const grant of input.requiredGrants ?? []) {
|
|
153
|
+
if (grant.startsWith("sso:")) {
|
|
154
|
+
const id = grant.slice("sso:".length);
|
|
155
|
+
if (id && !id.includes(":")) {
|
|
156
|
+
ids.add(id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (grant.startsWith("ext:")) {
|
|
160
|
+
const id = grant.slice("ext:".length).split(":")[0];
|
|
161
|
+
if (id) {
|
|
162
|
+
ids.add(id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const acquire of input.acquire ?? []) {
|
|
167
|
+
if (acquire.type === "login" && acquire.url) {
|
|
168
|
+
const id = parseConnectionAcquireRef(acquire.url);
|
|
169
|
+
if (id) {
|
|
170
|
+
ids.add(id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [...ids];
|
|
175
|
+
}
|
|
176
|
+
// A raw Ed25519 public key is 32 bytes → 43 base64url chars, no padding.
|
|
177
|
+
export const externalConnectionPublicKeySchema = z.object({
|
|
178
|
+
kid: z.string().min(1).max(64),
|
|
179
|
+
publicKey: z
|
|
180
|
+
.string()
|
|
181
|
+
.regex(/^[A-Za-z0-9_-]{43}$/, "publicKey must be a base64url raw 32-byte Ed25519 public key"),
|
|
182
|
+
});
|
|
183
|
+
const guestEmailDomainsSchema = z
|
|
184
|
+
.array(z
|
|
185
|
+
.string()
|
|
186
|
+
.min(3)
|
|
187
|
+
.max(255)
|
|
188
|
+
.regex(/^[a-z0-9.-]+\.[a-z]{2,}$/i, "must be a bare domain, e.g. acme.com"))
|
|
189
|
+
.max(20);
|
|
190
|
+
// The connection projected onto the wire — the OIDC client secret NEVER
|
|
191
|
+
// appears (masked after write, §4.4); `clientSecretSet` says one is stored.
|
|
192
|
+
export const accessConnectionSchema = z.object({
|
|
193
|
+
id: z.string().min(1),
|
|
194
|
+
teamId: z.string().min(1),
|
|
195
|
+
type: accessConnectionTypeSchema,
|
|
196
|
+
name: z.string().min(1),
|
|
197
|
+
// OIDC half (null for external connections).
|
|
198
|
+
issuer: z.string().min(1).nullable().default(null),
|
|
199
|
+
clientId: z.string().min(1).nullable().default(null),
|
|
200
|
+
clientSecretSet: z.boolean().default(false),
|
|
201
|
+
// External half (null for oidc connections). `authorizeUrl` is HTTPS-pinned
|
|
202
|
+
// at create and admin-only to mutate.
|
|
203
|
+
authorizeUrl: z.string().min(1).nullable().default(null),
|
|
204
|
+
publicKeys: z.array(externalConnectionPublicKeySchema).default([]),
|
|
205
|
+
sessionTtlSeconds: z.number().int().positive(),
|
|
206
|
+
// Verified-email domains auto-granted team-viewer at mint (§6.2); null = none.
|
|
207
|
+
guestEmailDomains: z.array(z.string().min(1)).nullable().default(null),
|
|
208
|
+
createdAt: z.string().min(1),
|
|
209
|
+
revokedAt: z.string().min(1).nullable().default(null),
|
|
210
|
+
active: z.boolean(),
|
|
211
|
+
});
|
|
212
|
+
export const oidcConnectionCreateSchema = z.object({
|
|
213
|
+
type: z.literal("oidc"),
|
|
214
|
+
name: z.string().min(1).max(200),
|
|
215
|
+
// The IdP issuer URL (HTTPS): discovery runs against
|
|
216
|
+
// `{issuer}/.well-known/openid-configuration`.
|
|
217
|
+
issuer: z.string().min(1).max(1024),
|
|
218
|
+
clientId: z.string().min(1).max(512),
|
|
219
|
+
clientSecret: z.string().min(1).max(2048),
|
|
220
|
+
sessionTtlSeconds: z
|
|
221
|
+
.number()
|
|
222
|
+
.int()
|
|
223
|
+
.min(60)
|
|
224
|
+
.max(24 * 60 * 60)
|
|
225
|
+
.optional(),
|
|
226
|
+
guestEmailDomains: guestEmailDomainsSchema.optional(),
|
|
227
|
+
});
|
|
228
|
+
export const externalConnectionCreateSchema = z.object({
|
|
229
|
+
type: z.literal("external"),
|
|
230
|
+
name: z.string().min(1).max(200),
|
|
231
|
+
// The shop's authorize endpoint (HTTPS-pinned): the challenge page's login
|
|
232
|
+
// link forwards straight here for single-audience external rules.
|
|
233
|
+
authorizeUrl: z.string().min(1).max(2048),
|
|
234
|
+
publicKeys: z.array(externalConnectionPublicKeySchema).min(1).max(5),
|
|
235
|
+
sessionTtlSeconds: z.number().int().min(60).max(EXTERNAL_TOKEN_MAX_TTL_SECONDS).optional(),
|
|
236
|
+
guestEmailDomains: guestEmailDomainsSchema.optional(),
|
|
237
|
+
});
|
|
238
|
+
export const accessConnectionCreateSchema = z.discriminatedUnion("type", [
|
|
239
|
+
oidcConnectionCreateSchema,
|
|
240
|
+
externalConnectionCreateSchema,
|
|
241
|
+
]);
|
|
242
|
+
// PATCH shape — fields are validated against the connection's type at the
|
|
243
|
+
// route (`authorizeUrl`/`publicKeys` are external-only; `issuer`/`clientId`/
|
|
244
|
+
// `clientSecret` are oidc-only). `publicKeys` REPLACES the key set (push-key
|
|
245
|
+
// rotation: push the new key alongside the old, flip signing, drop the old).
|
|
246
|
+
export const accessConnectionUpdateSchema = z.object({
|
|
247
|
+
name: z.string().min(1).max(200).optional(),
|
|
248
|
+
issuer: z.string().min(1).max(1024).optional(),
|
|
249
|
+
clientId: z.string().min(1).max(512).optional(),
|
|
250
|
+
clientSecret: z.string().min(1).max(2048).optional(),
|
|
251
|
+
authorizeUrl: z.string().min(1).max(2048).optional(),
|
|
252
|
+
publicKeys: z.array(externalConnectionPublicKeySchema).min(1).max(5).optional(),
|
|
253
|
+
sessionTtlSeconds: z
|
|
254
|
+
.number()
|
|
255
|
+
.int()
|
|
256
|
+
.min(60)
|
|
257
|
+
.max(24 * 60 * 60)
|
|
258
|
+
.optional(),
|
|
259
|
+
guestEmailDomains: guestEmailDomainsSchema.nullable().optional(),
|
|
260
|
+
});
|
|
261
|
+
// DELETE response: revocation is structural — rules referencing the
|
|
262
|
+
// connection lose those grants in the immediate recompile; rules left with an
|
|
263
|
+
// empty audience are disabled (removed) and listed so the owner knows exactly
|
|
264
|
+
// what stopped enforcing via that connection.
|
|
265
|
+
export const accessConnectionRevokeResultSchema = z.object({
|
|
266
|
+
connection: accessConnectionSchema,
|
|
267
|
+
disabledRules: z
|
|
268
|
+
.array(z.object({ spaceId: z.string().min(1), ruleId: z.string().min(1) }))
|
|
269
|
+
.default([]),
|
|
270
|
+
});
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
83
272
|
// 3. The unified runtime Rule (firewall = a rule with no auth)
|
|
84
273
|
// ---------------------------------------------------------------------------
|
|
85
|
-
// The `auth` object
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
274
|
+
// The `auth` object (access-plan X-32): there is exactly ONE satisfaction
|
|
275
|
+
// test — token grants ∩ `requiredGrants`. `acquire` entries only configure how
|
|
276
|
+
// the challenge page lets an unsatisfied visitor OBTAIN a token:
|
|
277
|
+
//
|
|
278
|
+
// password → the challenge page renders a password form; a valid POST mints
|
|
279
|
+
// the standard visitor token RUNTIME-LOCALLY (space HS256 key,
|
|
280
|
+
// `pw:{ruleId}` grant) into the one `spacefast_access` cookie.
|
|
281
|
+
// `basic` transport is the stateless HTTP 401 variant — valid
|
|
282
|
+
// Basic credentials satisfy `pw:{ruleId}` per-request (browsers
|
|
283
|
+
// replay the Authorization header; no cookie needed).
|
|
284
|
+
// login → the challenge page renders a sign-in link into the cloud
|
|
285
|
+
// authorize chain (or a BYO issuer's authorizeUrl).
|
|
286
|
+
//
|
|
287
|
+
// Mixed audiences are just multiple acquire entries on one rule — the chooser
|
|
288
|
+
// is the challenge page rendering every acquire block, never a new surface.
|
|
289
|
+
export const passwordAcquireSchema = z.object({
|
|
290
|
+
type: z.literal("password"),
|
|
291
|
+
// A secret reference (e.g. "secret:space-password"), never plaintext. The
|
|
292
|
+
// runtime resolves it from serving secrets and `password_verify`s.
|
|
93
293
|
ref: z.string().min(1),
|
|
94
|
-
// `basic` = stateless HTTP Basic 401 challenge. `form` = renders the password
|
|
95
|
-
// wall, and on success mints a short-lived `pw:` grant cookie.
|
|
96
294
|
transport: z.enum(["basic", "form"]),
|
|
97
295
|
username: z.string().min(1).optional(),
|
|
98
296
|
});
|
|
99
|
-
export const
|
|
297
|
+
export const loginAcquireSchema = z.object({
|
|
298
|
+
type: z.literal("login"),
|
|
299
|
+
// Interactive login surface the challenge page links to (the cloud authorize
|
|
300
|
+
// endpoint, or a BYO connection's authorizeUrl). Hydrated by the compiler;
|
|
301
|
+
// the runtime appends `return=` as a plain same-origin path.
|
|
302
|
+
url: z.string().min(1),
|
|
303
|
+
// Display label for the challenge-page button (e.g. "Continue with
|
|
304
|
+
// Spacefast", "Sign in with Acme Shop").
|
|
305
|
+
label: z.string().min(1).optional(),
|
|
306
|
+
});
|
|
307
|
+
export const acquireSchema = z.discriminatedUnion("type", [
|
|
308
|
+
passwordAcquireSchema,
|
|
309
|
+
loginAcquireSchema,
|
|
310
|
+
]);
|
|
311
|
+
export const ruleAuthSchema = z.object({
|
|
100
312
|
// ANY-OF: the visitor's verified grants must intersect this set. Entries may
|
|
101
313
|
// be globs (e.g. "email:*@acme.com").
|
|
102
314
|
requiredGrants: z.array(grantSchema).min(1),
|
|
@@ -104,26 +316,19 @@ export const tokenAuthSchema = z.object({
|
|
|
104
316
|
// compiler fills in the applicable platform/registered keys. Tokens signed by
|
|
105
317
|
// a key contribute only grants within that key's `grantNamespaces`.
|
|
106
318
|
issuers: z.array(accessKeySchema).optional(),
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
// Cookie name the runtime reads the visitor token from (default
|
|
112
|
-
// `stattic_access`); the Authorization Bearer header always takes precedence.
|
|
113
|
-
cookieName: z.string().min(1).optional(),
|
|
114
|
-
});
|
|
115
|
-
export const ruleAuthSchema = z
|
|
116
|
-
.object({
|
|
117
|
-
password: passwordAuthSchema.optional(),
|
|
118
|
-
token: tokenAuthSchema.optional(),
|
|
119
|
-
})
|
|
120
|
-
.refine((a) => a.password !== undefined || a.token !== undefined, {
|
|
121
|
-
message: "auth must specify at least one of `password` or `token`",
|
|
319
|
+
// How the challenge page lets a visitor get a token. Absent = no interactive
|
|
320
|
+
// acquisition (service-token-only rule): the runtime denies a browser rather
|
|
321
|
+
// than loop.
|
|
322
|
+
acquire: z.array(acquireSchema).optional(),
|
|
122
323
|
});
|
|
123
324
|
// Match conditions AND together (host AND path AND channel AND ipCidrs AND …).
|
|
124
325
|
// All fields optional; an empty match matches everything in scope.
|
|
125
326
|
export const ruleMatchSchema = z.object({
|
|
126
327
|
host: z.string().min(1).optional(),
|
|
328
|
+
hostPattern: z.string().min(1).optional(),
|
|
329
|
+
// `_headers` absolute host matcher template: ":branch.example.com" keeps
|
|
330
|
+
// placeholders single-label while "*" keeps the existing host wildcard shape.
|
|
331
|
+
hostTemplate: z.string().min(1).optional(),
|
|
127
332
|
// Glob path pattern: "/docs/**" | "/docs/*" | "/x.html".
|
|
128
333
|
pathPattern: z.string().min(1).optional(),
|
|
129
334
|
// Serving context channel name (e.g. "live", "staging").
|
|
@@ -137,46 +342,68 @@ export const ruleMatchSchema = z.object({
|
|
|
137
342
|
// Single request-header matcher: name + expected value.
|
|
138
343
|
header: z.object({ name: z.string().min(1), value: z.string() }).optional(),
|
|
139
344
|
});
|
|
345
|
+
// Managed-block owners (access-plan X-34). Block ownership is THIS schema
|
|
346
|
+
// field, never a reasonCode convention: `managedBy` is stripped/rejected from
|
|
347
|
+
// all user input, so a raw rule can never claim or clobber a managed block.
|
|
348
|
+
// Cloud-lane block order: operator/incident rules first, then `firewall`, then
|
|
349
|
+
// `sharing`/`file_share`/`cast_reviewer`, then `team_default` last.
|
|
350
|
+
export const MANAGED_BY_VALUES = [
|
|
351
|
+
"sharing",
|
|
352
|
+
"firewall",
|
|
353
|
+
"file_share",
|
|
354
|
+
"cast_reviewer",
|
|
355
|
+
"team_default",
|
|
356
|
+
];
|
|
357
|
+
export const managedBySchema = z.enum(MANAGED_BY_VALUES);
|
|
140
358
|
// The one Rule. `match` (AND) selects requests; `effect` is what happens on
|
|
141
359
|
// match; `auth` (absent = anonymous firewall) gates `challenge`/`allow`.
|
|
142
360
|
export const runtimeRuleSchema = z.object({
|
|
361
|
+
// Stable rule id. Required wherever a grant references the rule
|
|
362
|
+
// (`pw:{ruleId}`); the compiler assigns ids to managed rules.
|
|
363
|
+
id: z.string().min(1).optional(),
|
|
143
364
|
match: ruleMatchSchema.default({}),
|
|
144
365
|
effect: z.enum(["allow", "deny", "challenge"]),
|
|
145
366
|
// Absent = anonymous firewall rule (no credential required). Present = pass
|
|
146
|
-
// when
|
|
367
|
+
// when token grants ∩ requiredGrants ≠ ∅ (the one satisfaction test).
|
|
147
368
|
auth: ruleAuthSchema.optional(),
|
|
369
|
+
// Which product surface owns this rule (managed blocks). Never accepted from
|
|
370
|
+
// user input — `replaceManagedRules` is the only writer.
|
|
371
|
+
managedBy: managedBySchema.optional(),
|
|
372
|
+
// Unix seconds. TTL'd interventions (`sf block --ttl`): the runtime SKIPS an
|
|
373
|
+
// expired rule at match time (one timestamp compare, no recompile needed);
|
|
374
|
+
// the compiler GCs expired rules on the next write.
|
|
375
|
+
expiresAt: z.number().int().nonnegative().optional(),
|
|
148
376
|
// Surfaced on a `deny` (X-Spacefast-Reason header) and the rendered platform
|
|
149
|
-
// page so an operator/visitor can tell which rule fired and why.
|
|
377
|
+
// page so an operator/visitor can tell which rule fired and why. Diagnostic
|
|
378
|
+
// only — never load-bearing for ownership or semantics.
|
|
150
379
|
reasonCode: z.string().min(1).optional(),
|
|
151
380
|
message: z.string().min(1).optional(),
|
|
152
381
|
});
|
|
153
382
|
// ---------------------------------------------------------------------------
|
|
154
383
|
// 3b. Visitor token (the cloud "mint" half)
|
|
155
384
|
// ---------------------------------------------------------------------------
|
|
156
|
-
// The default cookie the runtime reads a visitor token from when a rule's token
|
|
157
|
-
// leg names no explicit `cookieName` (see access-rules.php
|
|
158
|
-
// `_stattic_unified_token_from_request`). The authorize endpoint sets the
|
|
159
|
-
// minted token under this name so the very next request to the protected host
|
|
160
|
-
// carries it.
|
|
161
|
-
export const ACCESS_TOKEN_COOKIE_NAME = "stattic_access";
|
|
162
385
|
// The header `kid`/`alg` the platform signs visitor tokens with. These match
|
|
163
|
-
// the runtime JWKS key (`
|
|
164
|
-
// JWKS-by-kid verify (
|
|
386
|
+
// the runtime JWKS key (`spacefast-runtime-v1` / EdDSA) so the runtime's
|
|
387
|
+
// JWKS-by-kid verify (shared/jwt.php) accepts a token the cloud minted. Do
|
|
165
388
|
// NOT introduce a second key system — these constants name the existing one.
|
|
166
|
-
export const ACCESS_TOKEN_KID = "
|
|
389
|
+
export const ACCESS_TOKEN_KID = "spacefast-runtime-v1";
|
|
167
390
|
export const ACCESS_TOKEN_ALG = "EdDSA";
|
|
168
|
-
// The
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
// `
|
|
172
|
-
//
|
|
173
|
-
|
|
391
|
+
// The published JWKS path (served under /.well-known/ by the control plane and
|
|
392
|
+
// fetched by the runtime + Cast).
|
|
393
|
+
export const RUNTIME_JWKS_WELL_KNOWN_FILENAME = "spacefast-runtime-jwks.json";
|
|
394
|
+
// The `kid` the runtime's SPACE-LOCAL HS256 mint signs under (access-plan
|
|
395
|
+
// §3.1). The key derives from the space's password secret + sessionVersion and
|
|
396
|
+
// may only sign `pw:` grants — the same namespace filter that bounds BYO keys
|
|
397
|
+
// bounds this one. Never published in any JWKS; verification re-derives the
|
|
398
|
+
// key from serving secrets.
|
|
399
|
+
export const LOCAL_PW_TOKEN_KID = "spacefast-local-pw-v1";
|
|
174
400
|
// The claim set carried by a minted visitor token. `grants` are concrete
|
|
175
401
|
// (no `*`), `email:`/`user:`/`team:<teamId>:<role>`/`sub-team:<subTeamId>:<role>` namespaced so they survive
|
|
176
402
|
// the platform issuer's `grantNamespaces` filter at the runtime. `aud`, when
|
|
177
403
|
// present, lowercases-binds the token to a single serving host.
|
|
178
404
|
export const accessTokenPayloadSchema = z.object({
|
|
179
|
-
// Subject: the authenticated platform user id the token was minted for
|
|
405
|
+
// Subject: the authenticated platform user id the token was minted for
|
|
406
|
+
// (or "invite:sin_x" / "link:lnk_x" / "pw:anon" for account-less audiences).
|
|
180
407
|
sub: z.string().min(1),
|
|
181
408
|
// Concrete (non-glob) grants. The runtime intersects these with a rule's
|
|
182
409
|
// `requiredGrants`; the visitor passes when the intersection is non-empty.
|
|
@@ -189,19 +416,25 @@ export const accessTokenPayloadSchema = z.object({
|
|
|
189
416
|
// Optional host binding (lowercased) — when set the token is only accepted on
|
|
190
417
|
// that serving host.
|
|
191
418
|
aud: z.string().min(1).optional(),
|
|
419
|
+
// Space session version (access-plan §3.1): the runtime rejects a visitor
|
|
420
|
+
// token whose `sv` mismatches `policy.sessionVersion`. Logout-all is an `sv`
|
|
421
|
+
// bump — structural, TTL-independent revocation. Absent = 0.
|
|
422
|
+
sv: z.number().int().nonnegative().optional(),
|
|
423
|
+
// Single-use id for authorize-chain callback handoffs (access-plan X-29):
|
|
424
|
+
// the runtime callback consumes `jti` in its replay cache, so a token lifted
|
|
425
|
+
// from browser history is dead after first use. Share tokens carry no `jti`
|
|
426
|
+
// (the share URL IS the credential; revocation is structural).
|
|
427
|
+
jti: z.string().min(1).optional(),
|
|
192
428
|
});
|
|
193
|
-
// Authorize request
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// the
|
|
197
|
-
// re-attaching the token. `aud` optionally binds the token to one host.
|
|
429
|
+
// Authorize request for `POST /v1/spaces/:id/access/authorize` — surviving for
|
|
430
|
+
// exactly one caller: the dashboard preview-token mint (iframe → runtime
|
|
431
|
+
// callback). It never sets cookies; only the runtime callback sets the visitor
|
|
432
|
+
// cookie. `aud` binds the token to the previewed host.
|
|
198
433
|
export const authorizeRequestSchema = z.object({
|
|
199
|
-
setCookie: z.boolean().default(true),
|
|
200
434
|
aud: z.string().min(1).optional(),
|
|
201
435
|
});
|
|
202
436
|
// Authorize response: the signed compact JWT plus its decoded claims (so a
|
|
203
|
-
// caller can show/inspect the grants without re-parsing)
|
|
204
|
-
// cookie was set on the response.
|
|
437
|
+
// caller can show/inspect the grants without re-parsing).
|
|
205
438
|
export const authorizeResponseSchema = z.object({
|
|
206
439
|
// The signed compact EdDSA JWT (`header.claims.signature`, base64url).
|
|
207
440
|
token: z.string().min(1),
|
|
@@ -209,100 +442,458 @@ export const authorizeResponseSchema = z.object({
|
|
|
209
442
|
payload: accessTokenPayloadSchema,
|
|
210
443
|
// Unix seconds the token expires at (convenience mirror of `payload.exp`).
|
|
211
444
|
expiresAt: z.number().int().nonnegative(),
|
|
212
|
-
|
|
213
|
-
|
|
445
|
+
});
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// 3b'. Access events (access-plan §5.6b, X-37)
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// The access event is a first-class contract from day one: the runtime emits
|
|
450
|
+
// one per enforced decision into the per-hostname analytics ingestion; the
|
|
451
|
+
// Views panel is a reader of an already-flowing stream. `grantsHash` is a
|
|
452
|
+
// sha256 of the sorted verified grant list (never the grants themselves).
|
|
453
|
+
export const accessEventSchema = z.object({
|
|
454
|
+
sub: z.string().min(1).nullable().default(null),
|
|
455
|
+
grantsHash: z.string().min(1).nullable().default(null),
|
|
456
|
+
ruleId: z.string().min(1).nullable().default(null),
|
|
457
|
+
effect: z.enum(["allow", "deny", "challenge"]),
|
|
458
|
+
reasonCode: z.string().min(1).nullable().default(null),
|
|
459
|
+
host: z.string().min(1),
|
|
460
|
+
path: z.string().min(1),
|
|
461
|
+
// Unix seconds.
|
|
462
|
+
ts: z.number().int().nonnegative(),
|
|
214
463
|
});
|
|
215
464
|
// ---------------------------------------------------------------------------
|
|
216
465
|
// 3c. Mint-time grant builders (the cloud "resolve identity → grants" half)
|
|
217
466
|
// ---------------------------------------------------------------------------
|
|
218
|
-
// The
|
|
219
|
-
// `space
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
export function spaceViewerGrant(spaceId) {
|
|
223
|
-
return `space:${spaceId}:viewer`;
|
|
224
|
-
}
|
|
225
|
-
// The leveled space grant an invited reviewer carries (CAST-NATIVE-PLAN D-2/
|
|
226
|
-
// D-4): `space:{spaceId}:{level}` where level ∈ viewer|commenter|editor. The
|
|
227
|
-
// `space:` namespace is on the platform issuer (resolve.ts `platformIssuer`), so
|
|
228
|
-
// the runtime keeps it; Cast reads the level off the token to key its
|
|
229
|
-
// capability table. `spaceViewerGrant` is the `viewer` special case kept for the
|
|
230
|
-
// accepted-invite path; this is the general leveled form the reviewer people
|
|
231
|
-
// layer reconciles and the Cast mint carries.
|
|
467
|
+
// The leveled space grant (access-plan X-38): `space:{spaceId}:{level}` where
|
|
468
|
+
// level ∈ SPACE_LEVELS. The `space:` namespace is on the platform issuer, so
|
|
469
|
+
// the runtime keeps it; Cast keys its capability table off this grant; the
|
|
470
|
+
// accepted-invite path mints the `viewer` level.
|
|
232
471
|
export function spaceLevelGrant(spaceId, level) {
|
|
233
472
|
return `space:${spaceId}:${level}`;
|
|
234
473
|
}
|
|
235
474
|
// The concrete grant a share-link holder carries: a token with `link:{linkId}`
|
|
236
|
-
// intersects the `
|
|
237
|
-
// fills from the ACTIVE link rows (§
|
|
475
|
+
// intersects the `sharing:`-block rule whose `requiredGrants` the compiler
|
|
476
|
+
// fills from the ACTIVE link rows (§5.2). Revoking the link recompiles it out
|
|
238
477
|
// and the token goes inert. The platform issuer lists the `link:` namespace.
|
|
239
478
|
export function shareLinkGrant(linkId) {
|
|
240
479
|
return `link:${linkId}`;
|
|
241
480
|
}
|
|
481
|
+
// The concrete grant an invited viewer carries: `invite:{inviteId}` compiled
|
|
482
|
+
// per active invite (§5.3); revocation is structural.
|
|
483
|
+
export function inviteGrant(inviteId) {
|
|
484
|
+
return `invite:${inviteId}`;
|
|
485
|
+
}
|
|
486
|
+
// The concrete grant a service token carries: `svc:{tokenId}` compiled
|
|
487
|
+
// wherever the token's `actsAs` satisfies (§5.5); revocation is structural.
|
|
488
|
+
export function serviceTokenGrant(tokenId) {
|
|
489
|
+
return `svc:${tokenId}`;
|
|
490
|
+
}
|
|
242
491
|
// ---------------------------------------------------------------------------
|
|
243
|
-
// 3d.
|
|
492
|
+
// 3d. Share links + the sharing projection (access-plan §5.1/§5.2, X-33)
|
|
244
493
|
// ---------------------------------------------------------------------------
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
export const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
|
|
494
|
+
// Share tokens are re-signed on demand from the link row (§5.2): the JWT is
|
|
495
|
+
// never stored, only the row. `exp` = the link's expiry capped at this ceiling
|
|
496
|
+
// (and re-copying re-mints, so a live link's URL never goes stale).
|
|
497
|
+
export const SHARE_LINK_TOKEN_MAX_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days
|
|
498
|
+
// A share link row projected onto the wire (§4.4). `path` is "/" for a
|
|
499
|
+
// space-wide link; any other value scopes the link's grant to that subtree
|
|
500
|
+
// (compile-time: the grant only ever appears in rules matching the path).
|
|
501
|
+
// `lastUsedAt`/`useCount` are the operational-observability columns (X-28);
|
|
502
|
+
// the runtime-side `?sf_share=` trade increments them offline, so they read 0
|
|
503
|
+
// until that ingestion lands.
|
|
504
|
+
export const shareLinkSchema = z.object({
|
|
505
|
+
id: z.string().min(1),
|
|
506
|
+
spaceId: z.string().min(1),
|
|
507
|
+
label: z.string().min(1).nullable().default(null),
|
|
508
|
+
path: z.string().min(1),
|
|
509
|
+
createdBy: z.string().min(1).nullable().default(null),
|
|
510
|
+
// Unix seconds; null = the link never expires (tokens still cap at 30d and
|
|
511
|
+
// re-mint from the row on copy).
|
|
512
|
+
expiresAt: z.number().int().nonnegative().nullable().default(null),
|
|
513
|
+
createdAt: z.string().min(1).describe("ISO 8601 timestamp."),
|
|
514
|
+
lastUsedAt: z.string().min(1).nullable().default(null),
|
|
515
|
+
useCount: z.number().int().nonnegative().default(0),
|
|
516
|
+
// false once revoked or past expiry — the grant is no longer compiled.
|
|
517
|
+
active: z.boolean(),
|
|
518
|
+
});
|
|
519
|
+
export const shareLinkCreateSchema = z.object({
|
|
520
|
+
// A plain absolute path ("/" = the whole space).
|
|
521
|
+
path: z.string().min(1).max(1024).default("/"),
|
|
522
|
+
// Unix seconds. Omitted = no expiry (structural revocation still applies).
|
|
523
|
+
expiresAt: z.number().int().positive().optional(),
|
|
524
|
+
label: z.string().min(1).max(200).optional(),
|
|
525
|
+
});
|
|
526
|
+
// Returned on create and on the explicit re-copy intent ONLY — list reads never
|
|
527
|
+
// carry the token-bearing URL (§5.2: share-URL responses are no-store).
|
|
528
|
+
export const shareLinkWithUrlSchema = shareLinkSchema.extend({
|
|
529
|
+
url: z.string().min(1).describe("https://<host><path>?sf_share=<token>"),
|
|
530
|
+
});
|
|
531
|
+
// The Share modal's General-access selector (§5.1). The mode is DERIVED from
|
|
532
|
+
// the `sharing:` managed block — never stored separately — so "who can see
|
|
533
|
+
// this" has exactly one home.
|
|
534
|
+
export const GENERAL_ACCESS_MODES = ["public", "password", "link", "people"];
|
|
535
|
+
export const generalAccessModeSchema = z.enum(GENERAL_ACCESS_MODES);
|
|
536
|
+
// A connection audience member on the People-only selector state (§6.2/§6.3):
|
|
537
|
+
// "your company SSO" (oidc — optionally narrowed to a verified email domain)
|
|
538
|
+
// or "Acme Shop subscribers" (external — the owner-defined grant tag).
|
|
539
|
+
export const sharingConnectionAudienceSchema = z.object({
|
|
540
|
+
connectionId: z.string().min(1),
|
|
541
|
+
// External connections: the grant tag ("wc-subscriber" → ext:{acn}:{tag}).
|
|
542
|
+
// Required for external, rejected for oidc.
|
|
543
|
+
tag: z
|
|
544
|
+
.string()
|
|
545
|
+
.min(1)
|
|
546
|
+
.max(100)
|
|
547
|
+
.regex(/^[A-Za-z0-9._-]+$/)
|
|
548
|
+
.optional(),
|
|
549
|
+
// OIDC connections: narrow the audience to a verified email domain — the
|
|
550
|
+
// rule compiles `email:*@{domain}` instead of `sso:{acn}` (the SSO mint
|
|
551
|
+
// carries the verified `email:` grant). Rejected for external.
|
|
552
|
+
emailDomain: z
|
|
553
|
+
.string()
|
|
554
|
+
.min(3)
|
|
555
|
+
.max(255)
|
|
556
|
+
.regex(/^[a-z0-9.-]+\.[a-z]{2,}$/i)
|
|
557
|
+
.optional(),
|
|
558
|
+
});
|
|
559
|
+
export const generalAccessWriteSchema = z.object({
|
|
560
|
+
mode: generalAccessModeSchema,
|
|
561
|
+
// Required when switching to "password" for the first time; optional when a
|
|
562
|
+
// stored password already exists (it is kept).
|
|
563
|
+
password: z.string().min(1).max(256).optional(),
|
|
564
|
+
// People-mode connection audiences (§6.4): compiled INTO the one sharing
|
|
565
|
+
// general rule (mixed audiences = one rule, multiple acquires — the chooser
|
|
566
|
+
// is the challenge page, never a new surface). Omitted = keep the current
|
|
567
|
+
// audiences; [] = remove them.
|
|
568
|
+
audiences: z.array(sharingConnectionAudienceSchema).max(20).optional(),
|
|
569
|
+
});
|
|
570
|
+
// A connection audience projected back on the sharing read (derived from the
|
|
571
|
+
// compiled block — never stored separately, X-33).
|
|
572
|
+
export const sharingAudienceSchema = z.object({
|
|
573
|
+
connectionId: z.string().min(1),
|
|
574
|
+
type: accessConnectionTypeSchema,
|
|
575
|
+
// Connection display name (null when the connection row is gone).
|
|
576
|
+
name: z.string().min(1).nullable().default(null),
|
|
577
|
+
tag: z.string().min(1).nullable().default(null),
|
|
578
|
+
emailDomain: z.string().min(1).nullable().default(null),
|
|
579
|
+
// The compiled requiredGrants entry this audience contributes.
|
|
580
|
+
grant: grantSchema,
|
|
581
|
+
});
|
|
582
|
+
// A row in the Share modal's People list. Team members are badged; invitees
|
|
583
|
+
// (§5.3) carry their invite id + status so the modal can revoke per-row and
|
|
584
|
+
// badge pending/accepted/expired.
|
|
585
|
+
export const sharingPersonSchema = z.object({
|
|
586
|
+
kind: z.enum(["team_member", "invitee"]),
|
|
587
|
+
userId: z.string().min(1).nullable().default(null),
|
|
588
|
+
email: z.string().min(1).nullable().default(null),
|
|
589
|
+
name: z.string().min(1).nullable().default(null),
|
|
590
|
+
// Team management role for members (owner|admin|member); null for invitees.
|
|
591
|
+
teamRole: z.string().min(1).nullable().default(null),
|
|
592
|
+
// Space level for invitees (viewer in v1); null for team members.
|
|
593
|
+
level: spaceLevelSchema.nullable().default(null),
|
|
594
|
+
// The sin_ invite id for invitees (per-row revoke); null for team members.
|
|
595
|
+
inviteId: z.string().min(1).nullable().default(null),
|
|
596
|
+
// Invite lifecycle for invitees ("expired" = the accept LINK lapsed
|
|
597
|
+
// unaccepted; access-grant compilation is structural, not TTL'd); null for
|
|
598
|
+
// team members. Revoked invites never appear in the People list.
|
|
599
|
+
status: z.enum(["pending", "accepted", "expired"]).nullable().default(null),
|
|
600
|
+
// Operational observability (X-28): per-invitee "last viewed"; null until
|
|
601
|
+
// the Views ingestion feeds it.
|
|
602
|
+
lastViewedAt: z.string().min(1).nullable().default(null),
|
|
603
|
+
});
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// 3d'. Invites (access-plan §5.3) + access requests (§5.4)
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// Accept-link validity (§4.4): how long the emailed magic link mints tokens.
|
|
608
|
+
// Access itself is structural (the invite grant stays compiled until revoke);
|
|
609
|
+
// an expired link only means "re-mail me a new one".
|
|
610
|
+
export const INVITE_ACCEPT_LINK_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days
|
|
611
|
+
// The visitor token an accept mints (§5.3): 30 days, re-entry by re-clicking
|
|
612
|
+
// the emailed link (X-8: no guest sessions, the inbox is the credential).
|
|
613
|
+
export const INVITE_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days
|
|
614
|
+
// Invite/request personal messages (X-25): ≤280 chars, plain text only —
|
|
615
|
+
// stored as typed, HTML-escaped at every render (email + pages), never
|
|
616
|
+
// linkified, rendered visually subordinate to the platform frame.
|
|
617
|
+
export const ACCESS_MESSAGE_MAX_LENGTH = 280;
|
|
618
|
+
export const INVITE_STATUSES = ["pending", "accepted", "expired", "revoked"];
|
|
619
|
+
export const inviteStatusSchema = z.enum(INVITE_STATUSES);
|
|
620
|
+
// A space invite projected onto the wire (§4.4). The accept token is NEVER
|
|
621
|
+
// stored or returned — only its hash is at rest, and the token travels solely
|
|
622
|
+
// in the invitee's email (possession of the inbox IS the credential). `email`
|
|
623
|
+
// is null once revoked (X-25: invitee addresses are PII of non-users and are
|
|
624
|
+
// purged on revoke; the row survives for audit).
|
|
264
625
|
export const spaceInviteSchema = z.object({
|
|
265
626
|
id: z.string().min(1),
|
|
266
627
|
spaceId: z.string().min(1),
|
|
267
|
-
email: z.string().
|
|
268
|
-
status: z.enum(["pending", "accepted", "revoked", "expired"]),
|
|
269
|
-
// The viewer grant this invite mints on acceptance — surfaced so the owner can
|
|
270
|
-
// see exactly what it grants (`space:{spaceId}:viewer`).
|
|
271
|
-
grant: z.string().min(1),
|
|
628
|
+
email: z.string().min(1).nullable().default(null),
|
|
272
629
|
invitedBy: z.string().min(1).nullable().default(null),
|
|
273
|
-
|
|
630
|
+
message: z.string().nullable().default(null),
|
|
631
|
+
status: inviteStatusSchema,
|
|
632
|
+
// Unix seconds the accept LINK stops minting (re-mail extends it).
|
|
633
|
+
expiresAt: z.number().int().nonnegative(),
|
|
274
634
|
acceptedAt: z.string().min(1).nullable().default(null),
|
|
275
|
-
revokedAt: z.string().min(1).nullable().default(null),
|
|
276
635
|
createdAt: z.string().min(1),
|
|
636
|
+
// Operational observability (X-28): honest null until the Views ingestion.
|
|
637
|
+
lastViewedAt: z.string().min(1).nullable().default(null),
|
|
277
638
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
expiresAt: z.string().min(1).optional(),
|
|
288
|
-
label: z.string().max(120).optional(),
|
|
289
|
-
});
|
|
290
|
-
// A share link as surfaced to owners. `url` (the token-bearing URL) is returned
|
|
291
|
-
// ONLY on the create response (the runtime trades `?stattic_share=` for a
|
|
292
|
-
// cookie once); list/read responses omit it and show only the lifecycle. `id`
|
|
293
|
-
// is `lnk_…`; `grant` is `link:{id}`.
|
|
294
|
-
export const shareLinkSchema = z.object({
|
|
639
|
+
export const spaceInviteCreateSchema = z.object({
|
|
640
|
+
email: z.string().min(3).max(320),
|
|
641
|
+
message: z.string().max(ACCESS_MESSAGE_MAX_LENGTH).optional(),
|
|
642
|
+
});
|
|
643
|
+
export const ACCESS_REQUEST_STATUSES = ["pending", "approved", "denied"];
|
|
644
|
+
export const accessRequestStatusSchema = z.enum(ACCESS_REQUEST_STATUSES);
|
|
645
|
+
// An access request (§5.4): a signed-in-but-unsatisfied visitor asking to be
|
|
646
|
+
// let in from the 403 page. Coalesced on (spaceId, email) while pending.
|
|
647
|
+
export const accessRequestSchema = z.object({
|
|
295
648
|
id: z.string().min(1),
|
|
296
649
|
spaceId: z.string().min(1),
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
650
|
+
email: z.string().min(1),
|
|
651
|
+
userId: z.string().min(1).nullable().default(null),
|
|
652
|
+
host: z.string().min(1),
|
|
653
|
+
path: z.string().min(1),
|
|
654
|
+
message: z.string().nullable().default(null),
|
|
655
|
+
status: accessRequestStatusSchema,
|
|
656
|
+
resolvedBy: z.string().min(1).nullable().default(null),
|
|
657
|
+
resolvedAt: z.string().min(1).nullable().default(null),
|
|
658
|
+
createdAt: z.string().min(1),
|
|
659
|
+
});
|
|
660
|
+
// POST /v1/access/requests — the 403-page form's target. Identity (email +
|
|
661
|
+
// userId) comes from the SESSION, never the body: the authorize chain
|
|
662
|
+
// guarantees requesters are identified (anonymous → 401).
|
|
663
|
+
export const accessRequestCreateSchema = z.object({
|
|
664
|
+
space: z.string().min(1),
|
|
665
|
+
host: z.string().min(1).max(255),
|
|
666
|
+
path: z.string().max(1024).default("/"),
|
|
667
|
+
nonce: z.string().min(1).max(2048).optional(),
|
|
668
|
+
message: z.string().max(ACCESS_MESSAGE_MAX_LENGTH).optional(),
|
|
669
|
+
});
|
|
670
|
+
// POST …/requests/:id/approve — the chooser (§5.4): "invite" reuses the §5.3
|
|
671
|
+
// magic-link invite; "team" is the Better-Auth team invitation.
|
|
672
|
+
export const accessRequestApproveSchema = z.object({
|
|
673
|
+
as: z.enum(["invite", "team"]).default("invite"),
|
|
674
|
+
});
|
|
675
|
+
// File-mode sharing (§5.1 files-mode merge): the dashboard-side file-share
|
|
676
|
+
// rule compiler is dead; file shares are a server-side intent surface owning
|
|
677
|
+
// the `file_share` managed block. A record is keyed by the file/folder PATH;
|
|
678
|
+
// the server expands the serving-path aliases (exact, directory index,
|
|
679
|
+
// extensionless .html variant) at compile time.
|
|
680
|
+
export const FILE_SHARE_AUDIENCES = ["public", "people"];
|
|
681
|
+
export const fileShareAudienceSchema = z.enum(FILE_SHARE_AUDIENCES);
|
|
682
|
+
// The projected file-share record (read shape on the sharing projection).
|
|
683
|
+
export const fileShareSchema = z.object({
|
|
684
|
+
// Canonical absolute file path ("/report/deck.pdf") or folder path ("/report/").
|
|
685
|
+
path: z.string().min(1),
|
|
686
|
+
audience: fileShareAudienceSchema,
|
|
687
|
+
// The email: grants admitted when audience = "people"; empty for "public".
|
|
688
|
+
grants: z.array(grantSchema).default([]),
|
|
689
|
+
// The compiled serving-path alias patterns the record covers (diagnostic).
|
|
690
|
+
patterns: z.array(z.string().min(1)).default([]),
|
|
691
|
+
});
|
|
692
|
+
// PUT /v1/spaces/:id/file-shares — upsert the record for one path. `grants`
|
|
693
|
+
// accepts raw emails or `email:` grants; the server normalizes + validates.
|
|
694
|
+
export const fileShareWriteSchema = z.object({
|
|
695
|
+
path: z.string().min(1).max(1024),
|
|
696
|
+
audience: fileShareAudienceSchema,
|
|
697
|
+
grants: z.array(z.string().min(1)).max(100).optional(),
|
|
698
|
+
});
|
|
699
|
+
// A pending access request row on the sharing projection (§5.4). `host`/`path`
|
|
700
|
+
// say where the requester was headed so the admin has context.
|
|
701
|
+
export const sharingRequestSchema = z.object({
|
|
702
|
+
id: z.string().min(1),
|
|
703
|
+
email: z.string().min(1),
|
|
704
|
+
message: z.string().nullable().default(null),
|
|
705
|
+
createdAt: z.string().min(1),
|
|
706
|
+
host: z.string().min(1).nullable().default(null),
|
|
707
|
+
path: z.string().min(1).nullable().default(null),
|
|
708
|
+
});
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
// 3d''. Service tokens (access-plan §4.4/§5.5 — machine principals)
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// Service tokens are the machine-principal leg of the IAM table (§5.5):
|
|
713
|
+
// `stk_` rows, Bearer-only, shown ONCE on create, `svc:{id}` grants compiled
|
|
714
|
+
// wherever the token's `actsAs` satisfies, structural revocation (X-7 —
|
|
715
|
+
// revoke recompiles the grant out; the credential's TTL is irrelevant).
|
|
716
|
+
//
|
|
717
|
+
// The secret IS a platform-signed visitor JWT ({sub: "svc:stk_x", grants:
|
|
718
|
+
// ["svc:stk_x"], exp: expiresAt | now+10y}) — the runtime's Bearer path
|
|
719
|
+
// already verifies visitor JWTs before the cookie (§3.2), so CI/agents fetch
|
|
720
|
+
// protected artifacts with `Authorization: Bearer <token>` and ZERO runtime
|
|
721
|
+
// work. Only the token's sha256 is stored (lastUsedAt attribution/display);
|
|
722
|
+
// the plaintext never persists.
|
|
723
|
+
//
|
|
724
|
+
// `sv` posture (documented, accepted): a space-scoped token pins the space's
|
|
725
|
+
// sessionVersion at mint; a team-wide token carries sv 0 (the absent-sv
|
|
726
|
+
// default). A space "logout-all" (sv bump) therefore ALSO rotates service
|
|
727
|
+
// access to that space — re-create the token. Arguably correct: logout-all
|
|
728
|
+
// means "everyone out", and service tokens are visitors too.
|
|
729
|
+
export const SERVICE_TOKEN_DEFAULT_TTL_SECONDS = 10 * 365 * 24 * 60 * 60; // 10 years
|
|
730
|
+
// The audience classes a service token can act as. v1 has exactly one:
|
|
731
|
+
// "viewers" — the token satisfies every managed rule that admits the
|
|
732
|
+
// team-viewer audience (the sharing block's general rule in `link`/`people`
|
|
733
|
+
// modes and file-share `people` rules). It does NOT satisfy a password wall
|
|
734
|
+
// (`pw:` is password-only, like team members) or raw rules that don't name
|
|
735
|
+
// `svc:` grants.
|
|
736
|
+
export const SERVICE_TOKEN_AUDIENCES = ["viewers"];
|
|
737
|
+
export const serviceTokenAudienceSchema = z.enum(SERVICE_TOKEN_AUDIENCES);
|
|
738
|
+
export const serviceTokenActsAsSchema = z.object({
|
|
739
|
+
audiences: z.array(serviceTokenAudienceSchema).min(1).default(["viewers"]),
|
|
740
|
+
});
|
|
741
|
+
// A service token projected onto the wire — NEVER carries the secret; the
|
|
742
|
+
// signed JWT appears exactly once, on the create response.
|
|
743
|
+
export const serviceTokenSchema = z.object({
|
|
744
|
+
id: z.string().min(1),
|
|
745
|
+
teamId: z.string().min(1),
|
|
746
|
+
// null = team-wide (the grant compiles into every team space).
|
|
747
|
+
spaceId: z.string().min(1).nullable().default(null),
|
|
748
|
+
name: z.string().min(1),
|
|
749
|
+
actsAs: serviceTokenActsAsSchema,
|
|
750
|
+
// Unix seconds; null = the 10-year default ceiling applies.
|
|
751
|
+
expiresAt: z.number().int().nonnegative().nullable().default(null),
|
|
752
|
+
// Operational observability (X-28, mirroring share links): fed by the
|
|
753
|
+
// runtime access-event ingestion; honest null until that reader lands.
|
|
754
|
+
lastUsedAt: z.string().min(1).nullable().default(null),
|
|
305
755
|
createdAt: z.string().min(1),
|
|
756
|
+
// false once revoked or past expiry — the grant is no longer compiled.
|
|
757
|
+
active: z.boolean(),
|
|
758
|
+
});
|
|
759
|
+
export const serviceTokenCreateSchema = z.object({
|
|
760
|
+
name: z.string().min(1).max(200),
|
|
761
|
+
// Scope the grant to one space; omitted = team-wide.
|
|
762
|
+
spaceId: z.string().min(1).optional(),
|
|
763
|
+
// Unix seconds; omitted = the 10-year default. Revocation stays structural
|
|
764
|
+
// either way — expiry is a hygiene ceiling, not the revocation mechanism.
|
|
765
|
+
expiresAt: z.number().int().positive().optional(),
|
|
766
|
+
actsAs: serviceTokenActsAsSchema.optional(),
|
|
767
|
+
});
|
|
768
|
+
// Returned ONLY on create (no-store): the signed Bearer JWT, shown once.
|
|
769
|
+
export const serviceTokenWithSecretSchema = serviceTokenSchema.extend({
|
|
770
|
+
token: z.string().min(1),
|
|
771
|
+
});
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
// 3d'''. The firewall card document (access-plan §5.6, X-15)
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
775
|
+
// Firewall is rows, not a subsystem (X-15): this document is the SIMPLE
|
|
776
|
+
// altitude — a server-side intent surface that compiles to plain anonymous
|
|
777
|
+
// (deny) or team-viewer challenge ("must sign in") rules in the `firewall`
|
|
778
|
+
// managed block, ordered before `sharing` so a block always beats a link.
|
|
779
|
+
// The projection is DERIVED from the block (X-33-style); rules in the block
|
|
780
|
+
// the projection doesn't recognize flip `custom` and the card sends the user
|
|
781
|
+
// to the raw-rules editor — never to a feature request.
|
|
782
|
+
export const FIREWALL_EFFECTS = ["deny", "challenge"];
|
|
783
|
+
export const firewallEffectSchema = z.enum(FIREWALL_EFFECTS);
|
|
784
|
+
// Country section: `block` denies/challenges the listed countries; `allow`
|
|
785
|
+
// admits ONLY the listed countries (the compiler enumerates the complement —
|
|
786
|
+
// rule `match.country` is an exact ISO code with no negation, so allow-mode
|
|
787
|
+
// is expressed as one rule per non-listed country).
|
|
788
|
+
export const firewallCountriesSchema = z.object({
|
|
789
|
+
mode: z.enum(["block", "allow"]),
|
|
790
|
+
// ISO 3166-1 alpha-2, uppercased server-side.
|
|
791
|
+
list: z.array(z.string().length(2)).min(1).max(250),
|
|
792
|
+
// deny = blocked outright; challenge = "visitors from these countries must
|
|
793
|
+
// sign in" (the team-viewer audience per §5.6).
|
|
794
|
+
effect: firewallEffectSchema.default("deny"),
|
|
795
|
+
});
|
|
796
|
+
export const firewallIpEntrySchema = z.object({
|
|
797
|
+
// CIDR ("203.0.113.0/24", v4 or v6) or a bare IP (exact match).
|
|
798
|
+
cidr: z.string().min(1).max(64),
|
|
799
|
+
effect: firewallEffectSchema.default("deny"),
|
|
800
|
+
});
|
|
801
|
+
export const firewallBotEntrySchema = z.object({
|
|
802
|
+
// Case-insensitive user-agent substring (the runtime's `agent` matcher).
|
|
803
|
+
agent: z.string().min(1).max(200),
|
|
804
|
+
effect: firewallEffectSchema.default("deny"),
|
|
805
|
+
});
|
|
806
|
+
// PUT /v1/spaces/:id/access/firewall — replaces the recognized firewall-card
|
|
807
|
+
// rules; unrecognized (raw-authored… impossible, managedBy is server-only —
|
|
808
|
+
// i.e. legacy/operator-written) firewall-block rules are PRESERVED and keep
|
|
809
|
+
// the projection's `custom` flag on.
|
|
810
|
+
export const firewallWriteSchema = z.object({
|
|
811
|
+
countries: firewallCountriesSchema.nullable().default(null),
|
|
812
|
+
ipCidrs: z.array(firewallIpEntrySchema).max(50).default([]),
|
|
813
|
+
bots: z.array(firewallBotEntrySchema).max(50).default([]),
|
|
814
|
+
});
|
|
815
|
+
// GET /v1/spaces/:id/access/firewall — the projection derived from the block.
|
|
816
|
+
export const spaceFirewallSchema = z.object({
|
|
817
|
+
countries: firewallCountriesSchema.nullable().default(null),
|
|
818
|
+
ipCidrs: z.array(firewallIpEntrySchema).default([]),
|
|
819
|
+
bots: z.array(firewallBotEntrySchema).default([]),
|
|
820
|
+
// True when the firewall block holds rules this document can't express —
|
|
821
|
+
// the card links to the raw-rules editor instead of lying.
|
|
822
|
+
custom: z.boolean().default(false),
|
|
823
|
+
});
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
// 3d''''. Team preview-lock (access-plan §4.2/X-24 flagship default)
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
// "Non-live channels are team-only", configured once for a whole team. The
|
|
828
|
+
// rule `match.channel` is an exact name with no negation, so NOT-live is
|
|
829
|
+
// expressed structurally inside the team_default block (evaluated LAST in the
|
|
830
|
+
// cloud lane): an anonymous allow pinned to the live channel, then a
|
|
831
|
+
// catch-all team-viewer challenge. Live requests terminate on the allow
|
|
832
|
+
// (identical to the no-rule-matches default); anything on another channel
|
|
833
|
+
// falls through to the lock. Because the block is last, every explicit space
|
|
834
|
+
// rule still wins first-match before either rule.
|
|
835
|
+
//
|
|
836
|
+
// These rules are canonical vocabulary (single-homed here): the dashboard
|
|
837
|
+
// toggle and the CLI post exactly these rows through the EXISTING
|
|
838
|
+
// PUT /v1/teams/:id/access/defaults — no second compiler exists.
|
|
839
|
+
export const TEAM_PREVIEW_LOCK_LIVE_RULE_ID = "team-preview-lock:live";
|
|
840
|
+
export const TEAM_PREVIEW_LOCK_RULE_ID = "team-preview-lock";
|
|
841
|
+
export function teamPreviewLockRules(teamId) {
|
|
842
|
+
return [
|
|
843
|
+
{
|
|
844
|
+
id: TEAM_PREVIEW_LOCK_LIVE_RULE_ID,
|
|
845
|
+
match: { channel: "live" },
|
|
846
|
+
effect: "allow",
|
|
847
|
+
reasonCode: "team_preview_lock",
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
id: TEAM_PREVIEW_LOCK_RULE_ID,
|
|
851
|
+
match: {},
|
|
852
|
+
effect: "challenge",
|
|
853
|
+
auth: { requiredGrants: [`team:${teamId}:viewer`] },
|
|
854
|
+
reasonCode: "team_preview_lock",
|
|
855
|
+
message: "Non-live channels on this team are team-only.",
|
|
856
|
+
},
|
|
857
|
+
];
|
|
858
|
+
}
|
|
859
|
+
export function isTeamPreviewLockRule(rule) {
|
|
860
|
+
return rule.id === TEAM_PREVIEW_LOCK_LIVE_RULE_ID || rule.id === TEAM_PREVIEW_LOCK_RULE_ID;
|
|
861
|
+
}
|
|
862
|
+
// Whether a team-defaults rule list carries the preview lock.
|
|
863
|
+
export function teamPreviewLockActive(rules) {
|
|
864
|
+
return rules.some((rule) => rule.id === TEAM_PREVIEW_LOCK_RULE_ID);
|
|
865
|
+
}
|
|
866
|
+
// Toggle helper: returns the defaults list with the lock applied/removed,
|
|
867
|
+
// preserving every other default rule (lock rules ride LAST so other
|
|
868
|
+
// defaults keep first-match precedence within the block).
|
|
869
|
+
export function withTeamPreviewLock(rules, teamId, enabled) {
|
|
870
|
+
const kept = rules.filter((rule) => !isTeamPreviewLockRule(rule));
|
|
871
|
+
return enabled ? [...kept, ...teamPreviewLockRules(teamId)] : kept;
|
|
872
|
+
}
|
|
873
|
+
// GET /v1/spaces/:id/sharing — the server-owned projection (X-33): the
|
|
874
|
+
// dashboard renders this and posts intents; it never interprets or compiles
|
|
875
|
+
// rules.
|
|
876
|
+
export const spaceSharingSchema = z.object({
|
|
877
|
+
generalAccess: z.object({
|
|
878
|
+
mode: generalAccessModeSchema,
|
|
879
|
+
// Whether a stored space password exists (the verifier hash, never the
|
|
880
|
+
// password) — lets the modal say "keep existing password".
|
|
881
|
+
hasPassword: z.boolean(),
|
|
882
|
+
}),
|
|
883
|
+
people: z.array(sharingPersonSchema).default([]),
|
|
884
|
+
// Connection audiences on the People-only state (§6.2/§6.3), derived from
|
|
885
|
+
// the compiled sharing block.
|
|
886
|
+
audiences: z.array(sharingAudienceSchema).default([]),
|
|
887
|
+
links: z.array(shareLinkSchema).default([]),
|
|
888
|
+
// Per-path file shares (the `file_share` managed block projected back).
|
|
889
|
+
fileShares: z.array(fileShareSchema).default([]),
|
|
890
|
+
requests: z.array(sharingRequestSchema).default([]),
|
|
891
|
+
// True when rules OUTSIDE the sharing block gate the space (unmanaged cloud
|
|
892
|
+
// rules or file-lane rules): the modal shows the "custom access rules
|
|
893
|
+
// active" chip linking to the access settings.
|
|
894
|
+
customRulesActive: z.boolean(),
|
|
895
|
+
// Plan-downgrade marker (X-23), mirrored from the access lane read.
|
|
896
|
+
accessExceedsPlan: z.boolean().default(false),
|
|
306
897
|
});
|
|
307
898
|
// ---------------------------------------------------------------------------
|
|
308
899
|
// 4. Lanes / nouns (additive types only — not wired)
|
|
@@ -335,6 +926,12 @@ export const servingConfigSchema = z.object({
|
|
|
335
926
|
// the firewall+access lane (own version + rollback, independent of content).
|
|
336
927
|
export const policyDocSchema = z.object({
|
|
337
928
|
rules: z.array(runtimeRuleSchema).default([]),
|
|
929
|
+
// Space-wide verifier issuers used by first-party access surfaces
|
|
930
|
+
// (callback/me/token), including public spaces that have no auth rules.
|
|
931
|
+
issuers: z.array(accessKeySchema).optional(),
|
|
932
|
+
// Runtime-serving session version. The enforcer rejects visitor tokens whose
|
|
933
|
+
// `sv` claim does not match and derives local `pw:` keys from this value.
|
|
934
|
+
sessionVersion: z.number().int().nonnegative().optional(),
|
|
338
935
|
});
|
|
339
936
|
// ---------------------------------------------------------------------------
|
|
340
937
|
// 5. Storage + resolution shapes (additive)
|
|
@@ -385,12 +982,38 @@ export const effectiveRuleSchema = runtimeRuleSchema.extend({
|
|
|
385
982
|
// rules were compiled from (null when the space has no compiled file rules).
|
|
386
983
|
export const effectivePolicySchema = z.object({
|
|
387
984
|
rules: z.array(effectiveRuleSchema).default([]),
|
|
985
|
+
// Space-wide verifier issuers. The control plane always emits the platform
|
|
986
|
+
// issuer here so identity-only public-space tokens verify without a rule.
|
|
987
|
+
issuers: z.array(accessKeySchema).default([]),
|
|
388
988
|
cloudVersion: z.number().int().nonnegative().default(0),
|
|
389
989
|
fileVersion: z.string().min(1).nullable().default(null),
|
|
390
990
|
// Serving-secret map (name -> value) the resolved rules reference by
|
|
391
|
-
// `
|
|
392
|
-
//
|
|
991
|
+
// password-acquire `ref` = "secret:<name>". Carries the space password's
|
|
992
|
+
// bcrypt verifier hash, keyed so the runtime resolves it from
|
|
393
993
|
// `serving['secrets']` and `password_verify`s against it. Never contains
|
|
394
994
|
// plaintext the visitor types — only stored verifier hashes / shared secrets.
|
|
395
995
|
secrets: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
996
|
+
// Space session version (access-plan §3.1/§4.4): visitor tokens must carry a
|
|
997
|
+
// matching `sv` claim. Bumped by logout-all; also an input to the space-local
|
|
998
|
+
// `pw:` key derivation, so a bump invalidates every wall pass with zero new
|
|
999
|
+
// state.
|
|
1000
|
+
sessionVersion: z.number().int().nonnegative().default(0),
|
|
1001
|
+
});
|
|
1002
|
+
// ---------------------------------------------------------------------------
|
|
1003
|
+
// 6. Simulate (access-plan §5.5, X-13)
|
|
1004
|
+
// ---------------------------------------------------------------------------
|
|
1005
|
+
// GET /v1/spaces/:id/access/simulate?host&path[&email&ip&country] → the rule
|
|
1006
|
+
// that would fire and why. Runs on the parity-pinned TS matcher, so simulate
|
|
1007
|
+
// correctness is a corollary of the corpus.
|
|
1008
|
+
export const accessSimulateResultSchema = z.object({
|
|
1009
|
+
verdict: z.enum(["allow", "deny", "challenge"]),
|
|
1010
|
+
// The first effective rule (with lane/source + order) whose match applies
|
|
1011
|
+
// and whose effect resolves for the simulated identity; null = no rule
|
|
1012
|
+
// gates the request (public).
|
|
1013
|
+
matchedRule: effectiveRuleSchema.nullable().default(null),
|
|
1014
|
+
// The matched rule's requiredGrants — which grants would satisfy it.
|
|
1015
|
+
why: z.array(grantSchema).default([]),
|
|
1016
|
+
// Human audience summary derived from requiredGrants ("Team members",
|
|
1017
|
+
// "Anyone with an active share link", …).
|
|
1018
|
+
whoWouldPass: z.array(z.string()).default([]),
|
|
396
1019
|
});
|