Skip to content

Commit c6330e8

Browse files
committed
Address review findings on the feature-gap pages
- Document visitor connector roles, the Connect button, and visitor_role_not_bindable. - Guard the Worker write examples by method and a bearer secret. - Quote shell placeholders as variables so copied commands run. - Encode the redeem URL with URLSearchParams. - Add steps to replace an identity connection without losing access. - State that removed members cannot rejoin a subteam with Join. - List every root Worker entry name. - State that a new connector run answers 201 even when it fails. - Separate unknown version IDs (exit 20) from unmatched refs (exit 2). - Pass --space explicitly after sf migrate, and state the claim deadline. - Match the sf feedback output sample to the CLI.
1 parent ea75c25 commit c6330e8

10 files changed

Lines changed: 87 additions & 36 deletions

File tree

‎content/(account)/auto-enroll.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ Only owners and admins can add or remove domains. An OAuth token needs the `team
1717
Over HTTP, send [`createTeamAutoEnrollEntry`](/api/reference/teams/createteamautoenrollentry):
1818

1919
```bash
20-
curl https://api.spacefast.com/v1/teams/<team-id>/auto-enroll \
21-
-H "Authorization: Bearer sfa_xxxxxxxx" \
20+
curl "https://api.spacefast.com/v1/teams/$TEAM_ID/auto-enroll" \
21+
-H "Authorization: Bearer $SPACEFAST_API_KEY" \
2222
-H "Content-Type: application/json" \
2323
-d '{"value":"example.com"}'
2424
```

‎content/(account)/identity-connections.mdx‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ The connection appears as a row with an **OIDC** or **Signer** badge and its iss
4343
Send [`createTeamIdentityConnection`](/api/reference/teams/createteamidentityconnection):
4444

4545
```bash
46-
curl https://api.spacefast.com/v1/teams/<team-id>/identity-connections \
47-
-H "Authorization: Bearer sfa_xxxxxxxx" \
46+
curl "https://api.spacefast.com/v1/teams/$TEAM_ID/identity-connections" \
47+
-H "Authorization: Bearer $SPACEFAST_API_KEY" \
4848
-H "Content-Type: application/json" \
4949
-d '{"type":"oidc","name":"Acme SSO","issuer":"https://login.example.com","clientId":"<client-id>","clientSecret":"<client-secret>"}'
5050
```
@@ -114,7 +114,7 @@ A `signer` connection sends visitors to your portal, and your portal vouches for
114114
1. The visitor presses **Continue with** on the access page. Spacefast redirects the browser to your `authorizeUrl` with the query parameters below.
115115
2. Your portal signs the visitor in and signs a proof token.
116116
3. Your server sends the proof to Spacefast and receives a short-lived token.
117-
4. Your portal sends the browser to `https://<host>/__sf/redeem?sf_token=<token>&return=<return>`. The Space sets a session cookie and opens the page.
117+
4. Your portal sends the browser to `/__sf/redeem` on `host`, with the `sf_token` and `return` query parameters. The Space sets a session cookie and opens the page.
118118

119119
| Query parameter | What it holds |
120120
| --- | --- |
@@ -143,14 +143,20 @@ Send the proof from your server, not from the browser:
143143

144144
```bash
145145
curl https://api.spacefast.com/identity/acn_xxxxxxxx/exchange \
146-
-H "Authorization: Bearer <proof-jwt>" \
146+
-H "Authorization: Bearer $PROOF_JWT" \
147147
-H "Content-Type: application/json" \
148148
-d '{"spaceId":"<spaceId>","host":"<host>","resource":"<return>","browserState":"<browserState>"}'
149149
```
150150

151151
A `200` answers with `token` and `expiresAt`. A `403` means the proof failed or no grant admits that subject to `resource`.
152152

