Skip to content

Commit

An access token can be regenerated: a new secret, its name, permissions, repositories and reach kept, and the old secret refused at once. Its page (yours under Settings, a workspace's under its settings for owners) has Regenerate with a new expiry, within the workspace's rules on lifetime, and shows the secret once; identity's regenerate_token logs it to the security log or the workspace's audit log, and refuses a revoked token. The token form's quick permissions have All, every resource at its highest level. A project's Observability settings no longer list the token g1t gives its apps on g1t.page: it is g1t's, and the page says apps there need none.

syntaqxcommitted Parentdbb4e2dBrowse files
14 files+260−420/14 viewed
+19−2
639639 a token that reads your notifications.
640640 5. Under **Permissions**, set each resource the token needs to a level.
641641 **Read only**, **Agent** and **CI** fill in a [preset](#presets);
642− **Clear** sets everything back to no access.
642+ **All** sets every resource to its highest level; **Clear** sets
643+ everything back to no access.
643644 6. Leave **Use the website as you**, under **Website**, off unless the
644645 token is for automation that drives a browser. See
645646 [use a token on the website](#use-a-token-on-the-website).
663664 itself stays the
664665 same; the change applies from its next request. Widening a token made for
665666 a workspace that requires approval asks its owners again. Where it reaches
666−and when it expires cannot change; make a new token instead.
667+cannot change; make a new token instead.
667668
669+### Regenerate a token
670+
671+To rotate a token, or replace one that may have leaked, open its page,
672+choose when the new one expires, and select **Regenerate**. The token gets
673+a new secret, shown once, and keeps its name, permissions, repositories
674+and where it reaches. The old secret stops working at once, so give
675+whatever used it the new one. A token made for a workspace keeps to that
676+workspace's [rules](#a-workspaces-rules-for-tokens) on how long it may
677+last. A workspace's owners regenerate its own tokens the same way, from
678+**Workspace settings** → **Access tokens**. Each regeneration is in your
679+[security log](#security-log), or the workspace's audit log.
680+
668681 **Delete token**, at the bottom of its page, stops it working at once.
669682
670683 ### Use a token on the website
876889 | Agent | Every resource at read except Self-hosted runners and Chat, and Code, Issues, Pull requests, Memory and context and Notifications at read and write, and g1t agents at run. Reads everything, works on issues and pull requests, pushes code, puts g1t to work, and answers your inbox. No admin level. |
877890 | CI | Repositories: read; Code, Packages, Workflows, Checks and statuses, and Deployments: read and write. Clones and pushes code, pushes and pulls packages, runs workflows, and reports [checks](/guides/checks/) and deployments. |
878891
892+**All** is not a preset that grows: it ticks every permission there is
893+when you select it. A permission g1t adds later is not added to a token on
894+its own; edit the token to give it, as with any other.
895+
879896 A new personal token starts on Read only; a new workspace token on CI.
880897
881898 ## Scopes
+14−0
235235 const blocked = Boolean(one && !workspaceOwned && chosen?.policy && !chosen.policy.allowTokensForThisWorkspace);
236236 const keptOut = !workspaceOwned && reach === ALL_WORKSPACES ? keptOutOfAll(workspaces) : [];
237237 const choosePreset = (id: PresetId) => setLevels(permissionsOf(presetScopes(id)));
238+ // Every permission this kind of token can have, each at its highest level.
239+ const chooseAll = () =>
240+ setLevels(Object.fromEntries(groups.flatMap((group) => group.resources.map((row) => [row.resource, levelsOf(row.resource).at(-1)!]))) as typeof levels);
238241
239242 return (
240243 <div className="space-y-8">
341344 </Button>
342345 </Hint>
343346 ))}
347+ <Hint label="Every permission, each at its highest level. A permission g1t adds later is added to the token by editing it.">
348+ <Button
349+ type="button"
350+ onClick={chooseAll}
351+ variant="outline"
352+ size="inline"
353+ className="rounded-full px-2.5 py-0.5 text-xs text-muted"
354+ >
355+ All
356+ </Button>
357+ </Hint>
344358 <Button
345359 type="button"
346360 onClick={() => setLevels({})}
+67−0
1+/**
2+ * Regenerating a token: a new secret for it, everything else kept, the
3+ * old secret refused from then on. For rotating a token without remaking
4+ * it, or replacing one that may have leaked. The page's action answers
5+ * `intent=regenerate` with `{ secret, token }`, shown once by `TokenCreated`.
6+ */
7+import { RefreshCw } from "lucide-react";
8+import { useState } from "react";
9+import { Form } from "react-router";
10+
11+import {
12+ AlertDialog,
13+ AlertDialogAction,
14+ AlertDialogCancel,
15+ AlertDialogContent,
16+ AlertDialogDescription,
17+ AlertDialogFooter,
18+ AlertDialogHeader,
19+ AlertDialogTitle,
20+ AlertDialogTrigger,
21+} from "./ui/alert-dialog";
22+import { Button } from "./ui/button";
23+import { SelectField } from "./ui/select";
24+import { DEFAULT_EXPIRY_DAYS, EXPIRY_DAYS, describeDays } from "../lib/access-tokens";
25+
26+export function RegenerateToken({ name }: { name: string }) {
27+ const [expires, setExpires] = useState(String(DEFAULT_EXPIRY_DAYS));
28+ const options = [...EXPIRY_DAYS.map((days) => ({ value: String(days), label: describeDays(days) })), { value: "never", label: "No expiration" }];
29+ return (
30+ <section className="rounded-xl border border-line p-4">
31+ <h2 className="text-sm font-medium">Regenerate this token</h2>
32+ <p className="mt-1 text-xs text-muted">
33+ A new secret, with the same name, permissions and reach. Anything using the old one stops working at once, until you give it the new
34+ one.
35+ </p>
36+ <div className="mt-3 flex flex-wrap items-center gap-2">
37+ <SelectField aria-label="Expires" className="w-40" value={expires} onValueChange={setExpires} options={options} />
38+ <AlertDialog>
39+ <AlertDialogTrigger asChild>
40+ <Button variant="outline">
41+ <RefreshCw size={14} />
42+ Regenerate
43+ </Button>
44+ </AlertDialogTrigger>
45+ <AlertDialogContent>
46+ <Form method="post" className="grid gap-4">
47+ <input type="hidden" name="intent" value="regenerate" />
48+ <input type="hidden" name="expires" value={expires} />
49+ <AlertDialogHeader>
50+ <AlertDialogTitle>Regenerate {name}?</AlertDialogTitle>
51+ <AlertDialogDescription>
52+ Its old secret stops working now. The new one is shown once, and {expires === "never" ? "does not expire" : `expires in ${describeDays(Number(expires))}`}.
53+ </AlertDialogDescription>
54+ </AlertDialogHeader>
55+ <AlertDialogFooter>
56+ <AlertDialogCancel>Cancel</AlertDialogCancel>
57+ <AlertDialogAction asChild>
58+ <button type="submit">Regenerate token</button>
59+ </AlertDialogAction>
60+ </AlertDialogFooter>
61+ </Form>
62+ </AlertDialogContent>
63+ </AlertDialog>
64+ </div>
65+ </section>
66+ );
67+}
+11−0
6060 return { days, never };
6161 }
6262
63+/** A new lifetime chosen in a form's `expires` (days, or `never`), as seconds; null for none, or an error. */
64+export function ttlFromForm(form: FormData): { ok: true; value: number | null } | { ok: false; error: string } {
65+ const expires = String(form.get("expires") ?? DEFAULT_EXPIRY_DAYS);
66+ if (expires === "never") return { ok: true, value: null };
67+ const days = Number(expires);
68+ if (!Number.isInteger(days) || days < 1 || days > MAX_TOKEN_LIFETIME_DAYS) {
69+ return { ok: false, error: `Choose when it expires: at most ${MAX_TOKEN_LIFETIME_DAYS} days, or never.` };
70+ }
71+ return { ok: true, value: days * 86_400 };
72+}
73+
6374 /** The resources a token can hold: a workspace's own token holds none about a person. */
6475 export function resourcesFor(workspaceOwned: boolean): { resource: ScopeResource; label: string; group: ResourceGroup }[] {
6576 return SCOPE_RESOURCES.filter((row) => !workspaceOwned || row.group !== "account");
+29−33
110110 export default function ObservabilitySettings({ loaderData, actionData }: Route.ComponentProps) {
111111 const { settings, tokens, exports, usage, slug } = loaderData;
112112 const submit = useSubmit();
113− const deploymentToken = tokens?.find((t) => t.managed);
114113
115114 return (
116115 <div className="mx-auto max-w-4xl">
153152 </h2>
154153 <NewToken />
155154 </div>
156− <p className="mt-1 text-sm text-muted">A token lets software send to this project and do nothing else. Each is shown once, when it is made.</p>
155+ <p className="mt-1 text-sm text-muted">
156+ For software running anywhere other than g1t.page: a token lets it send to this project and do nothing else. Each is shown once, when
157+ it is made. Apps on g1t.page need none; g1t gives them their own.
158+ </p>
157159 {!tokens ? (
158160 <Card className="mt-3 p-4 text-sm text-muted">Tokens could not be loaded just now.</Card>
159161 ) : tokens.length === 0 ? (
160− <Card className="mt-3 p-4 text-sm text-muted">No tokens yet.</Card>
162+ <Card className="mt-3 p-4 text-sm text-muted">None yet.</Card>
161163 ) : (
162164 <Card className="mt-3 divide-y divide-line">
163165 {tokens.map((t) => (
166168 <p className="flex items-center gap-2 text-sm font-medium">
167169 <KeyRound size={14} className="text-faint" />
168170 {t.name}
169− {t.managed && <Badge tone="accent">g1t</Badge>}
170171 </p>
171172 <p className="mt-0.5 text-xs text-faint">
172173 <span className="font-mono">{t.hint}</span> · made <TimeAgo at={t.created_us / 1000} /> ·{" "}
179180 )}
180181 </p>
181182 </div>
182− {t.managed ? (
183− <span className="text-xs text-faint">Given to the project's apps on g1t.page</span>
184− ) : (
185− <AlertDialog>
186− <AlertDialogTrigger asChild>
187− <Button variant="outline" size="sm" aria-label={`Revoke ${t.name}`}>
188− <Trash2 size={14} />
189− Revoke
190− </Button>
191− </AlertDialogTrigger>
192− <AlertDialogContent>
193− <Form method="post" className="grid gap-4">
194− <input type="hidden" name="intent" value="revoke-token" />
195− <input type="hidden" name="id" value={t.id} />
196− <AlertDialogHeader>
197− <AlertDialogTitle>Revoke {t.name}?</AlertDialogTitle>
198− <AlertDialogDescription>Anything sending with it is refused at once. What it already sent stays.</AlertDialogDescription>
199− </AlertDialogHeader>
200− <AlertDialogFooter>
201− <AlertDialogCancel>Cancel</AlertDialogCancel>
202− <AlertDialogAction asChild>
203− <button type="submit">Revoke token</button>
204− </AlertDialogAction>
205− </AlertDialogFooter>
206− </Form>
207− </AlertDialogContent>
208− </AlertDialog>
209− )}
183+ <AlertDialog>
184+ <AlertDialogTrigger asChild>
185+ <Button variant="outline" size="sm" aria-label={`Revoke ${t.name}`}>
186+ <Trash2 size={14} />
187+ Revoke
188+ </Button>
189+ </AlertDialogTrigger>
190+ <AlertDialogContent>
191+ <Form method="post" className="grid gap-4">
192+ <input type="hidden" name="intent" value="revoke-token" />
193+ <input type="hidden" name="id" value={t.id} />
194+ <AlertDialogHeader>
195+ <AlertDialogTitle>Revoke {t.name}?</AlertDialogTitle>
196+ <AlertDialogDescription>Anything sending with it is refused at once. What it already sent stays.</AlertDialogDescription>
197+ </AlertDialogHeader>
198+ <AlertDialogFooter>
199+ <AlertDialogCancel>Cancel</AlertDialogCancel>
200+ <AlertDialogAction asChild>
201+ <button type="submit">Revoke token</button>
202+ </AlertDialogAction>
203+ </AlertDialogFooter>
204+ </Form>
205+ </AlertDialogContent>
206+ </AlertDialog>
210207 </div>
211208 ))}
212209 </Card>
213210 )}
214− {!deploymentToken && <p className="mt-2 text-xs text-faint">The project's apps on g1t.page get a token of their own at their next deployment.</p>}
215211 </section>
216212
217213 <section className="mt-10" aria-labelledby="exports-title">
+13−2
55 import { page } from "../../lib/meta";
66 import { ErrorText, SubmitButton } from "../../components/ui";
77 import { TokenForm } from "../../components/token-form";
8−import { TokenBadges, TokenMeta } from "../../components/token-list";
9−import { changesTo, tokenFromForm } from "../../lib/access-tokens";
8+import { TokenBadges, TokenCreated, TokenMeta } from "../../components/token-list";
9+import { RegenerateToken } from "../../components/token-regenerate";
10+import { changesTo, tokenFromForm, ttlFromForm } from "../../lib/access-tokens";
1011 import { personalToken, workspaceChoices } from "../../lib/access-tokens.server";
1112 import { identity } from "../../lib/services.server";
1213 import { assertSameOrigin, requireUser } from "../../lib/session.server";
3334 await identity.removeAccessToken(user, params.id);
3435 throw redirect("/settings/tokens");
3536 }
37+ if (form.get("intent") === "regenerate") {
38+ const ttl = ttlFromForm(form);
39+ if (!ttl.ok) return { error: ttl.error, saved: false };
40+ const made = await identity.regenerateToken(user, params.id, ttl.value);
41+ return made.ok ? { error: null, saved: false, secret: made.value.token, made: made.value.info } : { error: made.error.message, saved: false };
42+ }
3643 const token = await personalToken(user, params.id);
3744 const input = tokenFromForm(form, { editing: true });
3845 if (!input.ok) return { error: input.error, saved: false };
5663 </h2>
5764 <TokenMeta token={token} />
5865 </div>
66+ {actionData && "secret" in actionData && actionData.secret && actionData.made && (
67+ <TokenCreated secret={actionData.secret} token={actionData.made} back="/settings/tokens" />
68+ )}
5969 <Form method="post" key={token.id} className="space-y-6">
6070 <input type="hidden" name="intent" value="save" />
6171 <TokenForm workspaces={workspaces} editing={token} />
7282 : "The token stays the same; what it may do changes from its next request."}
7383 </p>
7484 </Form>
85+ <RegenerateToken name={token.name} />
7586 <section className="rounded-xl border border-danger/30 p-4">
7687 <h2 className="text-sm font-medium">Delete this token</h2>
7788 <p className="mt-1 text-xs text-muted">Anything still using it stops working at once.</p>
+13−2
55 import { page } from "../../lib/meta";
66 import { ErrorText, SubmitButton } from "../../components/ui";
77 import { TokenForm } from "../../components/token-form";
8−import { TokenBadges, TokenFacts, TokenMeta } from "../../components/token-list";
9−import { changesTo, tokenFromForm } from "../../lib/access-tokens";
8+import { TokenBadges, TokenCreated, TokenFacts, TokenMeta } from "../../components/token-list";
9+import { RegenerateToken } from "../../components/token-regenerate";
10+import { changesTo, tokenFromForm, ttlFromForm } from "../../lib/access-tokens";
1011 import { workspaceRepos, workspaceToken } from "../../lib/access-tokens.server";
1112 import { identity } from "../../lib/services.server";
1213 import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
3637 if (!removed.ok) return { error: removed.error.message, saved: false };
3738 throw redirect(`/${slug}/-/tokens`);
3839 }
40+ if (form.get("intent") === "regenerate") {
41+ const ttl = ttlFromForm(form);
42+ if (!ttl.ok) return { error: ttl.error, saved: false };
43+ const made = await identity.regenerateToken(user, params.id, ttl.value, slug);
44+ return made.ok ? { error: null, saved: false, secret: made.value.token, made: made.value.info } : { error: made.error.message, saved: false };
45+ }
3946 const token = await workspaceToken(user, slug, params.id);
4047 const input = tokenFromForm(form, { editing: true, workspaceOwned: true, owner: slug });
4148 if (!input.ok) return { error: input.error, saved: false };
6067 {token.description && <p className="text-sm text-muted">{token.description}</p>}
6168 <TokenMeta token={token} />
6269 </div>
70+ {actionData && "secret" in actionData && actionData.secret && actionData.made && (
71+ <TokenCreated secret={actionData.secret} token={actionData.made} back={`/${slug}/-/tokens`} />
72+ )}
6373 {owner ? (
6474 <>
6575 <Form method="post" key={token.id} className="space-y-6">
7383 {actionData?.saved && <span className="text-sm text-success">Saved.</span>}
7484 </div>
7585 </Form>
86+ <RegenerateToken name={token.name} />
7687 <section className="rounded-xl border border-danger/30 p-4">
7788 <h3 className="text-sm font-medium">Delete this token</h3>
7889 <p className="mt-1 text-xs text-muted">Anything still using it stops working at once.</p>
+17−0
146146 pub website: Option<bool>,
147147 }
148148
149+/// `regenerate_token`: a person gives a token of theirs, or, as an
150+/// owner, a workspace's token, a new secret. Everything else about it
151+/// stays: its name, permissions, reach and repositories. The old secret
152+/// stops working at once. `ttl_seconds` is its new lifetime from now, as
153+/// when it was made (null: it does not expire), within its workspace's
154+/// rules. Returns `Outcome<CreatedAccessToken>`, the new secret shown once.
155+#[derive(Debug, Default, Serialize, Deserialize)]
156+pub struct RegenerateTokenArgs {
157+ pub actor: User,
158+ pub id: String,
159+ /// The workspace whose token it is; null for one of the actor's own.
160+ #[serde(default)]
161+ pub owner: Option<String>,
162+ #[serde(default)]
163+ pub ttl_seconds: Option<u64>,
164+}
165+
149166 /// A workspace's rules for personal access tokens.
150167 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
151168 #[serde(rename_all = "camelCase")]
+2−0
126126 permissions: change.permissions ?? null,
127127 website: change.website ?? null,
128128 }),
129+ regenerateToken: (actor, id, ttlSeconds, owner) =>
130+ call("regenerate_token", { actor, id, owner: owner ?? null, ttl_seconds: ttlSeconds ?? null }),
129131 getTokenPolicy: (slug, viewer) => call("get_token_policy", { slug, viewer }),
130132 setTokenPolicy: (actor, slug, change) =>
131133 call("set_token_policy", {
+7−0
933933 * workspace that approves tokens asks for approval again.
934934 */
935935 updateToken(actor: User, id: string, change: TokenChange, owner?: string | null): Promise<Result<AccessToken>>;
936+ /**
937+ * A new secret for a token, everything else kept, the old secret refused
938+ * from then on: rotating it. `ttlSeconds` is its new lifetime from now
939+ * (null: it does not expire), within its workspace's rules. The secret
940+ * is shown once.
941+ */
942+ regenerateToken(actor: User, id: string, ttlSeconds: number | null, owner?: string | null): Promise<Result<{ token: string; info: AccessToken }>>;
936943 /** A workspace's rules for personal access tokens. Members only. */
937944 getTokenPolicy(slug: string, viewer: Viewer): Promise<Result<TokenPolicy>>;
938945 /** Owners only, as people. `maxLifetimeDays` of 0 removes the limit. */
+1−0
10181018 // workspaces' rules for tokens; see token_reach.rs.
10191019 "create_token" => reply(&identity.create_token(args(body)?).await?),
10201020 "update_token" => reply(&identity.update_token(args(body)?).await?),
1021+ "regenerate_token" => reply(&identity.regenerate_token(args(body)?).await?),
10211022 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
10221023 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
10231024 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
+59−0
715715 Ok(Outcome::Ok(info))
716716 }
717717
718+ /// A new secret for a token, everything else kept: for rotating one
719+ /// without remaking it. See `RegenerateTokenArgs`.
720+ pub async fn regenerate_token(&self, a: RegenerateTokenArgs) -> Result<Outcome<CreatedAccessToken>> {
721+ if !is_person(&a.actor) || a.actor.token.is_some() {
722+ return Ok(Outcome::fail(FailureCode::Forbidden, "Only a person, signed in on g1t.sh, can regenerate an access token."));
723+ }
724+ if a.ttl_seconds.is_some_and(|ttl| !(DAY_SECONDS..=u64::from(MAX_LIFETIME_DAYS) * DAY_SECONDS).contains(&ttl)) {
725+ return Ok(Outcome::fail(
726+ FailureCode::Invalid,
727+ format!("A token lasts between 1 and {MAX_LIFETIME_DAYS} days, or does not expire."),
728+ ));
729+ }
730+ let owner_slug = a.owner.as_deref().map(|slug| slug.trim().to_lowercase()).filter(|slug| !slug.is_empty());
731+ let found = match (&owner_slug, self.owned_token(&a.id).await?) {
732+ (Some(slug), Some(token)) => {
733+ let workspace_id = match self.owned_workspace(&a.actor, slug).await? {
734+ Outcome::Ok(id) => id,
735+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
736+ };
737+ if token.workspace_id.as_deref() != Some(workspace_id.as_str()) {
738+ return Ok(Outcome::fail(FailureCode::NotFound, "No such token."));
739+ }
740+ token
741+ }
742+ (None, Some(token)) if token.user_id.as_deref() == Some(a.actor.id.as_str()) => token,
743+ _ => return Ok(Outcome::fail(FailureCode::NotFound, "No such token.")),
744+ };
745+ if found.status.as_deref() == Some(TokenStatus::Revoked.as_str()) {
746+ return Ok(Outcome::fail(FailureCode::Conflict, "This token was revoked. Make a new one instead."));
747+ }
748+ // A personal token made for one workspace keeps to its rules on lifetime.
749+ if found.user_id.is_some()
750+ && let Some(id) = &found.owner_workspace_id
751+ && let Some(slug) = self.slug_of(id).await?
752+ && let Some((_, policy)) = self.policy_of(&slug).await?
753+ && let Some(refusal) = policy.refusal(&slug, true, a.ttl_seconds)
754+ {
755+ return Ok(Outcome::fail(FailureCode::Forbidden, refusal));
756+ }
757+ let token = format!("{}{}", crate::tokens::TOKEN_PREFIX, crate::crypto::random_hex(20));
758+ let expires_at = a.ttl_seconds.map(|ttl| rfc3339(now_ms() + ttl * 1000));
759+ self.db
760+ .prepare("UPDATE access_tokens SET token_hash = ?, expires_at = ? WHERE id = ?")
761+ .bind(&[crate::crypto::sha256_hex(&token).into(), text(expires_at.as_deref()), a.id.as_str().into()])?
762+ .run()
763+ .await?;
764+ let Some(mut info) = self.token_info(&a.id).await? else {
765+ return Ok(Outcome::fail(FailureCode::NotFound, "No such token."));
766+ };
767+ self.name_repositories(std::slice::from_mut(&mut info), &Some(a.actor.clone())).await?;
768+ if found.user_id.is_some() {
769+ self.log_security(&a.actor.id, "token_regenerated", Some(&info.name), None).await;
770+ self.audit_account(&a.actor, "token.regenerated", &format!("Regenerated access token {}", info.name)).await;
771+ } else if let Some(slug) = &owner_slug {
772+ self.audit_workspace(&a.actor, "workspace_token.regenerated", slug, Surface::Web, format!("Regenerated workspace access token {}", info.name)).await;
773+ }
774+ Ok(Outcome::Ok(CreatedAccessToken { token, info }))
775+ }
776+
718777 /// A token a person or workspace made on purpose, by id: never an
719778 /// agent's or a workflow job's.
720779 async fn owned_token(&self, id: &str) -> Result<Option<Owned>> {
+3−2
259259 assert.match(a.secret, /^g1to_prj_1_[0-9a-f]{48}$/);
260260 const made = await store.createToken("prj_1", "laptop", "u1");
261261 const listed = store.tokens();
262− assert.equal(listed.length, 2);
262+ assert.deepEqual(listed.map((t) => t.name), ["laptop"], "the deployments' token is g1t's, not listed");
263263 assert.ok(!JSON.stringify(listed).includes(made.secret), "a token's secret is never listed");
264− assert.equal(store.revokeToken(listed.find((t) => t.managed)!.id), false);
264+ const managedId = String((store.sql.exec("SELECT id FROM tokens WHERE managed = 1").toArray()[0] as { id: string }).id);
265+ assert.equal(store.revokeToken(managedId), false);
265266 assert.equal(store.revokeToken(made.token.id), true);
266267 assert.equal(await store.checkToken(made.secret), false);
267268 });
+5−1
193193 };
194194 }
195195
196+ /**
197+ * The tokens people made. The one g1t's deployments send with is kept
198+ * out of sight: it is g1t's, given to the project's apps on their own.
199+ */
196200 tokens(): IngestToken[] {
197− return this.rows("SELECT * FROM tokens ORDER BY managed DESC, created_us DESC").map((r) => this.tokenRow(r));
201+ return this.rows("SELECT * FROM tokens WHERE managed = 0 ORDER BY created_us DESC").map((r) => this.tokenRow(r));
198202 }
199203
200204 /** A new token: `g1to_<project>_<secret>`, so the ingest door knows whose store to ask. */