@spacefast/common 0.0.12 → 0.0.17
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/client-registry.d.ts +22 -0
- package/dist/agents/client-registry.js +41 -0
- package/dist/agents/connect-targets.d.ts +51 -6
- package/dist/agents/connect-targets.js +93 -28
- package/dist/brand.d.ts +0 -11
- package/dist/brand.js +0 -20
- package/dist/config/domains.d.ts +15 -6
- package/dist/config/domains.js +24 -10
- package/dist/config/postgres-budget.d.ts +3 -0
- package/dist/config/postgres-budget.js +6 -1
- package/dist/config/postgres-budget.json +2 -1
- package/dist/contracts/abuse.js +1 -1
- package/dist/contracts/access.d.ts +486 -1196
- package/dist/contracts/access.js +323 -1054
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -15
- package/dist/contracts/api-keys.d.ts +16 -0
- package/dist/contracts/api-keys.js +11 -9
- package/dist/contracts/archives.d.ts +1 -2
- package/dist/contracts/archives.js +15 -17
- package/dist/contracts/beta.js +1 -1
- package/dist/contracts/builds.js +14 -14
- package/dist/contracts/channels.js +2 -2
- package/dist/contracts/comments.d.ts +143 -99
- package/dist/contracts/comments.js +194 -79
- package/dist/contracts/common.d.ts +12 -12
- package/dist/contracts/common.js +33 -30
- package/dist/contracts/continuation.js +5 -5
- package/dist/contracts/countries.js +2 -7
- package/dist/contracts/data-source-routes.d.ts +20 -0
- package/dist/contracts/data-source-routes.js +15 -0
- package/dist/contracts/device-auth.d.ts +13 -9
- package/dist/contracts/device-auth.js +12 -12
- package/dist/contracts/docs.d.ts +10 -10
- package/dist/contracts/domains.js +39 -33
- package/dist/contracts/enums.d.ts +4 -14
- package/dist/contracts/enums.js +3 -10
- package/dist/contracts/error-code-meta.d.ts +122 -56
- package/dist/contracts/error-code-meta.js +43 -19
- package/dist/contracts/error-codes.d.ts +7 -4
- package/dist/contracts/error-codes.js +48 -24
- package/dist/contracts/events.d.ts +22 -15
- package/dist/contracts/events.js +33 -5
- package/dist/contracts/execution.d.ts +238 -0
- package/dist/contracts/execution.js +309 -0
- package/dist/contracts/feature-lifecycle.d.ts +39 -188
- package/dist/contracts/feature-lifecycle.js +52 -196
- package/dist/contracts/features.d.ts +13 -115
- package/dist/contracts/features.js +5 -35
- package/dist/contracts/functions.d.ts +597 -0
- package/dist/contracts/functions.js +475 -0
- package/dist/contracts/grants.d.ts +1175 -0
- package/dist/contracts/grants.js +783 -0
- package/dist/contracts/ids.d.ts +7 -5
- package/dist/contracts/ids.js +7 -7
- package/dist/contracts/internal.js +2 -2
- package/dist/contracts/mcp.d.ts +17 -556
- package/dist/contracts/mcp.js +9 -103
- package/dist/contracts/me.js +3 -4
- package/dist/contracts/notifications.d.ts +108 -0
- package/dist/contracts/notifications.js +54 -0
- package/dist/contracts/oauth-resources.d.ts +22 -5
- package/dist/contracts/oauth-resources.js +23 -2
- package/dist/contracts/operations.d.ts +0 -48
- package/dist/contracts/operations.js +0 -19
- package/dist/contracts/pages.d.ts +10 -14
- package/dist/contracts/pages.js +17 -21
- package/dist/contracts/plan-policy.d.ts +1 -8
- package/dist/contracts/plan-policy.js +1 -4
- package/dist/contracts/platform.d.ts +78 -30
- package/dist/contracts/platform.js +69 -20
- package/dist/contracts/problem-document.d.ts +14 -0
- package/dist/contracts/problem-document.js +35 -0
- package/dist/contracts/push-new.d.ts +41 -2
- package/dist/contracts/push-new.js +4 -0
- package/dist/contracts/quotas.d.ts +0 -1
- package/dist/contracts/quotas.js +0 -5
- package/dist/contracts/repository-connections.d.ts +1 -1
- package/dist/contracts/resources.d.ts +16 -71
- package/dist/contracts/resources.js +13 -16
- package/dist/contracts/runtime-api.d.ts +666 -303
- package/dist/contracts/runtime-api.js +272 -113
- package/dist/contracts/runtime-app.d.ts +168 -0
- package/dist/contracts/runtime-app.js +42 -0
- package/dist/contracts/runtime-db.d.ts +424 -0
- package/dist/contracts/runtime-db.js +108 -0
- package/dist/contracts/runtime-storage.d.ts +120 -0
- package/dist/contracts/runtime-storage.js +40 -0
- package/dist/contracts/sf-config-v1.d.ts +54 -1
- package/dist/contracts/sf-config-v1.js +2 -2
- package/dist/contracts/sites.d.ts +6 -0
- package/dist/contracts/sites.js +30 -11
- package/dist/contracts/space-config.d.ts +91 -121
- package/dist/contracts/space-config.js +464 -42
- package/dist/contracts/spaces.d.ts +300 -171
- package/dist/contracts/spaces.js +128 -98
- package/dist/contracts/superadmin-emails.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +26 -130
- package/dist/contracts/superadmin-spaces.js +22 -0
- package/dist/contracts/superadmin-tenants.js +4 -4
- package/dist/contracts/superadmin.d.ts +4 -0
- package/dist/contracts/superadmin.js +14 -1
- package/dist/contracts/tags.d.ts +6 -6
- package/dist/contracts/tags.js +1 -1
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/teams.js +25 -16
- package/dist/contracts/theme-json.js +1 -1
- package/dist/contracts/transfers.d.ts +0 -1
- package/dist/contracts/transfers.js +4 -4
- package/dist/contracts/variables.js +3 -3
- package/dist/contracts/zero.d.ts +384 -596
- package/dist/contracts/zero.js +167 -299
- package/dist/dashboard-paths/index.d.ts +18 -3
- package/dist/dashboard-paths/index.js +19 -0
- package/dist/docs/agent-handoff-document.d.ts +2 -2
- package/dist/docs/agent-handoff-document.js +8 -7
- package/dist/docs/agent-prose.d.ts +22 -3
- package/dist/docs/agent-prose.js +227 -55
- package/dist/docs/agent-setup.d.ts +14 -11
- package/dist/docs/agent-setup.js +289 -61
- package/dist/docs/agent-solutions.d.ts +6 -6
- package/dist/docs/agent-solutions.js +24 -22
- package/dist/docs/catalog.d.ts +53 -98
- package/dist/docs/catalog.js +90 -117
- package/dist/docs/error-docs.js +304 -236
- package/dist/docs/skill-distribution.d.ts +2 -0
- package/dist/docs/skill-distribution.js +7 -4
- package/dist/docs/skills.d.ts +546 -0
- package/dist/docs/skills.js +340 -0
- package/dist/utils/auth-redirect.d.ts +1 -3
- package/dist/utils/auth-redirect.js +1 -21
- package/dist/utils/browser-credential.d.ts +19 -0
- package/dist/utils/browser-credential.js +62 -0
- package/dist/utils/build-settings.d.ts +2 -0
- package/dist/utils/build-settings.js +61 -9
- package/dist/utils/comment-avatars.d.ts +7 -0
- package/dist/utils/comment-avatars.js +42 -0
- package/dist/utils/content-type.d.ts +3 -3
- package/dist/utils/content-type.js +38 -61
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -0
- package/dist/utils/domain-diagnostics.d.ts +5 -5
- package/dist/utils/domain-diagnostics.js +21 -10
- package/dist/utils/egress-policy.fixtures.json +1 -1
- package/dist/utils/grants.d.ts +53 -0
- package/dist/utils/grants.js +160 -0
- package/dist/utils/local-space-state.d.ts +78 -4
- package/dist/utils/local-space-state.js +384 -82
- package/dist/utils/one-shot-replay.d.ts +14 -0
- package/dist/utils/one-shot-replay.js +21 -0
- package/dist/utils/page-preview.d.ts +7 -0
- package/dist/utils/page-preview.js +9 -0
- package/dist/utils/pages.d.ts +2 -5
- package/dist/utils/pages.js +64 -33
- package/dist/utils/publish-form-data.d.ts +22 -0
- package/dist/utils/publish-form-data.js +26 -0
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +149 -62
- package/dist/utils/query-keys.d.ts +8 -10
- package/dist/utils/query-keys.js +14 -10
- package/dist/utils/runtime-paths.d.ts +3 -0
- package/dist/utils/runtime-paths.js +7 -0
- package/dist/utils/secure-local-file.d.ts +73 -0
- package/dist/utils/secure-local-file.js +1373 -0
- package/dist/utils/space-config.d.ts +17 -4
- package/dist/utils/space-config.js +151 -8
- package/dist/utils/static-runtime-policy.d.ts +5 -0
- package/dist/utils/static-runtime-policy.js +54 -10
- package/dist/utils/storage-policy.d.ts +20 -0
- package/dist/utils/storage-policy.fixtures.json +174 -0
- package/dist/utils/storage-policy.js +193 -0
- package/dist/utils/upload-session.d.ts +15 -1
- package/dist/utils/upload-session.js +68 -2
- package/dist/utils/version-file-browser.d.ts +13 -0
- package/dist/utils/version-file-browser.js +59 -0
- package/package.json +20 -1
- package/dist/contracts/mounts.d.ts +0 -142
- package/dist/contracts/mounts.js +0 -56
- package/dist/utils/access-grant.d.ts +0 -14
- package/dist/utils/access-grant.js +0 -88
- package/dist/utils/access-match.d.ts +0 -46
- package/dist/utils/access-match.fixtures.json +0 -513
- package/dist/utils/access-match.js +0 -312
- package/dist/utils/access-verdict.fixtures.json +0 -421
- package/dist/utils/asset-fingerprint.d.ts +0 -53
- package/dist/utils/asset-fingerprint.js +0 -574
- package/dist/utils/sf-config-v1.d.ts +0 -2
- package/dist/utils/sf-config-v1.fixtures.json +0 -9
- package/dist/utils/sf-config-v1.js +0 -551
package/dist/contracts/access.js
CHANGED
|
@@ -1,1091 +1,360 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
// Unified access contracts (internal-docs/access-plan.html — the only access
|
|
4
|
-
// document).
|
|
5
|
-
//
|
|
6
|
-
// The one access & policy lane. The headline simplification: **firewall ⊂
|
|
7
|
-
// access** — there is exactly one Rule schema, and an anonymous firewall rule
|
|
8
|
-
// is simply a Rule whose `auth` is absent. There is exactly ONE satisfaction
|
|
9
|
-
// test: token grants ∩ requiredGrants (X-32); `auth.acquire` only configures
|
|
10
|
-
// how the challenge page lets a visitor obtain a token. The runtime verifies
|
|
11
|
-
// fully offline; the cloud resolves identity and mints signed tokens (plus
|
|
12
|
-
// exactly one non-cloud minter: the runtime's space-local `pw:` mint). Field
|
|
13
|
-
// names are the spec contract — do not rename.
|
|
2
|
+
import { grantConstraintsPatchSchema, grantConstraintsSchema, grantResourceSetSchema, grantTargetSelectorSchema, } from "./grants.js";
|
|
14
3
|
// ---------------------------------------------------------------------------
|
|
15
|
-
//
|
|
4
|
+
// Canonical sharing model
|
|
16
5
|
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// email:alice@acme.com a single verified email
|
|
25
|
-
// email:*@acme.com any verified email on a domain (glob — this IS
|
|
26
|
-
// the "anyone at a domain" audience; there is no
|
|
27
|
-
// separate email-domain namespace)
|
|
28
|
-
// team:team_123:member top-level team membership at a role
|
|
29
|
-
// sub-team:sub_123:member enterprise group membership inside a team
|
|
30
|
-
// user:<handle> a specific platform person
|
|
31
|
-
// space:{spaceId}:{level} a leveled space grant (SPACE_LEVELS)
|
|
32
|
-
// link:lnk_x an active share link (structural revocation)
|
|
33
|
-
// invite:sin_x an accepted invite (structural revocation)
|
|
34
|
-
// svc:stk_x a service token (CI, agents; structural)
|
|
35
|
-
// sso:acn_x an OIDC connection identity
|
|
36
|
-
// ext:{connectionId}:<tag> an owner-defined external (BYO issuer) grant
|
|
37
|
-
// pw:{ruleId} a satisfied password wall (runtime-local mint —
|
|
38
|
-
// the ONLY namespace the space-local key signs)
|
|
39
|
-
//
|
|
40
|
-
// Namespaces exist iff a minter exists. Tokens signed by an external (BYO) key
|
|
41
|
-
// may only carry grants namespaced to that key (see
|
|
42
|
-
// `accessKeySchema.grantNamespaces`); everything else is dropped before
|
|
43
|
-
// intersection.
|
|
44
|
-
export const grantSchema = z.string().min(1).max(255);
|
|
45
|
-
// Known grant namespace prefixes — final vocabulary (access-plan §1).
|
|
46
|
-
export const GRANT_NAMESPACES = [
|
|
47
|
-
"email:",
|
|
48
|
-
"team:",
|
|
49
|
-
"sub-team:",
|
|
50
|
-
"user:",
|
|
51
|
-
"space:",
|
|
52
|
-
"link:",
|
|
53
|
-
"invite:",
|
|
54
|
-
"svc:",
|
|
55
|
-
"sso:",
|
|
56
|
-
"ext:",
|
|
57
|
-
"pw:",
|
|
58
|
-
];
|
|
59
|
-
const GRANT_PATTERN = /^(email|team|sub-team|user|space|link|invite|svc|sso|ext|pw):.+$/;
|
|
60
|
-
// Validator for grant globs like "email:*@acme.com", "team:team_123:member",
|
|
61
|
-
// "sub-team:sub_123:member", "user:<h>", "svc:…", "ext:{conn}:…". Returns true when the grant string is a
|
|
62
|
-
// well-formed `namespace:value`. This is advisory (the runtime intersects
|
|
63
|
-
// opaque strings); use it for authoring-surface validation, not enforcement.
|
|
64
|
-
export function isWellFormedGrant(grant) {
|
|
65
|
-
return GRANT_PATTERN.test(grant);
|
|
6
|
+
export const SPACE_ACCESS_PRESETS = ["private", "team", "public"];
|
|
7
|
+
export const ACCESS_HANDOFF_MEDIA_TYPE = "application/vnd.spacefast.access-handoff+json";
|
|
8
|
+
export const spaceAccessPresetSchema = z.enum(SPACE_ACCESS_PRESETS);
|
|
9
|
+
export const ACCESS_ROLES = ["viewer", "commenter", "editor", "manager"];
|
|
10
|
+
export const accessRoleSchema = z.enum(ACCESS_ROLES);
|
|
11
|
+
export function accessRoleRank(role) {
|
|
12
|
+
return ACCESS_ROLES.indexOf(role);
|
|
66
13
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// The ONE space-level vocabulary (access-plan X-38): consumed by
|
|
74
|
-
// `space:{id}:{level}` grants, Cast's capability table, invitee levels, and the
|
|
75
|
-
// future editor role. No parallel level enum may exist anywhere. v1 surfaces
|
|
76
|
-
// mint only `viewer` and `commenter`.
|
|
77
|
-
export const SPACE_LEVELS = ["viewer", "commenter", "editor", "admin"];
|
|
78
|
-
export const spaceLevelSchema = z.enum(SPACE_LEVELS);
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// 2. Issuer registry
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// A registered signing key. SSO + BYO-issuer keys are registered once at
|
|
83
|
-
// team/space level; rules reference grants, and a token's grants are dropped
|
|
84
|
-
// unless namespaced to one of `grantNamespaces`. Service tokens are just tokens
|
|
85
|
-
// carrying `svc:` grants signed by a platform key.
|
|
86
|
-
export const accessKeySchema = z.object({
|
|
87
|
-
kid: z.string().min(1).describe("Key id; matches the token header `kid`."),
|
|
88
|
-
alg: z.literal("EdDSA"),
|
|
89
|
-
publicKey: z.string().min(1).describe("base64url-encoded raw Ed25519 public key."),
|
|
90
|
-
// Grants in a token signed by this key are DROPPED unless prefixed by one of
|
|
91
|
-
// these. Platform keys list the platform prefixes
|
|
92
|
-
// (["team:","sub-team:","user:","email:","space:","link:","svc:","sso:"]);
|
|
93
|
-
// external keys list exactly ["ext:{connectionId}:"], bounding a leaked key's
|
|
94
|
-
// blast radius to the rules the owner pointed at it.
|
|
95
|
-
grantNamespaces: z.array(z.string().min(1)).min(1),
|
|
96
|
-
});
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// 2b. Identity connections (access-plan §4.4/§6.2/§6.3, X-18/X-19)
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// A team-level identity connection (`acn_` row): either a company OIDC
|
|
101
|
-
// relying-party config (§6.2 — the authorize endpoint runs the code flow and
|
|
102
|
-
// mints the visitor token; no Spacefast account is created) or an external
|
|
103
|
-
// BYO issuer (§6.3 — the shop holds the private key and mints its own
|
|
104
|
-
// Ed25519 tokens; Spacefast is not in the login path).
|
|
105
|
-
export const ACCESS_CONNECTION_TYPES = ["oidc", "external"];
|
|
106
|
-
export const accessConnectionTypeSchema = z.enum(ACCESS_CONNECTION_TYPES);
|
|
107
|
-
// Session TTL defaults (§4.4): 8h for company SSO (re-auth is a transparent
|
|
108
|
-
// bounce while the IdP session lives), 30min for external issuers (the short
|
|
109
|
-
// TTL bounds entitlement drift — a cancelled subscriber survives at most one
|
|
110
|
-
// TTL).
|
|
111
|
-
export const OIDC_CONNECTION_DEFAULT_TTL_SECONDS = 8 * 60 * 60; // 28800
|
|
112
|
-
export const EXTERNAL_CONNECTION_DEFAULT_TTL_SECONDS = 30 * 60; // 1800
|
|
113
|
-
// Hard ceiling on an EXTERNAL-issued token's exp (§6.3: exp ≤ 30min). The
|
|
114
|
-
// connection's sessionTtlSeconds may be shorter, never longer.
|
|
115
|
-
export const EXTERNAL_TOKEN_MAX_TTL_SECONDS = 30 * 60;
|
|
116
|
-
// The grant a company-SSO visitor carries (§6.2): `sso:{connectionId}`.
|
|
117
|
-
export function ssoGrant(connectionId) {
|
|
118
|
-
return `sso:${connectionId}`;
|
|
119
|
-
}
|
|
120
|
-
// The grant namespace an external connection's keys are bound to (§6.3):
|
|
121
|
-
// tokens signed by a shop key are dropped unless prefixed `ext:{acn}:` — a
|
|
122
|
-
// leaked shop key can forge exactly the rules the owner pointed at it, never
|
|
123
|
-
// team:/user: grants (corpus-pinned).
|
|
124
|
-
export function externalGrantNamespace(connectionId) {
|
|
125
|
-
return `ext:${connectionId}:`;
|
|
126
|
-
}
|
|
127
|
-
// An owner-defined external grant (§6.3): `ext:{acn}:{tag}`, e.g.
|
|
128
|
-
// `ext:acn_x:wc-subscriber`.
|
|
129
|
-
export function externalGrant(connectionId, tag) {
|
|
130
|
-
return `ext:${connectionId}:${tag}`;
|
|
131
|
-
}
|
|
132
|
-
// STORED form of a connection login acquire (`loginAcquireSchema.url`): the
|
|
133
|
-
// compiler stores the symbolic `connection:{acn}` reference and the resolver
|
|
134
|
-
// rewrites it to the live URL on every policy resolution (the OIDC leg URL or
|
|
135
|
-
// the shop's authorizeUrl with the CURRENT space/sv baked in). The serving
|
|
136
|
-
// runtime adds the actual request host and same-origin return path. Nothing
|
|
137
|
-
// stored ever carries a stale sessionVersion; a revoked connection's acquire
|
|
138
|
-
// resolves to nothing (the login block disappears — fail closed).
|
|
139
|
-
export function connectionAcquireRef(connectionId) {
|
|
140
|
-
return `connection:${connectionId}`;
|
|
14
|
+
export function maxAccessRole(left, right) {
|
|
15
|
+
if (left === null)
|
|
16
|
+
return right;
|
|
17
|
+
if (right === null)
|
|
18
|
+
return left;
|
|
19
|
+
return accessRoleRank(left) >= accessRoleRank(right) ? left : right;
|
|
141
20
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
21
|
+
function hasUnsafeScopeCharacter(value) {
|
|
22
|
+
for (const character of value) {
|
|
23
|
+
const codePoint = character.codePointAt(0);
|
|
24
|
+
if (codePoint === undefined ||
|
|
25
|
+
codePoint <= 0x1f ||
|
|
26
|
+
codePoint === 0x7f ||
|
|
27
|
+
character === "\\" ||
|
|
28
|
+
character === "?" ||
|
|
29
|
+
character === "#") {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
145
32
|
}
|
|
146
|
-
|
|
147
|
-
return id.length > 0 && !id.includes("/") && !id.includes(":") ? id : null;
|
|
33
|
+
return false;
|
|
148
34
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
ids.add(id);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
35
|
+
/**
|
|
36
|
+
* Canonicalizes the URL route identity used by People, Links, requests,
|
|
37
|
+
* comments, and runtime authorization.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeAccessScopePath(input) {
|
|
40
|
+
const value = input;
|
|
41
|
+
if (!value.startsWith("/") || value.startsWith("//") || hasUnsafeScopeCharacter(value)) {
|
|
42
|
+
throw new TypeError("scope must be a plain absolute URL path");
|
|
43
|
+
}
|
|
44
|
+
let decoded;
|
|
45
|
+
try {
|
|
46
|
+
decoded = decodeURIComponent(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new TypeError("scope contains malformed percent encoding");
|
|
167
50
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
51
|
+
if (/%2f|%5c/iu.test(value) || hasUnsafeScopeCharacter(decoded)) {
|
|
52
|
+
throw new TypeError("scope contains an ambiguous encoded separator");
|
|
53
|
+
}
|
|
54
|
+
const segments = [];
|
|
55
|
+
for (const rawSegment of decoded.split("/")) {
|
|
56
|
+
const segment = rawSegment.normalize("NFC");
|
|
57
|
+
if (segment === "" || segment === ".")
|
|
58
|
+
continue;
|
|
59
|
+
if (segment === "..") {
|
|
60
|
+
if (segments.length === 0) {
|
|
61
|
+
throw new TypeError("scope escapes the URL root");
|
|
173
62
|
}
|
|
63
|
+
segments.pop();
|
|
64
|
+
continue;
|
|
174
65
|
}
|
|
66
|
+
segments.push(segment);
|
|
67
|
+
}
|
|
68
|
+
if (segments.at(-1) === "index.html") {
|
|
69
|
+
segments.pop();
|
|
175
70
|
}
|
|
176
|
-
return
|
|
71
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
177
72
|
}
|
|
178
|
-
|
|
179
|
-
export const externalConnectionPublicKeySchema = z.object({
|
|
180
|
-
kid: z.string().min(1).max(64),
|
|
181
|
-
publicKey: z
|
|
182
|
-
.string()
|
|
183
|
-
.regex(/^[A-Za-z0-9_-]{43}$/, "publicKey must be a base64url raw 32-byte Ed25519 public key"),
|
|
184
|
-
});
|
|
185
|
-
const guestEmailDomainsSchema = z
|
|
186
|
-
.array(z
|
|
73
|
+
export const accessScopePathSchema = z
|
|
187
74
|
.string()
|
|
188
|
-
.min(
|
|
189
|
-
.max(
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
clientId: z.string().min(1).nullable().default(null),
|
|
202
|
-
clientSecretSet: z.boolean().default(false),
|
|
203
|
-
// External half (null for oidc connections). `authorizeUrl` is HTTPS-pinned
|
|
204
|
-
// at create and admin-only to mutate.
|
|
205
|
-
authorizeUrl: z.string().min(1).nullable().default(null),
|
|
206
|
-
publicKeys: z.array(externalConnectionPublicKeySchema).default([]),
|
|
207
|
-
sessionTtlSeconds: z.number().int().positive(),
|
|
208
|
-
// Verified-email domains auto-granted team-viewer at mint (§6.2); null = none.
|
|
209
|
-
guestEmailDomains: z.array(z.string().min(1)).nullable().default(null),
|
|
210
|
-
createdAt: z.string().min(1),
|
|
211
|
-
revokedAt: z.string().min(1).nullable().default(null),
|
|
212
|
-
active: z.boolean(),
|
|
213
|
-
});
|
|
214
|
-
export const oidcConnectionCreateSchema = z.object({
|
|
215
|
-
type: z.literal("oidc"),
|
|
216
|
-
name: z.string().min(1).max(200),
|
|
217
|
-
// The IdP issuer URL (HTTPS): discovery runs against
|
|
218
|
-
// `{issuer}/.well-known/openid-configuration`.
|
|
219
|
-
issuer: z.string().min(1).max(1024),
|
|
220
|
-
clientId: z.string().min(1).max(512),
|
|
221
|
-
clientSecret: z.string().min(1).max(2048),
|
|
222
|
-
sessionTtlSeconds: z
|
|
223
|
-
.number()
|
|
224
|
-
.int()
|
|
225
|
-
.min(60)
|
|
226
|
-
.max(24 * 60 * 60)
|
|
227
|
-
.optional(),
|
|
228
|
-
guestEmailDomains: guestEmailDomainsSchema.optional(),
|
|
229
|
-
});
|
|
230
|
-
export const externalConnectionCreateSchema = z.object({
|
|
231
|
-
type: z.literal("external"),
|
|
232
|
-
name: z.string().min(1).max(200),
|
|
233
|
-
// The shop's authorize endpoint (HTTPS-pinned): the challenge page's login
|
|
234
|
-
// link forwards straight here for single-audience external rules.
|
|
235
|
-
authorizeUrl: z.string().min(1).max(2048),
|
|
236
|
-
publicKeys: z.array(externalConnectionPublicKeySchema).min(1).max(5),
|
|
237
|
-
sessionTtlSeconds: z.number().int().min(60).max(EXTERNAL_TOKEN_MAX_TTL_SECONDS).optional(),
|
|
238
|
-
guestEmailDomains: guestEmailDomainsSchema.optional(),
|
|
239
|
-
});
|
|
240
|
-
export const accessConnectionCreateSchema = z.discriminatedUnion("type", [
|
|
241
|
-
oidcConnectionCreateSchema,
|
|
242
|
-
externalConnectionCreateSchema,
|
|
243
|
-
]);
|
|
244
|
-
// PATCH shape — fields are validated against the connection's type at the
|
|
245
|
-
// route (`authorizeUrl`/`publicKeys` are external-only; `issuer`/`clientId`/
|
|
246
|
-
// `clientSecret` are oidc-only). `publicKeys` REPLACES the key set (push-key
|
|
247
|
-
// rotation: push the new key alongside the old, flip signing, drop the old).
|
|
248
|
-
export const accessConnectionUpdateSchema = z.object({
|
|
249
|
-
name: z.string().min(1).max(200).optional(),
|
|
250
|
-
issuer: z.string().min(1).max(1024).optional(),
|
|
251
|
-
clientId: z.string().min(1).max(512).optional(),
|
|
252
|
-
clientSecret: z.string().min(1).max(2048).optional(),
|
|
253
|
-
authorizeUrl: z.string().min(1).max(2048).optional(),
|
|
254
|
-
publicKeys: z.array(externalConnectionPublicKeySchema).min(1).max(5).optional(),
|
|
255
|
-
sessionTtlSeconds: z
|
|
256
|
-
.number()
|
|
257
|
-
.int()
|
|
258
|
-
.min(60)
|
|
259
|
-
.max(24 * 60 * 60)
|
|
260
|
-
.optional(),
|
|
261
|
-
guestEmailDomains: guestEmailDomainsSchema.nullable().optional(),
|
|
262
|
-
});
|
|
263
|
-
// DELETE response: revocation is structural — rules referencing the
|
|
264
|
-
// connection lose those grants in the immediate recompile; rules left with an
|
|
265
|
-
// empty audience are disabled (removed) and listed so the owner knows exactly
|
|
266
|
-
// what stopped enforcing via that connection.
|
|
267
|
-
export const accessConnectionRevokeResultSchema = z.object({
|
|
268
|
-
connection: accessConnectionSchema,
|
|
269
|
-
disabledRules: z
|
|
270
|
-
.array(z.object({ spaceId: z.string().min(1), ruleId: z.string().min(1) }))
|
|
271
|
-
.default([]),
|
|
272
|
-
});
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
// 3. The unified runtime Rule (firewall = a rule with no auth)
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// The `auth` object (access-plan X-32): there is exactly ONE satisfaction
|
|
277
|
-
// test — token grants ∩ `requiredGrants`. `acquire` entries only configure how
|
|
278
|
-
// the challenge page lets an unsatisfied visitor OBTAIN a token:
|
|
279
|
-
//
|
|
280
|
-
// password → the challenge page renders a password form; a valid POST mints
|
|
281
|
-
// the standard visitor token RUNTIME-LOCALLY (space HS256 key,
|
|
282
|
-
// `pw:{ruleId}` grant) into the one `spacefast_access` cookie.
|
|
283
|
-
// `basic` transport is the stateless HTTP 401 variant — valid
|
|
284
|
-
// Basic credentials satisfy `pw:{ruleId}` per-request (browsers
|
|
285
|
-
// replay the Authorization header; no cookie needed).
|
|
286
|
-
// login → the challenge page renders a sign-in link into the cloud
|
|
287
|
-
// authorize chain (or a BYO issuer's authorizeUrl).
|
|
288
|
-
//
|
|
289
|
-
// Mixed audiences are just multiple acquire entries on one rule — the chooser
|
|
290
|
-
// is the challenge page rendering every acquire block, never a new surface.
|
|
291
|
-
export const passwordAcquireSchema = z.object({
|
|
292
|
-
type: z.literal("password"),
|
|
293
|
-
// A secret reference (e.g. "secret:space-password"), never plaintext. The
|
|
294
|
-
// runtime resolves it from serving secrets and `password_verify`s.
|
|
295
|
-
ref: z.string().min(1),
|
|
296
|
-
transport: z.enum(["basic", "form"]),
|
|
297
|
-
username: z.string().min(1).optional(),
|
|
298
|
-
});
|
|
299
|
-
export const loginAcquireSchema = z.object({
|
|
300
|
-
type: z.literal("login"),
|
|
301
|
-
// Interactive handoff target (the cloud authorize endpoint, or a BYO
|
|
302
|
-
// connection's authorizeUrl). The challenge page links only to the runtime's
|
|
303
|
-
// same-origin login endpoint; it never exposes this target directly. The
|
|
304
|
-
// compiler hydrates static identity intent, then the runtime sets `host=`
|
|
305
|
-
// from the actual serving request and `return=` as a plain same-origin path.
|
|
306
|
-
url: z.string().min(1),
|
|
307
|
-
// Display label for the challenge-page button (e.g. "Continue with
|
|
308
|
-
// Spacefast", "Sign in with Acme Shop").
|
|
309
|
-
label: z.string().min(1).optional(),
|
|
310
|
-
});
|
|
311
|
-
export const acquireSchema = z.discriminatedUnion("type", [
|
|
312
|
-
passwordAcquireSchema,
|
|
313
|
-
loginAcquireSchema,
|
|
314
|
-
]);
|
|
315
|
-
export const ruleAuthSchema = z.object({
|
|
316
|
-
// ANY-OF: the visitor's verified grants must intersect this set. Entries may
|
|
317
|
-
// be globs (e.g. "email:*@acme.com").
|
|
318
|
-
requiredGrants: z.array(grantSchema).min(1),
|
|
319
|
-
// Issuer keys whose tokens are accepted for this rule. When omitted, the
|
|
320
|
-
// compiler fills in the applicable platform/registered keys. Tokens signed by
|
|
321
|
-
// a key contribute only grants within that key's `grantNamespaces`.
|
|
322
|
-
issuers: z.array(accessKeySchema).optional(),
|
|
323
|
-
// How the challenge page lets a visitor get a token. Absent = no interactive
|
|
324
|
-
// acquisition (service-token-only rule): the runtime denies a browser rather
|
|
325
|
-
// than loop.
|
|
326
|
-
acquire: z.array(acquireSchema).optional(),
|
|
327
|
-
});
|
|
328
|
-
// Match conditions AND together (host AND path AND channel AND ipCidrs AND …).
|
|
329
|
-
// All fields optional; an empty match matches everything in scope.
|
|
330
|
-
export const ruleMatchSchema = z.object({
|
|
331
|
-
host: z.string().min(1).optional(),
|
|
332
|
-
hostPattern: z.string().min(1).optional(),
|
|
333
|
-
// `_headers` absolute host matcher template: ":branch.example.com" keeps
|
|
334
|
-
// placeholders single-label while "*" keeps the existing host wildcard shape.
|
|
335
|
-
hostTemplate: z.string().min(1).optional(),
|
|
336
|
-
// Glob path pattern: "/docs/**" | "/docs/*" | "/x.html".
|
|
337
|
-
pathPattern: z.string().min(1).optional(),
|
|
338
|
-
// Serving context channel name (e.g. "live", "staging").
|
|
339
|
-
channel: z.string().min(1).optional(),
|
|
340
|
-
// CIDR v4/v6 list; the request IP must fall in one of these.
|
|
341
|
-
ipCidrs: z.array(z.string().min(1)).max(50).optional(),
|
|
342
|
-
// User-agent matcher (e.g. block a crawler, gate an agent).
|
|
343
|
-
agent: z.string().min(1).optional(),
|
|
344
|
-
// ISO 3166-1 alpha-2 country code matcher.
|
|
345
|
-
country: z.string().min(2).max(2).optional(),
|
|
346
|
-
// Matches when the resolved country is NOT in this list. Unlike compiling
|
|
347
|
-
// an allowlist as the finite ISO complement, this also matches missing,
|
|
348
|
-
// provider-special, malformed, and future country values.
|
|
349
|
-
countryNotIn: z.array(z.string().min(2).max(2)).min(1).max(250).optional(),
|
|
350
|
-
// Single request-header matcher: name + expected value.
|
|
351
|
-
header: z.object({ name: z.string().min(1), value: z.string() }).optional(),
|
|
352
|
-
});
|
|
353
|
-
// Managed-block owners (access-plan X-34). Block ownership is THIS schema
|
|
354
|
-
// field, never a reasonCode convention: `managedBy` is stripped/rejected from
|
|
355
|
-
// all user input, so a raw rule can never claim or clobber a managed block.
|
|
356
|
-
// Cloud-lane block order: operator/incident rules first, then `firewall`, then
|
|
357
|
-
// `sharing`/`file_share`/`cast_reviewer`, then `team_default` last.
|
|
358
|
-
export const MANAGED_BY_VALUES = [
|
|
359
|
-
"sharing",
|
|
360
|
-
"firewall",
|
|
361
|
-
"file_share",
|
|
362
|
-
"cast_reviewer",
|
|
363
|
-
"team_default",
|
|
364
|
-
];
|
|
365
|
-
export const managedBySchema = z.enum(MANAGED_BY_VALUES);
|
|
366
|
-
// The one Rule. `match` (AND) selects requests; `effect` is what happens on
|
|
367
|
-
// match; `auth` (absent = anonymous firewall) gates `challenge`/`allow`.
|
|
368
|
-
export const runtimeRuleSchema = z.object({
|
|
369
|
-
// Stable rule id. Required wherever a grant references the rule
|
|
370
|
-
// (`pw:{ruleId}`); the compiler assigns ids to managed rules.
|
|
371
|
-
id: z.string().min(1).optional(),
|
|
372
|
-
match: ruleMatchSchema.default({}),
|
|
373
|
-
effect: z.enum(["allow", "deny", "challenge"]),
|
|
374
|
-
// Absent = anonymous firewall rule (no credential required). Present = pass
|
|
375
|
-
// when token grants ∩ requiredGrants ≠ ∅ (the one satisfaction test).
|
|
376
|
-
auth: ruleAuthSchema.optional(),
|
|
377
|
-
// Which product surface owns this rule (managed blocks). Never accepted from
|
|
378
|
-
// user input — `replaceManagedRules` is the only writer.
|
|
379
|
-
managedBy: managedBySchema.optional(),
|
|
380
|
-
// Unix seconds. TTL'd interventions (`sf block --ttl`): the runtime SKIPS an
|
|
381
|
-
// expired rule at match time (one timestamp compare, no recompile needed);
|
|
382
|
-
// the compiler GCs expired rules on the next write.
|
|
383
|
-
expiresAt: z.number().int().nonnegative().optional(),
|
|
384
|
-
// Surfaced on a `deny` (X-Spacefast-Reason header) and the rendered platform
|
|
385
|
-
// page so an operator/visitor can tell which rule fired and why. Diagnostic
|
|
386
|
-
// only — never load-bearing for ownership or semantics.
|
|
387
|
-
reasonCode: z.string().min(1).optional(),
|
|
388
|
-
message: z.string().min(1).optional(),
|
|
389
|
-
});
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
|
-
// 3b. Visitor token (the cloud "mint" half)
|
|
392
|
-
// ---------------------------------------------------------------------------
|
|
393
|
-
// The header `kid`/`alg` the platform signs visitor tokens with. These match
|
|
394
|
-
// the runtime JWKS key (`spacefast-runtime-v1` / EdDSA) so the runtime's
|
|
395
|
-
// JWKS-by-kid verify (shared/jwt.php) accepts a token the cloud minted. Do
|
|
396
|
-
// NOT introduce a second key system — these constants name the existing one.
|
|
397
|
-
export const ACCESS_TOKEN_KID = "spacefast-runtime-v1";
|
|
398
|
-
export const ACCESS_TOKEN_ALG = "EdDSA";
|
|
399
|
-
// The published JWKS path (served under /.well-known/ by the control plane and
|
|
400
|
-
// fetched by the runtime + Cast).
|
|
401
|
-
export const RUNTIME_JWKS_WELL_KNOWN_FILENAME = "spacefast-runtime-jwks.json";
|
|
402
|
-
// The `kid` the runtime's SPACE-LOCAL HS256 mint signs under (access-plan
|
|
403
|
-
// §3.1). The key derives from the space's password secret + sessionVersion and
|
|
404
|
-
// may only sign `pw:` grants — the same namespace filter that bounds BYO keys
|
|
405
|
-
// bounds this one. Never published in any JWKS; verification re-derives the
|
|
406
|
-
// key from serving secrets.
|
|
407
|
-
export const LOCAL_PW_TOKEN_KID = "spacefast-local-pw-v1";
|
|
408
|
-
// The claim set carried by a minted visitor token. `grants` are concrete
|
|
409
|
-
// (no `*`), `email:`/`user:`/`team:<teamId>:<role>`/`sub-team:<subTeamId>:<role>` namespaced so they survive
|
|
410
|
-
// the platform issuer's `grantNamespaces` filter at the runtime. `aud`, when
|
|
411
|
-
// present, lowercases-binds the token to a single serving host.
|
|
412
|
-
export const accessTokenPayloadSchema = z.object({
|
|
413
|
-
// Subject: the authenticated platform user id the token was minted for
|
|
414
|
-
// (or "invite:sin_x" / "link:lnk_x" / "pw:anon" for account-less audiences).
|
|
415
|
-
sub: z.string().min(1),
|
|
416
|
-
// Concrete (non-glob) grants. The runtime intersects these with a rule's
|
|
417
|
-
// `requiredGrants`; the visitor passes when the intersection is non-empty.
|
|
418
|
-
grants: z.array(grantSchema).default([]),
|
|
419
|
-
// Unix seconds. Mirrors the runtime's exp check (300s leeway).
|
|
420
|
-
exp: z.number().int().nonnegative(),
|
|
421
|
-
nbf: z.number().int().nonnegative().optional(),
|
|
422
|
-
iat: z.number().int().nonnegative().optional(),
|
|
423
|
-
iss: z.string().min(1).optional(),
|
|
424
|
-
// Optional host binding (lowercased) — when set the token is only accepted on
|
|
425
|
-
// that serving host.
|
|
426
|
-
aud: z.string().min(1).optional(),
|
|
427
|
-
// Space session version (access-plan §3.1): the runtime rejects a visitor
|
|
428
|
-
// token whose `sv` mismatches `policy.sessionVersion`. Logout-all is an `sv`
|
|
429
|
-
// bump — structural, TTL-independent revocation. Absent = 0.
|
|
430
|
-
sv: z.number().int().nonnegative().optional(),
|
|
431
|
-
// Single-use id for authorize-chain callback handoffs (access-plan X-29):
|
|
432
|
-
// the runtime callback consumes `jti` in its replay cache, so a token lifted
|
|
433
|
-
// from browser history is dead after first use. Share tokens carry no `jti`
|
|
434
|
-
// (the share URL IS the credential; revocation is structural).
|
|
435
|
-
jti: z.string().min(1).optional(),
|
|
436
|
-
// Cast room capability. Collaboration tokens are valid for exactly one
|
|
437
|
-
// canonical room; other visitor tokens omit it.
|
|
438
|
-
room: z.string().min(1).optional(),
|
|
439
|
-
// Anonymous Comments tokens remain anonymous identities even though they
|
|
440
|
-
// carry a signed room capability.
|
|
441
|
-
anonymous_id: z.string().min(1).optional(),
|
|
442
|
-
name: z.string().min(1).max(120).optional(),
|
|
443
|
-
});
|
|
444
|
-
// Authorize request for `POST /v1/spaces/:id/access/authorize` — surviving for
|
|
445
|
-
// exactly one caller: the dashboard preview-token mint (iframe → runtime
|
|
446
|
-
// callback). It never sets cookies; only the runtime callback sets the visitor
|
|
447
|
-
// cookie. `aud` binds the token to the previewed host.
|
|
448
|
-
export const authorizeRequestSchema = z.object({
|
|
449
|
-
aud: z.string().min(1).optional(),
|
|
450
|
-
});
|
|
451
|
-
// Authorize response: the signed compact JWT plus its decoded claims (so a
|
|
452
|
-
// caller can show/inspect the grants without re-parsing).
|
|
453
|
-
export const authorizeResponseSchema = z.object({
|
|
454
|
-
// The signed compact EdDSA JWT (`header.claims.signature`, base64url).
|
|
455
|
-
token: z.string().min(1),
|
|
456
|
-
// Decoded claims of `token` (echo of what was signed).
|
|
457
|
-
payload: accessTokenPayloadSchema,
|
|
458
|
-
// Unix seconds the token expires at (convenience mirror of `payload.exp`).
|
|
459
|
-
expiresAt: z.number().int().nonnegative(),
|
|
460
|
-
});
|
|
461
|
-
// ---------------------------------------------------------------------------
|
|
462
|
-
// 3b'. Access events (access-plan §5.6b, X-37)
|
|
463
|
-
// ---------------------------------------------------------------------------
|
|
464
|
-
// The access event is a first-class contract from day one: the runtime emits
|
|
465
|
-
// one per enforced decision into the per-hostname analytics ingestion; the
|
|
466
|
-
// Views panel is a reader of an already-flowing stream. `grantsHash` is a
|
|
467
|
-
// sha256 of the sorted verified grant list (never the grants themselves).
|
|
468
|
-
export const accessEventSchema = z.object({
|
|
469
|
-
sub: z.string().min(1).nullable().default(null),
|
|
470
|
-
grantsHash: z.string().min(1).nullable().default(null),
|
|
471
|
-
ruleId: z.string().min(1).nullable().default(null),
|
|
472
|
-
effect: z.enum(["allow", "deny", "challenge"]),
|
|
473
|
-
reasonCode: z.string().min(1).nullable().default(null),
|
|
474
|
-
host: z.string().min(1),
|
|
475
|
-
path: z.string().min(1),
|
|
476
|
-
// Unix seconds.
|
|
477
|
-
ts: z.number().int().nonnegative(),
|
|
75
|
+
.min(1)
|
|
76
|
+
.max(1024)
|
|
77
|
+
.check((context) => {
|
|
78
|
+
try {
|
|
79
|
+
context.value = normalizeAccessScopePath(context.value);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
context.issues.push({
|
|
83
|
+
code: "custom",
|
|
84
|
+
input: context.value,
|
|
85
|
+
message: error instanceof Error ? error.message : "invalid access scope",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
478
88
|
});
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// accepted-invite path mints the `viewer` level.
|
|
486
|
-
export function spaceLevelGrant(spaceId, level) {
|
|
487
|
-
return `space:${spaceId}:${level}`;
|
|
89
|
+
export function accessScopeContains(scope, path) {
|
|
90
|
+
const canonicalScope = normalizeAccessScopePath(scope);
|
|
91
|
+
const canonicalPath = normalizeAccessScopePath(path);
|
|
92
|
+
return (canonicalScope === "/" ||
|
|
93
|
+
canonicalPath === canonicalScope ||
|
|
94
|
+
canonicalPath.startsWith(`${canonicalScope}/`));
|
|
488
95
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
96
|
+
export const authorityReferenceSchema = z
|
|
97
|
+
.string()
|
|
98
|
+
.regex(/^(member|person|link|password|machine|claim-preview|external):[A-Za-z0-9_.-]+$/);
|
|
99
|
+
// WHO a session is, orthogonal to WHAT it may do. A session has exactly one
|
|
100
|
+
// principal and any number of authorities; neither implies the other. A member
|
|
101
|
+
// who arrives by share link is still that member, and a link authority alone
|
|
102
|
+
// never makes someone anonymous.
|
|
103
|
+
export const accountPrincipalSchema = z.string().regex(/^account:[A-Za-z0-9_-]+$/);
|
|
104
|
+
// The three kinds of somebody. `account:` is a Spacefast user. `person:` is
|
|
105
|
+
// someone this Space invited by email and who accepted — a real, named identity
|
|
106
|
+
// with no platform account behind it. `external:` is a subject an identity
|
|
107
|
+
// connection vouched for. Anything else — a share Link, a password, a machine
|
|
108
|
+
// key, a claim preview — authenticates no one: those sessions stay anonymous
|
|
109
|
+
// and carry the credential purely as an authority.
|
|
110
|
+
export const identifiedPrincipalSchema = z
|
|
111
|
+
.string()
|
|
112
|
+
.regex(/^(?:account|person|external):[A-Za-z0-9_.-]+$/);
|
|
113
|
+
export const accessPrincipalSchema = z.union([z.literal("anonymous"), identifiedPrincipalSchema]);
|
|
114
|
+
export function accountPrincipal(userId) {
|
|
115
|
+
return accountPrincipalSchema.parse(`account:${userId}`);
|
|
495
116
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
117
|
+
/** The user id behind an account principal; null for anonymous or malformed. */
|
|
118
|
+
export function accountPrincipalUserId(principal) {
|
|
119
|
+
if (!principal || !accountPrincipalSchema.safeParse(principal).success)
|
|
120
|
+
return null;
|
|
121
|
+
return principal.slice("account:".length);
|
|
500
122
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
123
|
+
/**
|
|
124
|
+
* The identity an authority reference names on its own, if any. Credential
|
|
125
|
+
* classes name nobody. This is the ONE place a class maps to an identity: at
|
|
126
|
+
* the exchange that just authenticated it, never downstream.
|
|
127
|
+
*/
|
|
128
|
+
export function principalForAuthority(reference) {
|
|
129
|
+
return reference.startsWith("person:") || reference.startsWith("external:")
|
|
130
|
+
? identifiedPrincipalSchema.parse(reference)
|
|
131
|
+
: undefined;
|
|
505
132
|
}
|
|
506
|
-
|
|
507
|
-
// 3d. Share links + the sharing projection (access-plan §5.1/§5.2, X-33)
|
|
508
|
-
// ---------------------------------------------------------------------------
|
|
509
|
-
// Share tokens are re-signed on demand from the link row (§5.2): the JWT is
|
|
510
|
-
// never stored, only the row. Credential lifetime = artifact lifetime: `exp`
|
|
511
|
-
// IS the link's expiry and is omitted for a never-expiring link. The runtime
|
|
512
|
-
// permits that omission only for a signed, audience-bound token whose subject
|
|
513
|
-
// and sole grant are the same `link:` capability. Revocation remains
|
|
514
|
-
// structural — the grant drops from the compiled rules.
|
|
515
|
-
// A share link row projected onto the wire (§4.4). `path` is "/" for a
|
|
516
|
-
// space-wide link; any other value scopes the link's grant to that subtree
|
|
517
|
-
// (compile-time: the grant only ever appears in rules matching the path).
|
|
518
|
-
// `lastUsedAt`/`useCount` are the operational-observability columns (X-28);
|
|
519
|
-
// the runtime-side `?sf_share=` trade increments them offline, so they read 0
|
|
520
|
-
// until that ingestion lands.
|
|
521
|
-
export const shareLinkSchema = z.object({
|
|
133
|
+
export const personGrantSchema = z.object({
|
|
522
134
|
id: z.string().min(1),
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
});
|
|
536
|
-
export const shareLinkCreateSchema = z.object({
|
|
537
|
-
// A plain absolute path ("/" = the whole space).
|
|
538
|
-
path: z.string().min(1).max(1024).default("/"),
|
|
539
|
-
// Unix seconds. Omitted = no expiry (structural revocation still applies).
|
|
540
|
-
expiresAt: z.number().int().positive().optional(),
|
|
541
|
-
label: z.string().min(1).max(200).optional(),
|
|
542
|
-
});
|
|
543
|
-
// Returned on create and on the explicit re-copy intent ONLY — list reads never
|
|
544
|
-
// carry the token-bearing URL (§5.2: share-URL responses are no-store).
|
|
545
|
-
export const shareLinkWithUrlSchema = shareLinkSchema.extend({
|
|
546
|
-
url: z.string().min(1).describe("https://<host><path>?sf_share=<token>"),
|
|
547
|
-
});
|
|
548
|
-
// Who can open the space — ONE exclusive bit, always explicit. Everything
|
|
549
|
-
// else (password, share links, invites, connection audiences, service tokens)
|
|
550
|
-
// is an additive KEY into a restricted space: union semantics, any key
|
|
551
|
-
// admits; keys are dormant on a public space, never errors. Visibility is
|
|
552
|
-
// DERIVED from the `sharing:` managed block (a restricted space is one whose
|
|
553
|
-
// block carries the space-wide challenge rule) — never stored separately.
|
|
554
|
-
export const SPACE_VISIBILITIES = ["public", "restricted"];
|
|
555
|
-
export const spaceVisibilitySchema = z.enum(SPACE_VISIBILITIES);
|
|
556
|
-
// A connection audience member on the People-only selector state (§6.2/§6.3):
|
|
557
|
-
// "your company SSO" (oidc — optionally narrowed to a verified email domain)
|
|
558
|
-
// or "Acme Shop subscribers" (external — the owner-defined grant tag).
|
|
559
|
-
export const sharingConnectionAudienceSchema = z.object({
|
|
560
|
-
connectionId: z.string().min(1),
|
|
561
|
-
// External connections: the grant tag ("wc-subscriber" → ext:{acn}:{tag}).
|
|
562
|
-
// Required for external, rejected for oidc.
|
|
563
|
-
tag: z
|
|
564
|
-
.string()
|
|
565
|
-
.min(1)
|
|
566
|
-
.max(100)
|
|
567
|
-
.regex(/^[A-Za-z0-9._-]+$/)
|
|
568
|
-
.optional(),
|
|
569
|
-
// OIDC connections: narrow the audience to a verified email domain — the
|
|
570
|
-
// rule compiles `email:*@{domain}` instead of `sso:{acn}` (the SSO mint
|
|
571
|
-
// carries the verified `email:` grant). Rejected for external.
|
|
572
|
-
emailDomain: z
|
|
573
|
-
.string()
|
|
574
|
-
.min(3)
|
|
575
|
-
.max(255)
|
|
576
|
-
.regex(/^[a-z0-9.-]+\.[a-z]{2,}$/i)
|
|
577
|
-
.optional(),
|
|
578
|
-
});
|
|
579
|
-
// PUT …/sharing/visibility — the ONE exclusive choice. Flipping visibility
|
|
580
|
-
// never creates or destroys keys: restricting re-arms whatever keys exist;
|
|
581
|
-
// going public leaves them dormant (including a stored password).
|
|
582
|
-
export const visibilityWriteSchema = z.object({
|
|
583
|
-
visibility: spaceVisibilitySchema,
|
|
584
|
-
});
|
|
585
|
-
// PUT …/sharing/password — set/replace the password KEY. Setting a password
|
|
586
|
-
// also restricts a public space (the intent is unambiguous); it never touches
|
|
587
|
-
// other keys. DELETE clears the key and leaves visibility alone (fail-closed:
|
|
588
|
-
// a restricted space stays restricted, team-only if that was the last key).
|
|
589
|
-
export const sharingPasswordWriteSchema = z.object({
|
|
590
|
-
password: z.string().min(1).max(256),
|
|
591
|
-
});
|
|
592
|
-
// PUT …/sharing/audiences — replace the connection-audience keys (§6.2/§6.3).
|
|
593
|
-
// Compiled INTO the one sharing general rule (mixed audiences = one rule,
|
|
594
|
-
// multiple acquires — the chooser is the challenge page, never a new surface).
|
|
595
|
-
export const sharingAudiencesWriteSchema = z.object({
|
|
596
|
-
audiences: z.array(sharingConnectionAudienceSchema).max(20),
|
|
597
|
-
});
|
|
598
|
-
// The core protection op (agent surface): visibility plus the password key in
|
|
599
|
-
// one write. `password: string` sets the key (and restricts), `null` clears
|
|
600
|
-
// it, omitted leaves it alone. Deeper key management (links, invites,
|
|
601
|
-
// audiences) lives on the sharing surface.
|
|
602
|
-
export const spaceProtectionWriteSchema = z.object({
|
|
603
|
-
visibility: spaceVisibilitySchema,
|
|
604
|
-
password: z.string().min(1).max(256).nullable().optional(),
|
|
605
|
-
});
|
|
606
|
-
// A connection audience projected back on the sharing read (derived from the
|
|
607
|
-
// compiled block — never stored separately, X-33).
|
|
608
|
-
export const sharingAudienceSchema = z.object({
|
|
609
|
-
connectionId: z.string().min(1),
|
|
610
|
-
type: accessConnectionTypeSchema,
|
|
611
|
-
// Connection display name (null when the connection row is gone).
|
|
612
|
-
name: z.string().min(1).nullable().default(null),
|
|
613
|
-
tag: z.string().min(1).nullable().default(null),
|
|
614
|
-
emailDomain: z.string().min(1).nullable().default(null),
|
|
615
|
-
// The compiled requiredGrants entry this audience contributes.
|
|
616
|
-
grant: grantSchema,
|
|
617
|
-
});
|
|
618
|
-
// A row in the Share modal's People list. Team members are badged; invitees
|
|
619
|
-
// (§5.3) carry their invite id + status so the modal can revoke per-row and
|
|
620
|
-
// badge pending/accepted/expired.
|
|
621
|
-
export const sharingPersonSchema = z.object({
|
|
622
|
-
kind: z.enum(["team_member", "invitee"]),
|
|
623
|
-
userId: z.string().min(1).nullable().default(null),
|
|
624
|
-
email: z.string().min(1).nullable().default(null),
|
|
625
|
-
name: z.string().min(1).nullable().default(null),
|
|
626
|
-
// Team management role for members (owner|admin|member); null for invitees.
|
|
627
|
-
teamRole: z.string().min(1).nullable().default(null),
|
|
628
|
-
// Space level for invitees (viewer in v1); null for team members.
|
|
629
|
-
level: spaceLevelSchema.nullable().default(null),
|
|
630
|
-
// The sin_ invite id for invitees (per-row revoke); null for team members.
|
|
631
|
-
inviteId: z.string().min(1).nullable().default(null),
|
|
632
|
-
// Invite lifecycle for invitees ("expired" = the accept LINK lapsed
|
|
633
|
-
// unaccepted; access-grant compilation is structural, not TTL'd); null for
|
|
634
|
-
// team members. Revoked invites never appear in the People list.
|
|
635
|
-
status: z.enum(["pending", "accepted", "expired"]).nullable().default(null),
|
|
636
|
-
// Operational observability (X-28): per-invitee "last viewed"; null until
|
|
637
|
-
// the Views ingestion feeds it.
|
|
638
|
-
lastViewedAt: z.string().min(1).nullable().default(null),
|
|
639
|
-
});
|
|
640
|
-
// ---------------------------------------------------------------------------
|
|
641
|
-
// 3d'. Invites (access-plan §5.3) + access requests (§5.4)
|
|
642
|
-
// ---------------------------------------------------------------------------
|
|
643
|
-
// Accept-link validity (§4.4): how long the emailed magic link mints tokens.
|
|
644
|
-
// Access itself is structural (the invite grant stays compiled until revoke);
|
|
645
|
-
// an expired link only means "re-mail me a new one".
|
|
646
|
-
export const INVITE_ACCEPT_LINK_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days
|
|
647
|
-
// The visitor token an accept mints (§5.3): 30 days, re-entry by re-clicking
|
|
648
|
-
// the emailed link (X-8: no guest sessions, the inbox is the credential).
|
|
649
|
-
export const INVITE_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 days
|
|
650
|
-
// Invite/request personal messages (X-25): ≤280 chars, plain text only —
|
|
651
|
-
// stored as typed, HTML-escaped at every render (email + pages), never
|
|
652
|
-
// linkified, rendered visually subordinate to the platform frame.
|
|
653
|
-
export const ACCESS_MESSAGE_MAX_LENGTH = 280;
|
|
654
|
-
export const INVITE_STATUSES = ["pending", "accepted", "expired", "revoked"];
|
|
655
|
-
export const inviteStatusSchema = z.enum(INVITE_STATUSES);
|
|
656
|
-
// A space invite projected onto the wire (§4.4). The accept token is NEVER
|
|
657
|
-
// stored or returned — only its hash is at rest, and the token travels solely
|
|
658
|
-
// in the invitee's email (possession of the inbox IS the credential). `email`
|
|
659
|
-
// is null once revoked (X-25: invitee addresses are PII of non-users and are
|
|
660
|
-
// purged on revoke; the row survives for audit).
|
|
661
|
-
export const spaceInviteSchema = z.object({
|
|
135
|
+
scope: accessScopePathSchema,
|
|
136
|
+
role: accessRoleSchema,
|
|
137
|
+
createdAt: z.string().datetime(),
|
|
138
|
+
revokedAt: z.string().datetime().nullable(),
|
|
139
|
+
});
|
|
140
|
+
export const spacePersonStatusSchema = z.enum(["pending", "accepted", "revoked"]);
|
|
141
|
+
export const spacePersonDisplayNameSchema = z.string().trim().min(1).max(160);
|
|
142
|
+
export function fallbackSpacePersonDisplayName(personId) {
|
|
143
|
+
const suffix = personId.replace(/[^A-Za-z0-9]/gu, "").slice(-6);
|
|
144
|
+
return suffix ? `Guest ${suffix}` : "Guest";
|
|
145
|
+
}
|
|
146
|
+
export const spacePersonSchema = z.object({
|
|
662
147
|
id: z.string().min(1),
|
|
663
148
|
spaceId: z.string().min(1),
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
acceptedAt: z.string().
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
149
|
+
displayName: spacePersonDisplayNameSchema,
|
|
150
|
+
email: z.string().email().nullable(),
|
|
151
|
+
status: spacePersonStatusSchema,
|
|
152
|
+
grants: z.array(personGrantSchema),
|
|
153
|
+
invitedBy: z.string().nullable(),
|
|
154
|
+
inviteExpiresAt: z.string().datetime().nullable(),
|
|
155
|
+
acceptedAt: z.string().datetime().nullable(),
|
|
156
|
+
revokedAt: z.string().datetime().nullable(),
|
|
157
|
+
createdAt: z.string().datetime(),
|
|
158
|
+
});
|
|
159
|
+
export const inheritedTeamAccessSchema = z.object({
|
|
160
|
+
teamId: z.string().min(1),
|
|
161
|
+
teamName: z.string().min(1),
|
|
162
|
+
ruleScopes: z
|
|
163
|
+
.array(accessScopePathSchema)
|
|
164
|
+
.min(1)
|
|
165
|
+
.describe("Explicit team rule locations, not effective subtree coverage."),
|
|
166
|
+
members: z.array(z.object({
|
|
167
|
+
membershipId: z.string().min(1),
|
|
168
|
+
userId: z.string().min(1),
|
|
169
|
+
displayName: z.string().min(1),
|
|
170
|
+
email: z.string().email(),
|
|
171
|
+
teamRole: z.string().min(1),
|
|
172
|
+
role: z.literal("editor"),
|
|
173
|
+
})),
|
|
174
|
+
});
|
|
175
|
+
export const spacePeopleListSchema = z.object({
|
|
176
|
+
people: z.array(spacePersonSchema),
|
|
177
|
+
inheritedTeamAccess: inheritedTeamAccessSchema.nullable(),
|
|
178
|
+
});
|
|
179
|
+
export const accessShareLinkSchema = z.object({
|
|
684
180
|
id: z.string().min(1),
|
|
685
181
|
spaceId: z.string().min(1),
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
host: z.string().min(1),
|
|
689
|
-
path: z.string().min(1),
|
|
690
|
-
message: z.string().nullable().default(null),
|
|
691
|
-
status: accessRequestStatusSchema,
|
|
692
|
-
resolvedBy: z.string().min(1).nullable().default(null),
|
|
693
|
-
resolvedAt: z.string().min(1).nullable().default(null),
|
|
694
|
-
createdAt: z.string().min(1),
|
|
695
|
-
});
|
|
696
|
-
// POST /v1/access/requests — the 403-page form's target. Identity (email +
|
|
697
|
-
// userId) comes from the SESSION, never the body: the authorize chain
|
|
698
|
-
// guarantees requesters are identified (anonymous → 401).
|
|
699
|
-
export const accessRequestCreateSchema = z.object({
|
|
700
|
-
space: z.string().min(1),
|
|
701
|
-
host: z.string().min(1).max(255),
|
|
702
|
-
path: z.string().max(1024).default("/"),
|
|
703
|
-
nonce: z.string().min(1).max(2048).optional(),
|
|
704
|
-
message: z.string().max(ACCESS_MESSAGE_MAX_LENGTH).optional(),
|
|
705
|
-
});
|
|
706
|
-
// POST …/requests/:id/approve — the chooser (§5.4): "invite" reuses the §5.3
|
|
707
|
-
// magic-link invite; "team" is the Better-Auth team invitation.
|
|
708
|
-
export const accessRequestApproveSchema = z.object({
|
|
709
|
-
as: z.enum(["invite", "team"]).default("invite"),
|
|
710
|
-
});
|
|
711
|
-
// File-mode sharing (§5.1 files-mode merge): the dashboard-side file-share
|
|
712
|
-
// rule compiler is dead; file shares are a server-side intent surface owning
|
|
713
|
-
// the `file_share` managed block. A record is keyed by the file/folder PATH;
|
|
714
|
-
// the server expands the serving-path aliases (exact, directory index,
|
|
715
|
-
// extensionless .html variant) at compile time.
|
|
716
|
-
export const FILE_SHARE_AUDIENCES = ["public", "people"];
|
|
717
|
-
export const fileShareAudienceSchema = z.enum(FILE_SHARE_AUDIENCES);
|
|
718
|
-
// The projected file-share record (read shape on the sharing projection).
|
|
719
|
-
export const fileShareSchema = z.object({
|
|
720
|
-
// Canonical absolute file path ("/report/deck.pdf") or folder path ("/report/").
|
|
721
|
-
path: z.string().min(1),
|
|
722
|
-
audience: fileShareAudienceSchema,
|
|
723
|
-
// The email: grants admitted when audience = "people"; empty for "public".
|
|
724
|
-
grants: z.array(grantSchema).default([]),
|
|
725
|
-
// The compiled serving-path alias patterns the record covers (diagnostic).
|
|
726
|
-
patterns: z.array(z.string().min(1)).default([]),
|
|
727
|
-
});
|
|
728
|
-
// PUT /v1/spaces/:id/file-shares — upsert the record for one path. `grants`
|
|
729
|
-
// accepts raw emails or `email:` grants; the server normalizes + validates.
|
|
730
|
-
export const fileShareWriteSchema = z.object({
|
|
731
|
-
path: z.string().min(1).max(1024),
|
|
732
|
-
audience: fileShareAudienceSchema,
|
|
733
|
-
grants: z.array(z.string().min(1)).max(100).optional(),
|
|
734
|
-
});
|
|
735
|
-
// A pending access request row on the sharing projection (§5.4). `host`/`path`
|
|
736
|
-
// say where the requester was headed so the admin has context.
|
|
737
|
-
export const sharingRequestSchema = z.object({
|
|
738
|
-
id: z.string().min(1),
|
|
739
|
-
email: z.string().min(1),
|
|
740
|
-
message: z.string().nullable().default(null),
|
|
741
|
-
createdAt: z.string().min(1),
|
|
742
|
-
host: z.string().min(1).nullable().default(null),
|
|
743
|
-
path: z.string().min(1).nullable().default(null),
|
|
744
|
-
});
|
|
745
|
-
// ---------------------------------------------------------------------------
|
|
746
|
-
// 3d''. Service tokens (access-plan §4.4/§5.5 — machine principals)
|
|
747
|
-
// ---------------------------------------------------------------------------
|
|
748
|
-
// Service tokens are the machine-principal leg of the IAM table (§5.5):
|
|
749
|
-
// `stk_` rows, Bearer-only, shown ONCE on create, `svc:{id}` grants compiled
|
|
750
|
-
// wherever the token's `actsAs` satisfies, structural revocation (X-7 —
|
|
751
|
-
// revoke recompiles the grant out; the credential's TTL is irrelevant).
|
|
752
|
-
//
|
|
753
|
-
// The secret IS a platform-signed visitor JWT ({sub: "svc:stk_x", grants:
|
|
754
|
-
// ["svc:stk_x"], exp: expiresAt | now+10y}) — the runtime's Bearer path
|
|
755
|
-
// already verifies visitor JWTs before the cookie (§3.2), so CI/agents fetch
|
|
756
|
-
// protected artifacts with `Authorization: Bearer <token>` and ZERO runtime
|
|
757
|
-
// work. Only the token's sha256 is stored (lastUsedAt attribution/display);
|
|
758
|
-
// the plaintext never persists.
|
|
759
|
-
//
|
|
760
|
-
// `sv` posture (documented, accepted): a space-scoped token pins the space's
|
|
761
|
-
// sessionVersion at mint; a team-wide token carries sv 0 (the absent-sv
|
|
762
|
-
// default). A space "logout-all" (sv bump) therefore ALSO rotates service
|
|
763
|
-
// access to that space — re-create the token. Arguably correct: logout-all
|
|
764
|
-
// means "everyone out", and service tokens are visitors too.
|
|
765
|
-
export const SERVICE_TOKEN_DEFAULT_TTL_SECONDS = 10 * 365 * 24 * 60 * 60; // 10 years
|
|
766
|
-
// The audience classes a service token can act as. v1 has exactly one:
|
|
767
|
-
// "viewers" — the token satisfies every managed rule that admits the
|
|
768
|
-
// team-viewer audience (the sharing block's general rule in `link`/`people`
|
|
769
|
-
// modes and file-share `people` rules). It does NOT satisfy a password wall
|
|
770
|
-
// (`pw:` is password-only, like team members) or raw rules that don't name
|
|
771
|
-
// `svc:` grants.
|
|
772
|
-
export const SERVICE_TOKEN_AUDIENCES = ["viewers"];
|
|
773
|
-
export const serviceTokenAudienceSchema = z.enum(SERVICE_TOKEN_AUDIENCES);
|
|
774
|
-
export const serviceTokenActsAsSchema = z.object({
|
|
775
|
-
audiences: z.array(serviceTokenAudienceSchema).min(1).default(["viewers"]),
|
|
776
|
-
});
|
|
777
|
-
// A service token projected onto the wire — NEVER carries the secret; the
|
|
778
|
-
// signed JWT appears exactly once, on the create response.
|
|
779
|
-
export const serviceTokenSchema = z.object({
|
|
780
|
-
id: z.string().min(1),
|
|
781
|
-
teamId: z.string().min(1),
|
|
782
|
-
// null = team-wide (the grant compiles into every team space).
|
|
783
|
-
spaceId: z.string().min(1).nullable().default(null),
|
|
182
|
+
grantId: z.string().min(1),
|
|
183
|
+
revision: z.number().int().positive(),
|
|
784
184
|
name: z.string().min(1),
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
185
|
+
landingPath: accessScopePathSchema,
|
|
186
|
+
resources: grantResourceSetSchema,
|
|
187
|
+
capabilities: z.array(z.enum(["page.view", "comments.read", "comments.write"])).min(1),
|
|
188
|
+
constraints: grantConstraintsSchema,
|
|
189
|
+
target: grantTargetSelectorSchema,
|
|
190
|
+
createdBy: z.string().nullable(),
|
|
191
|
+
openCount: z.number().int().nonnegative(),
|
|
192
|
+
lastUsedAt: z.string().datetime().nullable(),
|
|
193
|
+
revokedAt: z.string().datetime().nullable(),
|
|
194
|
+
createdAt: z.string().datetime(),
|
|
793
195
|
active: z.boolean(),
|
|
794
196
|
});
|
|
795
|
-
export const
|
|
796
|
-
|
|
797
|
-
// Scope the grant to one space; omitted = team-wide.
|
|
798
|
-
spaceId: z.string().min(1).optional(),
|
|
799
|
-
// Unix seconds; omitted = the 10-year default. Revocation stays structural
|
|
800
|
-
// either way — expiry is a hygiene ceiling, not the revocation mechanism.
|
|
801
|
-
expiresAt: z.number().int().positive().optional(),
|
|
802
|
-
actsAs: serviceTokenActsAsSchema.optional(),
|
|
803
|
-
});
|
|
804
|
-
// Returned ONLY on create (no-store): the signed Bearer JWT, shown once.
|
|
805
|
-
export const serviceTokenWithSecretSchema = serviceTokenSchema.extend({
|
|
806
|
-
token: z.string().min(1),
|
|
197
|
+
export const accessShareLinkWithUrlSchema = accessShareLinkSchema.extend({
|
|
198
|
+
url: z.string().url(),
|
|
807
199
|
});
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
export const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
200
|
+
// Reads re-derive the durable branded URL from the sealed token. Links minted
|
|
201
|
+
// before branded URLs existed report null until the backfill mints theirs,
|
|
202
|
+
// rather than pretending to hold a URL that would not open.
|
|
203
|
+
export const accessShareLinkReadSchema = accessShareLinkSchema.extend({
|
|
204
|
+
url: z.string().url().nullable(),
|
|
205
|
+
});
|
|
206
|
+
// The publish receipt exposes one durable access capability in both of its
|
|
207
|
+
// useful transports: a browser entry URL and the raw token integrations send
|
|
208
|
+
// in X-SF-Authorization. It is access to published bytes, never proof of
|
|
209
|
+
// ownership; claim credentials stay a separate receipt field.
|
|
210
|
+
export const publishedAccessSchema = z.object({
|
|
211
|
+
url: z.string().url(),
|
|
212
|
+
token: z.string().min(16).max(512),
|
|
213
|
+
expiresAt: z.string().datetime().nullable(),
|
|
214
|
+
});
|
|
215
|
+
export const canonicalAccessRequestStatusSchema = z.enum([
|
|
216
|
+
"verification_pending",
|
|
217
|
+
"pending",
|
|
218
|
+
"approved",
|
|
219
|
+
"denied",
|
|
220
|
+
"canceled",
|
|
221
|
+
"expired",
|
|
222
|
+
]);
|
|
223
|
+
export const accessRequestRoleSchema = accessRoleSchema;
|
|
224
|
+
export const canonicalAccessRequestSchema = z.object({
|
|
225
|
+
id: z.string().min(1),
|
|
226
|
+
spaceId: z.string().min(1),
|
|
227
|
+
displayName: spacePersonDisplayNameSchema.nullable(),
|
|
228
|
+
email: z.string().email(),
|
|
229
|
+
scope: accessScopePathSchema,
|
|
230
|
+
requestedRole: accessRequestRoleSchema,
|
|
231
|
+
message: z.string().nullable(),
|
|
232
|
+
status: canonicalAccessRequestStatusSchema,
|
|
233
|
+
emailVerifiedAt: z.string().datetime().nullable(),
|
|
234
|
+
resolvedBy: z.string().nullable(),
|
|
235
|
+
resolvedAt: z.string().datetime().nullable(),
|
|
236
|
+
createdAt: z.string().datetime(),
|
|
237
|
+
updatedAt: z.string().datetime(),
|
|
238
|
+
});
|
|
239
|
+
export const accessFenceKindValues = ["none", "ownership", "exposure"];
|
|
240
|
+
export const accessFenceKindSchema = z.enum(accessFenceKindValues);
|
|
241
|
+
export const ACCESS_GRANT_SCOPE_LIMIT = 32;
|
|
242
|
+
export const linkCapabilitiesSchema = z
|
|
243
|
+
.array(z.enum(["page.view", "comments.read", "comments.write"]))
|
|
244
|
+
.min(1)
|
|
245
|
+
.refine((capabilities) => capabilities.includes("page.view"), {
|
|
246
|
+
message: "Link capabilities must include page.view",
|
|
247
|
+
})
|
|
248
|
+
.refine((capabilities) => new Set(capabilities).size === capabilities.length, {
|
|
249
|
+
message: "Link capabilities must be unique",
|
|
250
|
+
});
|
|
251
|
+
export const spacePersonInviteWriteSchema = z.object({
|
|
252
|
+
email: z.string().email(),
|
|
253
|
+
grants: z
|
|
254
|
+
.array(z.object({ scope: accessScopePathSchema, role: accessRoleSchema }))
|
|
830
255
|
.min(1)
|
|
831
|
-
.max(
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
export const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
// cloud lane): an anonymous allow pinned to the live channel, then a
|
|
871
|
-
// catch-all team-viewer challenge. Live requests terminate on the allow
|
|
872
|
-
// (identical to the no-rule-matches default); anything on another channel
|
|
873
|
-
// falls through to the lock. Because the block is last, every explicit space
|
|
874
|
-
// rule still wins first-match before either rule.
|
|
875
|
-
//
|
|
876
|
-
// These rules are canonical vocabulary (single-homed here): the dashboard
|
|
877
|
-
// toggle and the CLI post exactly these rows through the EXISTING
|
|
878
|
-
// PUT /v1/teams/:id/access/defaults — no second compiler exists.
|
|
879
|
-
export const TEAM_PREVIEW_LOCK_LIVE_RULE_ID = "team-preview-lock:live";
|
|
880
|
-
export const TEAM_PREVIEW_LOCK_RULE_ID = "team-preview-lock";
|
|
881
|
-
export function teamPreviewLockRules(teamId) {
|
|
882
|
-
return [
|
|
883
|
-
{
|
|
884
|
-
id: TEAM_PREVIEW_LOCK_LIVE_RULE_ID,
|
|
885
|
-
match: { channel: "live" },
|
|
886
|
-
effect: "allow",
|
|
887
|
-
reasonCode: "team_preview_lock",
|
|
888
|
-
},
|
|
889
|
-
{
|
|
890
|
-
id: TEAM_PREVIEW_LOCK_RULE_ID,
|
|
891
|
-
match: {},
|
|
892
|
-
effect: "challenge",
|
|
893
|
-
auth: { requiredGrants: [`team:${teamId}:viewer`] },
|
|
894
|
-
reasonCode: "team_preview_lock",
|
|
895
|
-
message: "Non-live channels on this team are team-only.",
|
|
896
|
-
},
|
|
897
|
-
];
|
|
898
|
-
}
|
|
899
|
-
export function isTeamPreviewLockRule(rule) {
|
|
900
|
-
return rule.id === TEAM_PREVIEW_LOCK_LIVE_RULE_ID || rule.id === TEAM_PREVIEW_LOCK_RULE_ID;
|
|
901
|
-
}
|
|
902
|
-
// Whether a team-defaults rule list carries the preview lock.
|
|
903
|
-
export function teamPreviewLockActive(rules) {
|
|
904
|
-
return rules.some((rule) => rule.id === TEAM_PREVIEW_LOCK_RULE_ID);
|
|
905
|
-
}
|
|
906
|
-
// Toggle helper: returns the defaults list with the lock applied/removed,
|
|
907
|
-
// preserving every other default rule (lock rules ride LAST so other
|
|
908
|
-
// defaults keep first-match precedence within the block).
|
|
909
|
-
export function withTeamPreviewLock(rules, teamId, enabled) {
|
|
910
|
-
const kept = rules.filter((rule) => !isTeamPreviewLockRule(rule));
|
|
911
|
-
return enabled ? [...kept, ...teamPreviewLockRules(teamId)] : kept;
|
|
912
|
-
}
|
|
913
|
-
// GET /v1/spaces/:id/sharing — the server-owned projection (X-33): the
|
|
914
|
-
// dashboard renders this and posts intents; it never interprets or compiles
|
|
915
|
-
// rules.
|
|
916
|
-
export const spaceSharingSchema = z.object({
|
|
917
|
-
// The one exclusive bit. Everything below it is keys.
|
|
918
|
-
visibility: spaceVisibilitySchema,
|
|
919
|
-
// Whether the password KEY exists (the verifier hash, never the password).
|
|
920
|
-
// Independent of visibility: a public space can hold a dormant password.
|
|
921
|
-
hasPassword: z.boolean(),
|
|
922
|
-
people: z.array(sharingPersonSchema).default([]),
|
|
923
|
-
// Connection audiences on the People-only state (§6.2/§6.3), derived from
|
|
924
|
-
// the compiled sharing block.
|
|
925
|
-
audiences: z.array(sharingAudienceSchema).default([]),
|
|
926
|
-
links: z.array(shareLinkSchema).default([]),
|
|
927
|
-
// Per-path file shares (the `file_share` managed block projected back).
|
|
928
|
-
fileShares: z.array(fileShareSchema).default([]),
|
|
929
|
-
requests: z.array(sharingRequestSchema).default([]),
|
|
930
|
-
// True when rules OUTSIDE the sharing block gate the space (unmanaged cloud
|
|
931
|
-
// rules or file-lane rules): the modal shows the "custom access rules
|
|
932
|
-
// active" chip linking to the access settings.
|
|
933
|
-
customRulesActive: z.boolean(),
|
|
934
|
-
// Plan-downgrade marker (X-23), mirrored from the access lane read.
|
|
935
|
-
accessExceedsPlan: z.boolean().default(false),
|
|
936
|
-
});
|
|
937
|
-
// ---------------------------------------------------------------------------
|
|
938
|
-
// 4. Lanes / nouns (additive types only — not wired)
|
|
939
|
-
// ---------------------------------------------------------------------------
|
|
940
|
-
// A channel is a named serving lane (e.g. "live", "staging") pointing at a
|
|
941
|
-
// version, with promote/rollback history.
|
|
942
|
-
export const channelSchema = z.object({
|
|
943
|
-
name: z.string().min(1),
|
|
944
|
-
versionId: z.string().min(1),
|
|
945
|
-
// Newest-first promote/rollback log: which version this channel pointed at.
|
|
946
|
-
history: z
|
|
947
|
-
.array(z.object({
|
|
948
|
-
versionId: z.string().min(1),
|
|
949
|
-
at: z.string().min(1).describe("ISO 8601 timestamp."),
|
|
950
|
-
}))
|
|
951
|
-
.default([]),
|
|
952
|
-
});
|
|
953
|
-
// Content-lane serving config (scalar, field-ownership / SSA semantics). No
|
|
954
|
-
// `spa` toggle — that mode is gone; a SPA is expressed as a `fallback`.
|
|
955
|
-
export const servingConfigSchema = z.object({
|
|
956
|
-
// Directory index document (e.g. "index.html").
|
|
957
|
-
index: z.string().min(1).optional(),
|
|
958
|
-
// Single fallback document served when no file/route matches (replaces the
|
|
959
|
-
// old `spa` boolean).
|
|
960
|
-
fallback: z.string().min(1).optional(),
|
|
961
|
-
// Whether directory listings are rendered when there is no index.
|
|
962
|
-
listing: z.boolean().optional(),
|
|
963
|
-
});
|
|
964
|
-
// The policy-lane document: an ordered, additive list of unified Rules. This is
|
|
965
|
-
// the firewall+access lane (own version + rollback, independent of content).
|
|
966
|
-
export const policyDocSchema = z.object({
|
|
967
|
-
rules: z.array(runtimeRuleSchema).default([]),
|
|
968
|
-
// Space-wide verifier issuers used by first-party access surfaces
|
|
969
|
-
// (callback/me/token), including public spaces that have no auth rules.
|
|
970
|
-
issuers: z.array(accessKeySchema).optional(),
|
|
971
|
-
// Runtime-serving session version. The enforcer rejects visitor tokens whose
|
|
972
|
-
// `sv` claim does not match and derives local `pw:` keys from this value.
|
|
973
|
-
sessionVersion: z.number().int().nonnegative().optional(),
|
|
974
|
-
});
|
|
975
|
-
// ---------------------------------------------------------------------------
|
|
976
|
-
// 5. Storage + resolution shapes (additive)
|
|
977
|
-
// ---------------------------------------------------------------------------
|
|
978
|
-
// The CLOUD-stored policy for one space. This is the persisted, versioned form
|
|
979
|
-
// of the policy lane: an ordered list of unified Rules plus a monotonic
|
|
980
|
-
// `version` so the lane can promote/rollback independently of content (mirrors
|
|
981
|
-
// `channelSchema.history`). `version` is bumped on every write; a write that
|
|
982
|
-
// supplies a stale `version` is a lost-update and is rejected by the resolver.
|
|
983
|
-
//
|
|
984
|
-
// Provenance: these are the "cloud" rules. They are merged with any "file"
|
|
985
|
-
// rules compiled from the space's `sf.jsonc` (which travel + roll back
|
|
986
|
-
// WITH a content version) to produce the effective policy — see
|
|
987
|
-
// `effectiveRuleSchema` / `effectivePolicySchema`.
|
|
988
|
-
export const spacePolicySchema = z.object({
|
|
989
|
-
// Ordered, additive Rules (first-match-wins at serving compile time). Same
|
|
990
|
-
// unified Rule the runtime verifies; an absent `auth` is an anonymous
|
|
991
|
-
// firewall rule (firewall ⊂ access).
|
|
992
|
-
rules: z.array(runtimeRuleSchema).default([]),
|
|
993
|
-
// Monotonic lane version. Starts at 1 on first write; every accepted write
|
|
994
|
-
// increments it. Used for optimistic concurrency (lost-update rejection) and
|
|
995
|
-
// for rollback to a prior lane revision.
|
|
996
|
-
version: z.number().int().nonnegative().default(0),
|
|
997
|
-
});
|
|
998
|
-
// GET/PUT /v1/spaces/:id/access-rules wire schemas — the ordered, always-latest
|
|
999
|
-
// access policy pair. Shared by the still-mounted /access route family and the
|
|
1000
|
-
// detached /access-rules core adapter so activation has exactly one schema to
|
|
1001
|
-
// retire, not two to reconcile.
|
|
1002
|
-
export const spaceAccessRulesWriteSchema = z.object({
|
|
1003
|
-
rules: z.array(runtimeRuleSchema).default([]),
|
|
1004
|
-
expectedVersion: z.number().int().nonnegative().optional(),
|
|
1005
|
-
});
|
|
1006
|
-
// The lane read plus the plan-downgrade marker (access-plan X-23): plan-gated
|
|
1007
|
-
// rules keep enforcing after a downgrade; this flag is the notice surface.
|
|
1008
|
-
export const spaceAccessRulesReadSchema = spacePolicySchema.extend({
|
|
1009
|
-
accessExceedsPlan: z.boolean().default(false),
|
|
1010
|
-
});
|
|
1011
|
-
// PUT/DELETE /v1/spaces/:id/quick-share wire schemas — the one active
|
|
1012
|
-
// whole-space guest link (§5.2).
|
|
1013
|
-
export const quickShareSchema = z.object({
|
|
1014
|
-
kind: z.literal("quick_share"),
|
|
256
|
+
.max(ACCESS_GRANT_SCOPE_LIMIT),
|
|
257
|
+
});
|
|
258
|
+
export const personGrantWriteSchema = z.object({
|
|
259
|
+
scope: accessScopePathSchema,
|
|
260
|
+
role: accessRoleSchema,
|
|
261
|
+
});
|
|
262
|
+
export const spacePersonGrantSetWriteSchema = z.object({
|
|
263
|
+
grants: z.array(personGrantWriteSchema).min(1).max(ACCESS_GRANT_SCOPE_LIMIT),
|
|
264
|
+
});
|
|
265
|
+
export const accessShareLinkCreateSchema = z.object({
|
|
266
|
+
name: z.string().trim().min(1).max(200),
|
|
267
|
+
landingPath: accessScopePathSchema.default("/"),
|
|
268
|
+
resources: grantResourceSetSchema,
|
|
269
|
+
// Anonymous commenting is a product default: a Link that names no
|
|
270
|
+
// capabilities admits reading the page and leaving a comment.
|
|
271
|
+
capabilities: linkCapabilitiesSchema.default(["page.view", "comments.read", "comments.write"]),
|
|
272
|
+
constraints: grantConstraintsSchema.default({}),
|
|
273
|
+
target: grantTargetSelectorSchema.default({ kind: "live" }),
|
|
274
|
+
});
|
|
275
|
+
export const accessShareLinkUpdateSchema = z.object({
|
|
276
|
+
expectedRevision: z.number().int().positive(),
|
|
277
|
+
name: z.string().trim().min(1).max(200).optional(),
|
|
278
|
+
landingPath: accessScopePathSchema.optional(),
|
|
279
|
+
resources: grantResourceSetSchema.optional(),
|
|
280
|
+
capabilities: linkCapabilitiesSchema.optional(),
|
|
281
|
+
constraints: grantConstraintsPatchSchema.optional(),
|
|
282
|
+
target: grantTargetSelectorSchema.optional(),
|
|
283
|
+
});
|
|
284
|
+
export const canonicalAccessRequestCreateSchema = z.object({
|
|
285
|
+
email: z.string().email(),
|
|
286
|
+
scope: accessScopePathSchema,
|
|
287
|
+
requestedRole: accessRequestRoleSchema.default("viewer"),
|
|
288
|
+
message: z.string().max(280).optional(),
|
|
289
|
+
});
|
|
290
|
+
export const canonicalAccessRequestApproveSchema = z.object({
|
|
291
|
+
scope: accessScopePathSchema.optional(),
|
|
292
|
+
role: accessRoleSchema.optional(),
|
|
293
|
+
});
|
|
294
|
+
export const openHandoffSchema = z.object({
|
|
1015
295
|
url: z.string().url(),
|
|
1016
|
-
expiresAt: z.string().
|
|
1017
|
-
|
|
296
|
+
expiresAt: z.string().datetime(),
|
|
297
|
+
kind: z.enum(["management", "claim_preview", "person"]),
|
|
1018
298
|
});
|
|
1019
|
-
export const
|
|
1020
|
-
|
|
299
|
+
export const openLinkSchema = z.object({
|
|
300
|
+
url: z.string().url(),
|
|
301
|
+
expiresAt: z.string().datetime().nullable(),
|
|
302
|
+
kind: z.literal("link"),
|
|
1021
303
|
});
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
export const enhancementsSchema = z.object({
|
|
1027
|
-
enabled: z.boolean(),
|
|
1028
|
-
active: z.array(z.string()),
|
|
304
|
+
export const privateOpenSchema = z.union([openLinkSchema, openHandoffSchema]);
|
|
305
|
+
export const openHandoffCreateSchema = z.object({
|
|
306
|
+
landingPath: accessScopePathSchema.default("/"),
|
|
307
|
+
targetHost: z.string().trim().min(1).max(255).optional(),
|
|
1029
308
|
});
|
|
1030
|
-
export const
|
|
1031
|
-
|
|
1032
|
-
// platform state (plan window, abuse/ban/billing) and PREPENDED above every
|
|
1033
|
-
// user-authored rule so a platform decision always wins first-match-wins; the
|
|
1034
|
-
// user can NEVER author a `platform` rule. `file` = compiled from the space's
|
|
1035
|
-
// `sf.jsonc` (timeless, rolls back with the content version). `cloud` =
|
|
1036
|
-
// the stored `spacePolicySchema` lane (incident interventions, password walls
|
|
1037
|
-
// set via the API/CLI, etc.). Provenance is surfaced so an operator reading the
|
|
1038
|
-
// effective policy can tell which surface to edit to change a given rule.
|
|
1039
|
-
export const ruleSourceSchema = z.enum(["platform", "file", "cloud"]);
|
|
1040
|
-
// A provenance-annotated effective Rule: the unified `runtimeRuleSchema` plus
|
|
1041
|
-
// the `source` it was contributed by and the `order` it occupies in the
|
|
1042
|
-
// resolved, first-match-wins list. This is the READ shape returned by the
|
|
1043
|
-
// effective-policy resolver — never stored. `order` is the 0-based index in the
|
|
1044
|
-
// flattened effective list (file rules then cloud rules, or whatever order the
|
|
1045
|
-
// resolver composes), letting a caller reconstruct match precedence.
|
|
1046
|
-
export const effectiveRuleSchema = runtimeRuleSchema.extend({
|
|
1047
|
-
source: ruleSourceSchema,
|
|
1048
|
-
order: z.number().int().nonnegative(),
|
|
309
|
+
export const teamSpaceAccessDefaultSchema = z.object({
|
|
310
|
+
newSpaceGrantPreset: spaceAccessPresetSchema,
|
|
1049
311
|
});
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
// rules were resolved from) so a reader can detect drift / pin a write against
|
|
1053
|
-
// the revision it observed. `fileVersion` is the content version id the `file`
|
|
1054
|
-
// rules were compiled from (null when the space has no compiled file rules).
|
|
1055
|
-
export const effectivePolicySchema = z.object({
|
|
1056
|
-
rules: z.array(effectiveRuleSchema).default([]),
|
|
1057
|
-
// Space-wide verifier issuers. The control plane always emits the platform
|
|
1058
|
-
// issuer here so identity-only public-space tokens verify without a rule.
|
|
1059
|
-
issuers: z.array(accessKeySchema).default([]),
|
|
1060
|
-
cloudVersion: z.number().int().nonnegative().default(0),
|
|
1061
|
-
fileVersion: z.string().min(1).nullable().default(null),
|
|
1062
|
-
// Serving-secret map (name -> value) the resolved rules reference by
|
|
1063
|
-
// password-acquire `ref` = "secret:<name>". Carries the space password's
|
|
1064
|
-
// bcrypt verifier hash, keyed so the runtime resolves it from
|
|
1065
|
-
// `serving['secrets']` and `password_verify`s against it. Never contains
|
|
1066
|
-
// plaintext the visitor types — only stored verifier hashes / shared secrets.
|
|
1067
|
-
secrets: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
1068
|
-
// Space session version (access-plan §3.1/§4.4): visitor tokens must carry a
|
|
1069
|
-
// matching `sv` claim. Bumped by logout-all; also an input to the space-local
|
|
1070
|
-
// `pw:` key derivation, so a bump invalidates every wall pass with zero new
|
|
1071
|
-
// state.
|
|
1072
|
-
sessionVersion: z.number().int().nonnegative().default(0),
|
|
312
|
+
export const teamSpaceAccessDefaultWriteSchema = z.object({
|
|
313
|
+
newSpaceGrantPreset: spaceAccessPresetSchema,
|
|
1073
314
|
});
|
|
1074
315
|
// ---------------------------------------------------------------------------
|
|
1075
|
-
//
|
|
316
|
+
// Unified visitor and collaboration tokens
|
|
1076
317
|
// ---------------------------------------------------------------------------
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
318
|
+
export const ACCESS_TOKEN_KID = "spacefast-runtime-v1";
|
|
319
|
+
export const ACCESS_TOKEN_ALG = "EdDSA";
|
|
320
|
+
export const RUNTIME_JWKS_WELL_KNOWN_FILENAME = "spacefast-runtime-jwks.json";
|
|
321
|
+
// Public identity attached only when a token consumer needs to render the
|
|
322
|
+
// person behind an authority. Collaboration tickets carry this audience-bound
|
|
323
|
+
// claim so Cast can paint an avatar without ever seeing an address: the
|
|
324
|
+
// minting side normalizes a provider avatar or derives one from the email,
|
|
325
|
+
// then drops the email. Raw email and provider-private profile metadata never
|
|
326
|
+
// belong here.
|
|
327
|
+
export const accessTokenPublicProfileSchema = z
|
|
328
|
+
.object({
|
|
329
|
+
name: z.string().min(1).max(120).optional(),
|
|
330
|
+
username: z.string().min(1).max(160).optional(),
|
|
331
|
+
avatar_url: z
|
|
332
|
+
.url({ protocol: /^https?$/ })
|
|
333
|
+
.max(2_048)
|
|
334
|
+
.optional(),
|
|
335
|
+
})
|
|
336
|
+
.strict();
|
|
337
|
+
export const accessTokenPayloadSchema = z.object({
|
|
338
|
+
sub: authorityReferenceSchema,
|
|
339
|
+
// Who this handoff authenticated, when it authenticated anybody. The account
|
|
340
|
+
// lane names the signed-in user; the invite and identity-connection lanes
|
|
341
|
+
// name the Person or external subject they just proved. Link, password,
|
|
342
|
+
// machine, and claim-preview lanes never set it, and its absence means the
|
|
343
|
+
// runtime session is anonymous. Identity comes from here alone — never
|
|
344
|
+
// inferred from the authority classes below.
|
|
345
|
+
principal: identifiedPrincipalSchema.optional(),
|
|
346
|
+
authorities: z.array(authorityReferenceSchema).min(1).max(16),
|
|
347
|
+
exp: z.number().int().nonnegative(),
|
|
348
|
+
nbf: z.number().int().nonnegative(),
|
|
349
|
+
iat: z.number().int().nonnegative(),
|
|
350
|
+
iss: z.string().min(1),
|
|
351
|
+
aud: z.string().min(1),
|
|
352
|
+
spaceId: z.string().min(1),
|
|
353
|
+
// Shared opaque identity for the runtime session and its access-origin
|
|
354
|
+
// collaboration session. It is random per handoff and never authorizes by itself.
|
|
355
|
+
sid: z.string().regex(/^[a-f0-9]{64}$/),
|
|
356
|
+
generation: z.number().int().nonnegative(),
|
|
357
|
+
emailVerified: z.boolean().default(false),
|
|
358
|
+
profile: accessTokenPublicProfileSchema.optional(),
|
|
359
|
+
jti: z.string().min(1),
|
|
1091
360
|
});
|