@skillcdn/console 0.1.6 → 0.1.7

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.
Files changed (88) hide show
  1. package/README.md +50 -2
  2. package/SKILLCDN.md +29 -0
  3. package/dist/cli/cli.d.ts +3 -0
  4. package/dist/cli/cli.d.ts.map +1 -1
  5. package/dist/cli/cli.js +61 -0
  6. package/dist/cli/cli.js.map +1 -1
  7. package/dist/cli/help.d.ts.map +1 -1
  8. package/dist/cli/help.js +5 -0
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/main.js +2 -0
  11. package/dist/cli/main.js.map +1 -1
  12. package/dist/cli/serve.d.ts +37 -0
  13. package/dist/cli/serve.d.ts.map +1 -0
  14. package/dist/cli/serve.js +218 -0
  15. package/dist/cli/serve.js.map +1 -0
  16. package/dist/console.d.ts +6 -0
  17. package/dist/console.d.ts.map +1 -1
  18. package/dist/console.js +78 -13
  19. package/dist/console.js.map +1 -1
  20. package/dist/data.d.ts.map +1 -1
  21. package/dist/data.js +5 -1
  22. package/dist/data.js.map +1 -1
  23. package/dist/i18n/en.d.ts +4 -0
  24. package/dist/i18n/en.d.ts.map +1 -1
  25. package/dist/i18n/en.js +4 -0
  26. package/dist/i18n/en.js.map +1 -1
  27. package/dist/i18n/ko.d.ts.map +1 -1
  28. package/dist/i18n/ko.js +4 -0
  29. package/dist/i18n/ko.js.map +1 -1
  30. package/dist/schemas.d.ts +1 -0
  31. package/dist/schemas.d.ts.map +1 -1
  32. package/dist/schemas.js +5 -0
  33. package/dist/schemas.js.map +1 -1
  34. package/dist/web.d.ts +16 -0
  35. package/dist/web.d.ts.map +1 -0
  36. package/dist/web.js +149 -0
  37. package/dist/web.js.map +1 -0
  38. package/docs/specs/cli.md +92 -0
  39. package/docs/specs/rest.md +220 -0
  40. package/package.json +14 -3
  41. package/skills/building-a-console/SKILL.md +79 -0
  42. package/skills/working-the-board/SKILL.md +78 -0
  43. package/src/api.ts +235 -0
  44. package/src/cli/cli.ts +1114 -0
  45. package/src/cli/credentials.ts +68 -0
  46. package/src/cli/directory.ts +59 -0
  47. package/src/cli/format.ts +284 -0
  48. package/src/cli/help.ts +326 -0
  49. package/src/cli/main.ts +39 -0
  50. package/src/cli/serve.ts +276 -0
  51. package/src/cli/terminal.ts +28 -0
  52. package/src/client.ts +533 -0
  53. package/src/components/board.tsx +112 -0
  54. package/src/components/connect.tsx +126 -0
  55. package/src/components/decision-form.tsx +136 -0
  56. package/src/components/decision-list.tsx +278 -0
  57. package/src/components/documents.tsx +569 -0
  58. package/src/components/event-feed.tsx +129 -0
  59. package/src/components/markdown.tsx +72 -0
  60. package/src/components/people.tsx +59 -0
  61. package/src/components/projects.tsx +364 -0
  62. package/src/components/runs.tsx +175 -0
  63. package/src/components/shell.tsx +284 -0
  64. package/src/components/sign-in.tsx +61 -0
  65. package/src/components/skills.tsx +78 -0
  66. package/src/components/task-form.tsx +229 -0
  67. package/src/components/task-view.tsx +272 -0
  68. package/src/components/tokens.tsx +223 -0
  69. package/src/components/ui.tsx +219 -0
  70. package/src/connect-page.tsx +148 -0
  71. package/src/connect.ts +27 -0
  72. package/src/console.tsx +1166 -0
  73. package/src/data.ts +493 -0
  74. package/src/docs-page.tsx +334 -0
  75. package/src/documents.ts +76 -0
  76. package/src/i18n/en.ts +537 -0
  77. package/src/i18n/index.ts +76 -0
  78. package/src/i18n/ko.ts +511 -0
  79. package/src/index.ts +142 -0
  80. package/src/limits.ts +61 -0
  81. package/src/router.ts +295 -0
  82. package/src/routes.ts +106 -0
  83. package/src/schemas.ts +776 -0
  84. package/src/styles/console.css +2432 -0
  85. package/src/text.ts +33 -0
  86. package/src/use-action.ts +37 -0
  87. package/src/vocabulary.ts +109 -0
  88. package/src/web.ts +170 -0