153-
Redirect the browser to `/__sf/redeem` on the same `host` within 60 seconds. The token works once, and only in the browser that started the sign-in.
153+
Redirect the browser to `/__sf/redeem` on the same `host` within 60 seconds. The token works once, and only in the browser that started the sign-in. Encode each value as one query parameter. A `return` such as `/docs?tab=a&sort=b` holds `&` and `=`, so do not join the strings by hand:
154+
155+
```js
156+
const redeem = new URL(`https://${host}/__sf/redeem`);
157+
redeem.searchParams.set("sf_token", token);
158+
redeem.searchParams.set("return", returnPath);
159+
```
154160

155161
## Update a connection
156162

@@ -163,7 +169,7 @@ In **SSO providers**, press **Update** on the connection's row.
163169

164170
Leave **Replacement client secret** blank to keep the current secret. To rename a connection or change a signer's sign-in URL, use the API or the CLI.
165171

166-
[`updateTeamIdentityConnection`](/api/reference/teams/updateteamidentityconnection) accepts `name`, `clientId`, `clientSecret`, `authorizeUrl`, `publicKeys`, and `sessionTtlSeconds`. It accepts `sessionTtlSeconds` from 60 to 86,400. For a signer, Spacefast lowers any value above 1,800 to 1,800. `publicKeys` replaces the whole key set. The `type` and `issuer` cannot change. To change them, create a new connection.
172+
[`updateTeamIdentityConnection`](/api/reference/teams/updateteamidentityconnection) accepts `name`, `clientId`, `clientSecret`, `authorizeUrl`, `publicKeys`, and `sessionTtlSeconds`. It accepts `sessionTtlSeconds` from 60 to 86,400. For a signer, Spacefast lowers any value above 1,800 to 1,800. `publicKeys` replaces the whole key set. The `type` and `issuer` cannot change.
167173

168174
Grants that use the connection keep working. A change to the client ID, client secret, keys, or session lifetime sets `rotatedAt`.
169175

@@ -173,13 +179,20 @@ To rotate a signer key without a sign-in gap:
173179
2. Sign new proofs with the new key.
174180
3. Send a key set that holds only the new key.
175181

182+
### Replace a connection
183+
184+
A team can have one active connection per issuer. Revoking a connection also revokes its grants. Plan the order:
185+
186+
- **To move to a new issuer**, create the new connection and its grants first. Then revoke the old connection.
187+
- **To change the type for the same issuer**, revoke the old connection first, because the new one uses the same issuer. Then create the new connection and add its grants again. Visitors cannot sign in through that issuer until the new grants exist.
188+
176189
## Revoke a connection
177190

178191
Press **Revoke** on the row, then **Revoke connection** in the dialog. Over HTTP, send [`revokeTeamIdentityConnection`](/api/reference/teams/revoketeamidentityconnection). It answers `204`. From a terminal, run `sf share identity revoke`.
179192

180193
Revoking removes the grants too. Spacefast revokes every grant on the team's Spaces that trusts the connection's issuer, then the connection itself.
181194

182-
A revoked connection stays in [`listTeamIdentityConnections`](/api/reference/teams/listteamidentityconnections) with `revokedAt` set. It cannot be updated. To restore access, create a new connection and new grants.
195+
A revoked connection stays in [`listTeamIdentityConnections`](/api/reference/teams/listteamidentityconnections) with `revokedAt` set. It cannot be updated. To restore access, create a new connection and add its grants again.
183196

184197
## Errors
185198

‎content/(account)/subteams.mdx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Subteams need an Enterprise parent team. On every other plan, subteam operations
1212
## How subteams work
1313

1414
- **The parent's owners and admins are in every subteam.** They hold the same role there. To change that role, change it on the parent team.
15-
- **Parent members can join a subteam.** Every subteam appears in **Teams you can join** for parent members. Pressing **Join** makes them a subteam `member`, with no new seat.
15+
- **Parent members can join a subteam.** A subteam appears in **Teams you can join** for parent members. Pressing **Join** makes them a subteam `member`, with no new seat. A person removed from a subteam does not see it there. An owner or admin of the parent team must add them again.
1616
- **Seats are shared.** Seats count across the parent and all its subteams. A person in several teams of the family uses one seat.
1717
- **Subteams do not nest.** A subteam cannot have subteams of its own.
1818
- **Leaving the parent leaves every subteam.** When someone leaves or is removed from the parent team, they also lose every subteam membership.
@@ -32,8 +32,8 @@ Spacefast creates the subteam and opens it. Its URL slug starts with the parent'
3232
Over HTTP, send [`createSubTeam`](/api/reference/teams/createsubteam) with a `name` of 1 to 255 characters:
3333

3434
```bash
35-
curl https://api.spacefast.com/v1/teams/<team-id>/subteams \
36-
-H "Authorization: Bearer sfa_xxxxxxxx" \
35+
curl "https://api.spacefast.com/v1/teams/$TEAM_ID/subteams" \
36+
-H "Authorization: Bearer $SPACEFAST_API_KEY" \
3737
-H "Content-Type: application/json" \
3838
-d '{"name":"Design"}'
3939
```
@@ -51,7 +51,7 @@ The **Subteams** page shows the subteams you are in, each with **Open**. Below t
5151
Send [`updateSubTeam`](/api/reference/teams/updatesubteam) with a new `name`. The slug does not change. From a terminal:
5252

5353
```bash
54-
sf teams subteams rename <subteam-id> "Brand design"
54+
sf teams subteams rename "$SUBTEAM_ID" "Brand design"
5555
```
5656

5757
You can also open the subteam and rename it in **Team settings → General**.
@@ -61,9 +61,9 @@ You can also open the subteam and rename it in **Team settings → General**.
6161
Adding and removing members needs an owner or admin of the parent team.
6262

6363
```bash
64-
sf teams subteams members ls <subteam-id>
65-
sf teams subteams members add <subteam-id> <user-id>
66-
sf teams subteams members rm <subteam-id> <user-id>
64+
sf teams subteams members ls "$SUBTEAM_ID"
65+
sf teams subteams members add "$SUBTEAM_ID" "$USER_ID"
66+
sf teams subteams members rm "$SUBTEAM_ID" "$USER_ID"
6767
```
6868

6969
| Operation | What it does |

‎content/(dynamic)/functions.mdx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ Two more bindings appear on `env` when the version needs them. Run `sf functions
148148
| `env.CONNECTORS` | The version's capsule declares connector roles, or the Space has a connector binding | A connector role |
149149
| `env.ZERO` | The version includes a [Zero](/zero-runtime) capsule | The capsule's queries and mutations |
150150