package/src/limits.ts ADDED
@@ -0,0 +1,61 @@
1
+ // How much of anything the console takes in. Every text a person or an agent writes is bounded
2
+ // at the edge (AGENTS.md, rule 2); these are the bounds, shared by the server that enforces them
3
+ // and the pages that say so before a request is made.
4
+
5
+ /** A task's title, a decision's question: one line. */
6
+ export const MAX_TITLE_LENGTH = 200;
7
+ export const MAX_QUESTION_LENGTH = 500;
8
+ /** A body in Markdown: a task's, or the context of a decision. */
9
+ export const MAX_BODY_LENGTH = 20_000;
10
+ /** The links of a task: a repository, a pull request, a document. */
11
+ export const MAX_LINKS = 20;
12
+ export const MAX_LINK_LABEL_LENGTH = 120;
13
+ export const MAX_URL_LENGTH = 2048;
14
+ /** The options of a decision, and how a person words each. */
15
+ export const MIN_OPTIONS = 2;
16
+ export const MAX_OPTIONS = 10;
17
+ export const MAX_OPTION_LABEL_LENGTH = 120;
18
+ /** What a person adds to an answer. */
19
+ export const MAX_NOTE_LENGTH = 2000;
20
+ /** How many tasks or decisions one listing answers with. A board is read whole, for now. */
21
+ export const LIST_LIMIT = 500;
22
+ /** How many events one page of the feed carries. */
23
+ export const EVENTS_PAGE_LIMIT = 100;
24
+ /** What an agent calls itself when it takes a task: its kind, where it runs. One line. */
25
+ export const MAX_AGENT_LENGTH = 80;
26
+ /** What an agent says when it ends a run. Markdown. */
27
+ export const MAX_SUMMARY_LENGTH = 2000;
28
+ /** How many reports, and how many artifacts, one run may carry. */
29
+ export const MAX_REPORTS_PER_RUN = 500;
30
+ export const MAX_ARTIFACTS_PER_RUN = 50;
31
+ /** A file a run hands in: how many bytes, and its name, one line without a path. */
32
+ export const MAX_FILE_BYTES = 10 * 1024 * 1024;
33
+ export const MAX_FILE_NAME_LENGTH = 200;
34
+ /** What a person calls a token: the agent it is for, where it runs. One line. */
35
+ export const MAX_TOKEN_NAME_LENGTH = 80;
36
+ /** How long a token may be good for, in days, and how long one is when nothing is said. */
37
+ export const MAX_TOKEN_DAYS = 365;
38
+ export const DEFAULT_TOKEN_DAYS = 90;
39
+ /** How many tokens one person may hold at a time. */
40
+ export const MAX_TOKENS_PER_PERSON = 25;
41
+ /** How long a secret a client presents in a body may be: a token or a connection's secret is far shorter. */
42
+ export const MAX_SECRET_LENGTH = 200;
43
+ /** How long a language tag may be: the longest a browser sends is far shorter. */
44
+ export const MAX_LANGUAGE_TAG_LENGTH = 35;
45
+ /** A project's key, what paths and the command say: lowercase letters, digits and hyphens. */
46
+ export const MAX_PROJECT_KEY_LENGTH = 40;
47
+ /** A project's name, one line, and its description, a paragraph. */
48
+ export const MAX_PROJECT_NAME_LENGTH = 100;
49
+ export const MAX_PROJECT_DESCRIPTION_LENGTH = 500;
50
+ /** The address of a project's skills, as the standard spells one; the console checks it as one. */
51
+ export const MAX_SKILLS_ADDRESS_LENGTH = 2048;
52
+ /** A document's path: segments of lowercase letters, digits and hyphens, separated by slashes. */
53
+ export const MAX_DOCUMENT_PATH_LENGTH = 200;
54
+ export const MAX_DOCUMENT_SEGMENT_LENGTH = 64;
55
+ /** A document's body: a page of Markdown. */
56
+ export const MAX_DOCUMENT_LENGTH = 64_000;
57
+ /** How many versions one document keeps, and how many files attach to one. */
58
+ export const MAX_VERSIONS_PER_DOCUMENT = 500;
59
+ export const MAX_FILES_PER_DOCUMENT = 50;
60
+ /** The words a search of the documents takes. */
61
+ export const MAX_SEARCH_LENGTH = 200;
package/src/router.ts ADDED
@@ -0,0 +1,295 @@
1
+ import { isFolderPath, isSignInFailure, SIGN_IN_PARAM, type SignInFailure } from "./api.js";
2
+
3
+ // The addresses of the default console (ADR-0010): the router is a function from a URL to a
4
+ // route, and the hrefs below are its inverse. A project's pages live under `/projects/<key>`,
5
+ // the word the REST API uses; an address names what it shows (a task by its number, a decision
6
+ // by its id, a document by its path), every form has one, and the query says how a page is
7
+ // shown (`?edit`, `?ask`, `?new`, `?version=`, `?q=`). The old addresses are taken to the new.
8
+
9
+ /** How a task's page is shown: as it is, or with one of its forms open. */
10
+ export type TaskFormKind = "edit" | "ask";
11
+
12
+ /** How the documents are shown at a path: a folder or a page, a form, a version, or a search. */
13
+ export type DocsView =
14
+ | { readonly kind: "read"; readonly version?: number | undefined }
15
+ | { readonly kind: "new" }
16
+ | { readonly kind: "edit" }
17
+ | { readonly kind: "search"; readonly q: string };
18
+
19
+ export type Route =
20
+ | { readonly name: "projects" }
21
+ | { readonly name: "new-project" }
22
+ | { readonly name: "board"; readonly project: string }
23
+ | { readonly name: "new-task"; readonly project: string }
24
+ | {
25
+ readonly name: "task";
26
+ readonly project: string;
27
+ /** The task's number, or nothing when the address names it by id. */
28
+ readonly number: number | undefined;
29
+ readonly id: string | undefined;
30
+ readonly form: TaskFormKind | undefined;
31
+ }
32
+ | {
33
+ readonly name: "docs";
34
+ readonly project: string;
35
+ readonly path: string;
36
+ readonly view: DocsView;
37
+ }
38
+ | { readonly name: "decisions"; readonly project: string }
39
+ | { readonly name: "new-decision"; readonly project: string }
40
+ | { readonly name: "decision"; readonly project: string; readonly id: string }
41
+ | { readonly name: "feed"; readonly project: string }
42
+ | { readonly name: "skills"; readonly project: string }
43
+ | { readonly name: "members"; readonly project: string }
44
+ | { readonly name: "settings"; readonly project: string }
45
+ | { readonly name: "people" }
46
+ /** The agents of one person, for an administrator. */
47
+ | { readonly name: "person-agents"; readonly id: string }
48
+ | { readonly name: "agents" }
49
+ | { readonly name: "new-token" }
50
+ /** Where a person approves an agent's connection, by the code it showed, or types one. */
51
+ | { readonly name: "connect"; readonly code: string | undefined }
52
+ /** An address that moved: the page takes the person to `to` in its place. */
53
+ | { readonly name: "moved"; readonly to: string }
54
+ | { readonly name: "not-found" };
55
+
56
+ /** The pages of a project, after its key. */
57
+ export type ProjectPage = "docs" | "decisions" | "feed" | "skills" | "members" | "settings";
58
+
59
+ export const PATHS = {
60
+ /** The projects the person may see: the front page. */
61
+ projects: "/",
62
+ /** Where a project's pages are: `/projects/<key>`, then the page. */
63
+ project: "/projects",
64
+ newProject: "/projects/new",
65
+ people: "/people",
66
+ /** The agents of whoever is signed in, which hold their tokens: their own page. */
67
+ agents: "/agents",
68
+ /** The form for a token made by hand, for a script or a console of one's own. */
69
+ newToken: "/agents/new",
70
+ /** Where an agent's connection is approved (ADR-0011): `/connect/<code>`, or the code typed. */
71
+ connect: "/connect",
72
+ } as const;
73
+
74
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
75
+ const PROJECT_KEY = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
76
+ const NUMBER = /^[1-9][0-9]{0,8}$/;
77
+ /** A code as it may be typed into an address: checked for what it is by the page. */
78
+ const CODE_TEXT = /^[A-Za-z0-9-]{1,16}$/;
79
+ /** The pages of a project that take nothing after their name. */
80
+ const PLAIN_PAGES: readonly Exclude<ProjectPage, "docs">[] = [
81
+ "decisions",
82
+ "feed",
83
+ "skills",
84
+ "members",
85
+ "settings",
86
+ ];
87
+ const NOT_FOUND: Route = { name: "not-found" };
88
+
89
+ /** Where an address of the 0.1 line before this one leads now, or nothing for a current one. */
90
+ export function movedFrom(pathname: string): string | undefined {
91
+ if (pathname === "/tokens") {
92
+ return PATHS.agents;
93
+ }
94
+ if (pathname === "/p" || pathname.startsWith("/p/")) {
95
+ return `${PATHS.project}${pathname.slice(2)}`;
96
+ }
97
+ if (pathname === PATHS.project) {
98
+ return PATHS.projects;
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ const taskFormOf = (params: URLSearchParams): TaskFormKind | undefined =>
104
+ params.has("edit") ? "edit" : params.has("ask") ? "ask" : undefined;
105
+
106
+ /** The view the query asks for at a path, or nothing for one the path cannot be shown as. */
107
+ const docsViewOf = (params: URLSearchParams, path: string): DocsView | undefined => {
108
+ const q = params.get("q");
109
+ if (q !== null && q.trim().length > 0) {
110
+ return { kind: "search", q: q.trim() };
111
+ }
112
+ if (params.has("new")) {
113
+ return { kind: "new" };
114
+ }
115
+ if (params.has("edit")) {
116
+ return path === "" ? undefined : { kind: "edit" };
117
+ }
118
+ const version = params.get("version");
119
+ if (version !== null) {
120
+ return path !== "" && NUMBER.test(version)
121
+ ? { kind: "read", version: Number(version) }
122
+ : undefined;
123
+ }
124
+ return { kind: "read" };
125
+ };
126
+
127
+ export function matchRoute(pathname: string, search = ""): Route {
128
+ const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
129
+ const moved = movedFrom(path);
130
+ if (moved !== undefined) {
131
+ return { name: "moved", to: `${moved}${search}` };
132
+ }
133
+ const params = new URLSearchParams(search);
134
+ if (path === PATHS.projects) {
135
+ return { name: "projects" };
136
+ }
137
+ if (path === PATHS.newProject) {
138
+ return { name: "new-project" };
139
+ }
140
+ if (path === PATHS.people) {
141
+ return { name: "people" };
142
+ }
143
+ if (path === PATHS.agents) {
144
+ return { name: "agents" };
145
+ }
146
+ if (path === PATHS.newToken) {
147
+ return { name: "new-token" };
148
+ }
149
+ if (path === PATHS.connect) {
150
+ return { name: "connect", code: undefined };
151
+ }
152
+ if (path.startsWith(`${PATHS.connect}/`)) {
153
+ const code = path.slice(PATHS.connect.length + 1);
154
+ return CODE_TEXT.test(code) ? { name: "connect", code } : NOT_FOUND;
155
+ }
156
+ if (path.startsWith(`${PATHS.people}/`)) {
157
+ const [id, page, ...more] = path.slice(PATHS.people.length + 1).split("/");
158
+ return id !== undefined && UUID.test(id) && page === "agents" && more.length === 0
159
+ ? { name: "person-agents", id }
160
+ : NOT_FOUND;
161
+ }
162
+ if (!path.startsWith(`${PATHS.project}/`)) {
163
+ return NOT_FOUND;
164
+ }
165
+ const [project, page, third, ...rest] = path.slice(PATHS.project.length + 1).split("/");
166
+ if (project === undefined || !PROJECT_KEY.test(project)) {
167
+ return NOT_FOUND;
168
+ }
169
+ if (page === undefined) {
170
+ return { name: "board", project };
171
+ }
172
+ if (page === "docs") {
173
+ // The rest of the path is the document's, or a folder's, or nothing for the root.
174
+ const docPath = [third, ...rest].filter((segment) => segment !== undefined).join("/");
175
+ if (!isFolderPath(docPath)) {
176
+ return NOT_FOUND;
177
+ }
178
+ const view = docsViewOf(params, docPath);
179
+ return view === undefined ? NOT_FOUND : { name: "docs", project, path: docPath, view };
180
+ }
181
+ if (rest.length > 0) {
182
+ return NOT_FOUND;
183
+ }
184
+ if (page === "tasks") {
185
+ if (third === "new") {
186
+ return { name: "new-task", project };
187
+ }
188
+ if (third !== undefined && NUMBER.test(third)) {
189
+ return {
190
+ name: "task",
191
+ project,
192
+ number: Number(third),
193
+ id: undefined,
194
+ form: taskFormOf(params),
195
+ };
196
+ }
197
+ if (third !== undefined && UUID.test(third)) {
198
+ return { name: "task", project, number: undefined, id: third, form: taskFormOf(params) };
199
+ }
200
+ return NOT_FOUND;
201
+ }
202
+ if (page === "decisions" && third !== undefined) {
203
+ if (third === "new") {
204
+ return { name: "new-decision", project };
205
+ }
206
+ return UUID.test(third) ? { name: "decision", project, id: third } : NOT_FOUND;
207
+ }
208
+ if (third !== undefined) {
209
+ return NOT_FOUND;
210
+ }
211
+ const found = PLAIN_PAGES.find((candidate) => candidate === page);
212
+ return found === undefined ? NOT_FOUND : { name: found, project };
213
+ }
214
+
215
+ /** Where a person approves an agent's connection by its code, or types one. */
216
+ export function connectHref(code?: string): string {
217
+ return code === undefined ? PATHS.connect : `${PATHS.connect}/${encodeURIComponent(code)}`;
218
+ }
219
+
220
+ /** Where an administrator sees one person's agents. */
221
+ export function personAgentsHref(personId: string): string {
222
+ return `${PATHS.people}/${personId}/agents`;
223
+ }
224
+
225
+ /** Where a project's board is, or one of its pages. */
226
+ export function projectHref(key: string, page?: ProjectPage): string {
227
+ const base = `${PATHS.project}/${key}`;
228
+ return page === undefined ? base : `${base}/${page}`;
229
+ }
230
+
231
+ /** Where a task is written: the form. */
232
+ export function newTaskHref(key: string): string {
233
+ return `${PATHS.project}/${key}/tasks/new`;
234
+ }
235
+
236
+ /** Where a task is read by its number, or one of its forms is open. */
237
+ export function taskHref(key: string, number: number, form?: TaskFormKind): string {
238
+ const base = `${PATHS.project}/${key}/tasks/${number}`;
239
+ return form === undefined ? base : `${base}?${form}`;
240
+ }
241
+
242
+ /** Where a decision is raised from the Decisions page: the form. */
243
+ export function newDecisionHref(key: string): string {
244
+ return `${PATHS.project}/${key}/decisions/new`;
245
+ }
246
+
247
+ /** Where one decision is read, and answered. */
248
+ export function decisionHref(key: string, id: string): string {
249
+ return `${PATHS.project}/${key}/decisions/${id}`;
250
+ }
251
+
252
+ /**
253
+ * Where a document of a project is read, or a folder of them listed (`""` for the root), and
254
+ * how: a form, a version, or a search over all of them.
255
+ */
256
+ export function docHref(key: string, path: string, view?: DocsView): string {
257
+ const base = path === "" ? projectHref(key, "docs") : `${PATHS.project}/${key}/docs/${path}`;
258
+ if (view === undefined) {
259
+ return base;
260
+ }
261
+ switch (view.kind) {
262
+ case "search":
263
+ return `${base}?q=${encodeURIComponent(view.q)}`;
264
+ case "new":
265
+ return `${base}?new`;
266
+ case "edit":
267
+ return `${base}?edit`;
268
+ case "read":
269
+ return view.version === undefined ? base : `${base}?version=${view.version}`;
270
+ }
271
+ }
272
+
273
+ /** The ways a page moves the person: to an address, in place of the one shown, or back. */
274
+ export interface Navigation {
275
+ /** Shows the address, as one more step in the history. */
276
+ go(href: string): void;
277
+ /** Shows the address in place of the one shown: for a redirect, or for what a form made. */
278
+ replace(href: string): void;
279
+ /** Goes back to where the page was reached from, or to `fallback` when there is no such place. */
280
+ back(fallback: string): void;
281
+ }
282
+
283
+ /** Why the last sign-in did not complete, as the page's URL says it, or nothing. */
284
+ export function signInFailureOf(search: string): SignInFailure | undefined {
285
+ const value = new URLSearchParams(search).get(SIGN_IN_PARAM);
286
+ return isSignInFailure(value) ? value : undefined;
287
+ }
288
+
289
+ /** The URL without the sign-in parameter, once the page has read it. */
290
+ export function withoutSignInParam(pathname: string, search: string): string {
291
+ const params = new URLSearchParams(search);
292
+ params.delete(SIGN_IN_PARAM);
293
+ const query = params.toString();
294
+ return query.length === 0 ? pathname : `${pathname}?${query}`;
295
+ }
package/src/routes.ts ADDED
@@ -0,0 +1,106 @@
1
+ // Where the console answers (docs/architecture.md). The REST API is for the pages and for a
2
+ // custom console; signing in and out are navigations and one form post, not part of it. The
3
+ // board is nested under the project it belongs to (ADR-0008).
4
+ import type { ProviderKey } from "./vocabulary.js";
5
+
6
+ /** One prefix per resource of the workspace; an id follows where one is needed. */
7
+ export const REST_ROUTES = {
8
+ /** Who is signed in, what the workspace is called, and whether anyone can sign in. */
9
+ me: "/api/v1/me",
10
+ /** The people of the workspace: everyone who has signed in. */
11
+ people: "/api/v1/people",
12
+ /** The projects the asker may see; the board of each is under its key. */
13
+ projects: "/api/v1/projects",
14
+ /** The workspace's own events, the ones about no project: who joined, who was made what. */
15
+ events: "/api/v1/events",
16
+ /** The tokens of whoever asks: what their agents, scripts and consoles act as them with. */
17
+ tokens: "/api/v1/tokens",
18
+ /** Connecting an agent (ADR-0011): begun as nobody, approved by a person, claimed by the command. */
19
+ connect: "/api/v1/connect",
20
+ } as const;
21
+
22
+ /** Where the command claims the token of a connection, with its secret. */
23
+ export const CLAIM_PATH = `${REST_ROUTES.connect}/claim`;
24
+
25
+ /** The REST path of one connection, by its code, or of approving or denying it. */
26
+ export function connectPath(code: string, action?: "approve" | "deny"): string {
27
+ const base = `${REST_ROUTES.connect}/${encodeURIComponent(code)}`;
28
+ return action === undefined ? base : `${base}/${action}`;
29
+ }
30
+
31
+ /** The REST path of a person's tokens, or of one of them: for an administrator. */
32
+ export function personTokensPath(personId: string, tokenId?: string): string {
33
+ const base = `${restPath("people", personId)}/tokens`;
34
+ return tokenId === undefined ? base : `${base}/${encodeURIComponent(tokenId)}`;
35
+ }
36
+
37
+ /** What a project holds, each under `/api/v1/projects/<key>/<collection>`. */
38
+ export const PROJECT_COLLECTIONS = [
39
+ "tasks",
40
+ "decisions",
41
+ "runs",
42
+ "files",
43
+ "events",
44
+ "skills",
45
+ "members",
46
+ "docs",
47
+ ] as const;
48
+
49
+ export type ProjectCollection = (typeof PROJECT_COLLECTIONS)[number];
50
+
51
+ /** Where a browser signs in and out. Signing in is per provider: `/auth/<provider>/...`. */
52
+ export const AUTH_ROUTES = {
53
+ /** Sends the browser to the provider to sign in, and afterwards to `return_to`. */
54
+ login: (provider: ProviderKey): string => `/auth/${provider}/login`,
55
+ /** Where the provider sends the browser back to. */
56
+ callback: (provider: ProviderKey): string => `/auth/${provider}/callback`,
57
+ /** A POST from the pages that ends the session. */
58
+ logout: "/auth/logout",
59
+ } as const;
60
+
61
+ /** The parameter of the login route naming the page to come back to: a path of this origin. */
62
+ export const RETURN_TO_PARAM = "return_to";
63
+
64
+ /**
65
+ * The parameter by which the server tells a page that a sign-in did not complete: the person
66
+ * said no at the provider, the attempt was not finished in time or in the browser that began
67
+ * it, the provider did not confirm it, or the account is not a member of the workspace.
68
+ */
69
+ export const SIGN_IN_PARAM = "sign_in";
70
+ export const SIGN_IN_FAILURES = ["denied", "expired", "failed", "refused"] as const;
71
+ export type SignInFailure = (typeof SIGN_IN_FAILURES)[number];
72
+
73
+ export function isSignInFailure(value: string | null | undefined): value is SignInFailure {
74
+ return (SIGN_IN_FAILURES as readonly string[]).includes(value ?? "");
75
+ }
76
+
77
+ /** Where a browser leaves for the provider to sign in and come back to `returnTo`. */
78
+ export function loginPath(provider: ProviderKey, returnTo: string): string {
79
+ return `${AUTH_ROUTES.login(provider)}?${RETURN_TO_PARAM}=${encodeURIComponent(returnTo)}`;
80
+ }
81
+
82
+ /** A page of this origin, told why the sign-in it was waiting for did not complete. */
83
+ export function signInPath(page: string, failure: SignInFailure): string {
84
+ return `${page}${page.includes("?") ? "&" : "?"}${SIGN_IN_PARAM}=${failure}`;
85
+ }
86
+
87
+ /** The REST path of one person, token or project: the collection, then the id or the key. */
88
+ export function restPath(collection: "people" | "tokens" | "projects", id: string): string {
89
+ return `${REST_ROUTES[collection]}/${encodeURIComponent(id)}`;
90
+ }
91
+
92
+ /**
93
+ * The REST path of a project, of one of its collections, or of one item in it: the tasks of
94
+ * `web` are at `projectPath("web", "tasks")`, task 7 at `projectPath("web", "tasks", "7")`.
95
+ * A document's path is one segment of the URL, its slashes encoded: `projectPath("web",
96
+ * "docs", "guides/onboarding")` is `/api/v1/projects/web/docs/guides%2Fonboarding`.
97
+ */
98
+ export function projectPath(key: string, collection?: ProjectCollection, id?: string): string {
99
+ const base = restPath("projects", key);
100
+ if (collection === undefined) {
101
+ return base;
102
+ }
103
+ return id === undefined
104
+ ? `${base}/${collection}`
105
+ : `${base}/${collection}/${encodeURIComponent(id)}`;
106
+ }