151-
A Zero project ships a Worker when it also has a `functions/` directory or a root `handler.ts`. `sf publish` compiles the capsule and the Worker into one version.
151+
A Zero project ships a Worker when it also has a `functions/` directory or a root `handler.ts`, `handler.js`, or `handler.mjs`. `sf publish` compiles the capsule and the Worker into one version.
152152

153153
### `env.CONNECTORS`
154154

@@ -159,16 +159,20 @@ A Zero project ships a Worker when it also has a `functions/` directory or a roo
159159
```js handler.js
160160
export default {
161161
async fetch(request, env) {
162-
const notes = await env.ZERO.query("notes");
163-
await env.ZERO.mutate("addNote", "Written by the Worker");
164-
return Response.json(notes);
162+
if (request.method !== "POST") return Response.json(await env.ZERO.query("notes"));
163+
const secret = env.NOTES_SECRET;
164+
if (!secret || request.headers.get("authorization") !== `Bearer ${secret}`) {
165+
return new Response("Unauthorized", { status: 401 });
166+
}
167+
const { body } = await request.json();
168+
return Response.json(await env.ZERO.mutate("addNote", body), { status: 201 });
165169
},
166170
};
167171
```
168172

169173
`env.ZERO.query(name, ...args)` resolves to the query's data. `env.ZERO.mutate(name, ...args)` resolves to the mutation's return value. There is no call for actions. A failed call throws an `Error` with a `code`. The code is `zero_upstream_unavailable` when the capsule cannot be reached.
170174

171-
Both bindings forward the request's cookie. The runtime verifies the signed Zero identity cookie before it acts for that visitor. When no visitor is signed in, `env.ZERO` runs as the Space's service principal. In the capsule handler, `ctx.auth.userId` is then `service:<spaceId>` and `ctx.auth.provider` is `service`. `ctx.auth.isAuthenticated` is `true`, so a sign-in check alone does not keep the Worker out.
175+
Both bindings forward the request's cookie. The runtime verifies the signed Zero identity cookie before it acts for that visitor. When no visitor is signed in, `env.ZERO` runs as the Space's service principal. In the capsule handler, `ctx.auth.userId` is then `service:<spaceId>` and `ctx.auth.provider` is `service`. `ctx.auth.isAuthenticated` is `true`, so a sign-in check alone does not keep the Worker out. Check the caller in the Worker before it calls a mutation. The example requires a secret that you set with `sf env set NOTES_SECRET --value-from-stdin`.
172176

173177
## Limits
174178

‎content/(dynamic)/zero-connectors.mdx‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ export default capsule({
3535

3636
| Key | Required | What it does |
3737
| --- | --- | --- |
38-
| `as` | yes | Whose account the role uses: `team` or `owner` |
39-
| `connector` | no | The connector slug for the role. The dashboard offers only connections for this connector |
38+
| `as` | yes | Whose account the role uses: `team`, `owner`, or `visitor` |
39+
| `connector` | no | The connector slug for the role. The dashboard offers only connections for this connector. A `visitor` role needs it |
4040

4141
| `as` | Account | Condition |
4242
| --- | --- | --- |
4343
| `team` | A team connection | The connection allows sites to use it |
4444
| `owner` | Your own personal connection | The Space is private |
45+
| `visitor` | Each signed-in visitor's own account | The visitor connects it on the Space. See [Let visitors connect their own account](#let-visitors-connect-their-own-account) |
4546

4647
A role name starts with a lowercase letter and holds only letters and digits, up to 64 characters. The compiler reads the `connectors` map from source without running it, so write it as a literal. A map that is not a literal object of roles fails the publish with "capsule() connectors must be a role map."
4748

@@ -55,6 +56,7 @@ The connection must fit the role:
5556

5657
- A `team` role needs a team connection that allows sites to use it. Turn that on with **Allow sites to use this account** in the dashboard, `sf connectors connections edit <connection> --serving`, or [`updateConnection`](/api/reference/connectors/updateconnection) with `serving: true`.
5758
- An `owner` role needs a personal connection that you created, on a private Space. If the Space stops being private, Spacefast removes its owner bindings.
59+
- A `visitor` role cannot be bound. Binding one fails with `visitor_role_not_bindable`.
5860

5961
The request must act for a user who can manage the Space. A credential that acts for no user gets `forbidden`.
6062

@@ -88,6 +90,25 @@ sf connectors unbind tracker --space my-app --yes
8890

8991
All three need the `connectors:write` scope.
9092

93+
## Let visitors connect their own account
94+
95+
A `visitor` role calls the service as the signed-in visitor, with that visitor's own account. Nobody on the team binds it, and the dashboard row has no **Bind account** button.
96+
97+
```ts server/index.ts
98+
connectors: {
99+
myTracker: connector({ as: "visitor", connector: "linear-app" }),
100+
},
101+
```
102+
103+
1. A call returns `connect_required` until the visitor connects an account.
104+
2. `ConnectorGate` shows a **Connect** button for that state. The button opens the service's sign-in in a popup.
105+
3. A visitor who is not signed in to the Space signs in first.
106+
4. After the service sign-in, the popup closes and the gate runs the call again.
107+
108+
The connector must support OAuth sign-in. Each connection belongs to one visitor, one Space, and one role. For `reconnect_required`, the gate shows **Reconnect** instead.
109+
110+
To disconnect, call `disconnect()` on the result of `useConnectorState(result, role)`. To see who connected, call [`listVisitorConnectionUsers`](/api/reference/spaces/listvisitorconnectionusers). It returns counts and roles per Space user, not the accounts. To disconnect one user, call [`revokeVisitorConnection`](/api/reference/spaces/revokevisitorconnection).
111+
91112
## Call a role from Zero
92113

93114
`ctx.connectors.<role>` is on every query, mutation, action, and endpoint context. The property path after the role is the tool name, joined with dots. `ctx.connectors.tracker.issues.create(args)` calls the `issues.create` tool on the bound connection.
@@ -135,19 +156,27 @@ To read the state of one result yourself, call `useConnectorState(result)` from
135156

136157
## Call a role from a Worker
137158

138-
A [Worker](/functions) gets `env.CONNECTORS` when the version's capsule declares roles or the Space has a binding. To ship a Worker beside a capsule, add a `functions/` directory or a root `handler.ts` to the Zero project. `sf publish` compiles both into one version.
159+
A [Worker](/functions) gets `env.CONNECTORS` when the version's capsule declares roles or the Space has a binding. To ship a Worker beside a capsule, add a `functions/` directory or a root `handler.ts`, `handler.js`, or `handler.mjs` to the Zero project. `sf publish` compiles both into one version.
139160

140161
The role proxy works as it does in Zero. It resolves to the tool's result and throws a `ConnectorCallError` for any other state.
141162

142163
```js handler.js
143164
export default {
144165
async fetch(request, env) {
145-
const issue = await env.CONNECTORS.tracker.issues.create({ title: "Checkout is down" });
146-
return Response.json(issue);
166+
if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
167+
const secret = env.REPORT_SECRET;
168+
if (!secret || request.headers.get("authorization") !== `Bearer ${secret}`) {
169+
return new Response("Unauthorized", { status: 401 });
170+
}
171+
const { title } = await request.json();
172+
const issue = await env.CONNECTORS.tracker.issues.create({ title });
173+
return Response.json(issue, { status: 201 });
147174
},
148175
};
149176
```
150177

178+
A `team` role acts with the team's account for every request that reaches the Worker. Call a tool that writes only on a write method, and only after you check the caller. This example requires a secret that you set with `sf env set REPORT_SECRET --value-from-stdin`.
179+
151180
To handle every state without a throw, call `env.CONNECTORS.call(role, tool, args)`. It resolves to `{ state, run }`. Check `state.code` before you read `run`.
152181

153182
```js handler.js
@@ -173,8 +202,8 @@ Every call ends in one state. `state.message` carries the text below.
173202
| `ok` | Connected. | The tool ran |
174203
| `unbound` | The site owner needs to connect this service. | No usable connection is bound to the role, or the Space has no team. An owner binding stops working when its owner leaves the team |
175204
| `forbidden` | You do not have permission to use this connection. | The bound connection does not fit the role, policy blocks the tool, or a Zero read handler called a tool that writes |
176-
| `connect_required` | Connect your account to continue. | The bound connection is still waiting for authorization |
177-
| `reconnect_required` | Reconnect your account to continue. | The bound connection was revoked or needs a new sign-in |
205+
| `connect_required` | Connect your account to continue. | The bound connection is still waiting for authorization, or the visitor has not connected an account for a `visitor` role |
206+
| `reconnect_required` | Reconnect your account to continue. | The connection was revoked or needs a new sign-in |
178207
| `approval_pending` | This action is waiting for approval. | Policy parked the call. `pendingReason` is `approval`, and `approvalUrl` links to the decision |
179208
| `approval_pending` | Still working on it. Try again in a moment. | The run did not finish within 25 seconds. `pendingReason` is `running` |
180209
| `unavailable` | This service is unavailable. Try again. | The tool failed for another reason |
@@ -203,6 +232,7 @@ Each call has an idempotency key built from the version, the request id, the han
203232
| --- | --- |
204233
| `space_must_be_claimed` | The Space does not belong to a team |
205234
| `connector_role_unknown` | The live version does not declare the role |
235+
| `visitor_role_not_bindable` | The role is a `visitor` role. Each visitor connects their own account |
206236
| `connection_serving_not_granted` | A `team` role got a personal connection, or a team connection that does not allow sites to use it |
207237
| `owner_binding_requires_private_space` | An `owner` role got a connection that is not your personal one, or the Space is not private |
208238
| `forbidden` | The credential does not act for a user |

‎content/(publish)/migrate.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ To return at once and check later, pass `--no-wait`. The receipt has the build I
110110

111111
## Finish the move
112112

113-
1. To keep an anonymous copy, open the **Claim** link and pick a team. See [Publish without an account](/anonymous-and-claim).
113+
1. To keep an anonymous copy, open the **Claim** link and pick a team. An unclaimed copy stops serving after 33 hours and 20 minutes. See [The clock](/anonymous-and-claim#the-clock).
114114
2. To serve the copy on your own domain, attach the domain to the new Space. See [Custom domains](/domains).
115-
3. To edit the copy after you claim it, download it with `sf spaces download`. Change the files, then publish them with `sf publish`.
115+
3. To edit the copy after you claim it, download it with `sf spaces download --space` and the ID from the `Space` line. Change the files, then publish them with `sf publish --space` and the same ID.
116116

117117
## Errors
118118

‎content/(reference)/feedback.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The CLI prints the reference:
3333
```text
3434
Feedback ref: fdb_0123456789abcdef
3535
Thanks! We read every one.
36+
Signed in with `sf login`? Read it back with `sf feedback get fdb_0123456789abcdef`.
3637
```
3738

3839
With `--json`, `data` is `{ "ref": "fdb_…", "status": "stored" }`.

0 commit comments

Comments
 (0)