@skillcdn/console 0.4.0 → 0.4.1

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 (98) hide show
  1. package/README.md +1 -0
  2. package/dist/api.d.ts +1 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +1 -1
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +36 -11
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/help.d.ts.map +1 -1
  10. package/dist/cli/help.js +25 -5
  11. package/dist/cli/help.js.map +1 -1
  12. package/dist/client.d.ts +12 -1
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +4 -0
  15. package/dist/client.js.map +1 -1
  16. package/dist/compat.d.ts +1 -1
  17. package/dist/compat.js +2 -2
  18. package/dist/components/markdown.d.ts.map +1 -1
  19. package/dist/components/markdown.js +6 -3
  20. package/dist/components/markdown.js.map +1 -1
  21. package/dist/components/pack-connect.d.ts +3 -1
  22. package/dist/components/pack-connect.d.ts.map +1 -1
  23. package/dist/components/pack-connect.js +4 -4
  24. package/dist/components/pack-connect.js.map +1 -1
  25. package/dist/components/pack-token.d.ts +3 -1
  26. package/dist/components/pack-token.d.ts.map +1 -1
  27. package/dist/components/pack-token.js +1 -1
  28. package/dist/components/pack-token.js.map +1 -1
  29. package/dist/components/people.d.ts.map +1 -1
  30. package/dist/components/people.js +14 -3
  31. package/dist/components/people.js.map +1 -1
  32. package/dist/components/remote-pictures.d.ts +6 -0
  33. package/dist/components/remote-pictures.d.ts.map +1 -0
  34. package/dist/components/remote-pictures.js +8 -0
  35. package/dist/components/remote-pictures.js.map +1 -0
  36. package/dist/components/sign-in.d.ts.map +1 -1
  37. package/dist/components/sign-in.js +49 -2
  38. package/dist/components/sign-in.js.map +1 -1
  39. package/dist/components/ui.d.ts +3 -2
  40. package/dist/components/ui.d.ts.map +1 -1
  41. package/dist/components/ui.js +6 -3
  42. package/dist/components/ui.js.map +1 -1
  43. package/dist/console.css +57 -5
  44. package/dist/console.d.ts.map +1 -1
  45. package/dist/console.js +13 -6
  46. package/dist/console.js.map +1 -1
  47. package/dist/data.d.ts +4 -1
  48. package/dist/data.d.ts.map +1 -1
  49. package/dist/data.js +16 -19
  50. package/dist/data.js.map +1 -1
  51. package/dist/i18n/en.d.ts +14 -1
  52. package/dist/i18n/en.d.ts.map +1 -1
  53. package/dist/i18n/en.js +14 -1
  54. package/dist/i18n/en.js.map +1 -1
  55. package/dist/i18n/ko.d.ts.map +1 -1
  56. package/dist/i18n/ko.js +10 -1
  57. package/dist/i18n/ko.js.map +1 -1
  58. package/dist/limits.d.ts +14 -0
  59. package/dist/limits.d.ts.map +1 -1
  60. package/dist/limits.js +14 -0
  61. package/dist/limits.js.map +1 -1
  62. package/dist/routes.d.ts +2 -0
  63. package/dist/routes.d.ts.map +1 -1
  64. package/dist/routes.js +2 -0
  65. package/dist/routes.js.map +1 -1
  66. package/dist/schemas.d.ts +30 -1
  67. package/dist/schemas.d.ts.map +1 -1
  68. package/dist/schemas.js +15 -4
  69. package/dist/schemas.js.map +1 -1
  70. package/dist/web.d.ts.map +1 -1
  71. package/dist/web.js +3 -2
  72. package/dist/web.js.map +1 -1
  73. package/docs/specs/cli.md +5 -5
  74. package/docs/specs/rest.md +22 -11
  75. package/package.json +2 -2
  76. package/skills/building-a-console/SKILL.md +2 -2
  77. package/skills/working-the-board/references/setting-up.md +1 -1
  78. package/src/api.ts +8 -0
  79. package/src/cli/cli.ts +39 -12
  80. package/src/cli/help.ts +30 -4
  81. package/src/client.ts +30 -1
  82. package/src/compat.ts +2 -2
  83. package/src/components/markdown.tsx +6 -3
  84. package/src/components/pack-connect.tsx +10 -7
  85. package/src/components/pack-token.tsx +4 -1
  86. package/src/components/people.tsx +17 -7
  87. package/src/components/remote-pictures.ts +19 -0
  88. package/src/components/sign-in.tsx +114 -13
  89. package/src/components/ui.tsx +6 -3
  90. package/src/console.tsx +24 -5
  91. package/src/data.ts +20 -20
  92. package/src/i18n/en.ts +15 -1
  93. package/src/i18n/ko.ts +11 -1
  94. package/src/limits.ts +14 -0
  95. package/src/routes.ts +2 -0
  96. package/src/schemas.ts +17 -4
  97. package/src/styles/console.css +57 -5
  98. package/src/web.ts +3 -2
@@ -47,6 +47,8 @@ The API is the board as its members see it: the people, the projects, and in eac
47
47
  | 403 | `auth.forbidden_origin` | A request that changes something on a session did not come from the console's own pages. |
48
48
  | 403 | `auth.forbidden` | A member asked for what only an administrator, or only an owner of the project, may do. |
49
49
  | 403 | `auth.session_required` | A token asked for what only a person signed in may do: the tokens themselves, and the approval of an agent's connection, which makes one. |
50
+ | 403 | `auth.deactivated` | A token the console knows, for a person deactivated: the token is kept, refused on every route while they are out, `GET /api/v1/me` included, and theirs again when they are taken back in ([ADR-0044](../adr/0044-a-token-of-a-person-who-is-out-is-kept-and-refused-with-the-reason-until-they-are-back-in.md)). |
51
+ | 403 | `auth.not_admitted` | A token the console knows, for a person no longer let in: what let them in, the domain or the organization, is no longer named. Kept and refused the same way, and theirs again when they are back in. |
50
52
  | 403 | `run.not_yours` | The run is another person's. |
51
53
  | 404 | `project.not_found` | No project has that key, or it is not the asker's to see: the two are one answer. The key is checked before anything is asked. |
52
54
  | 404 | `task.not_found`, `decision.not_found`, `goal.not_found`, `token.not_found`, `person.not_found`, `run.not_found`, `run.task_not_found`, `member.not_found` | No such id in the project (or in the workspace, for a token, a person); for a token, none of the asker's. An id that is not a UUID is not found either, except a task's number. |
@@ -60,6 +62,11 @@ The API is the board as its members see it: the people, the projects, and in eac
60
62
  | 409 | `member.exists` | The person is listed in the project already. |
61
63
  | 409 | `member.open_project` | The project is open to the workspace, so everyone of it is a member already; only an owner is listed there ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)). |
62
64
  | 409 | `category.key_taken`, `category.in_use`, `category.too_many` | A category of the workspace has that key already; tasks or goals are sorted into the category, so it stays until they are moved; the workspace has as many categories as one may (`MAX_CATEGORIES`). |
65
+ | 409 | `project.too_many`, `task.too_many`, `goal.too_many`, `decision.too_many`, `document.too_many`, `run.too_many` | The workspace holds as many projects as one may (`MAX_PROJECTS_PER_WORKSPACE`); the project as many tasks, goals, decisions or pages (`MAX_TASKS_PER_PROJECT`, `MAX_GOALS_PER_PROJECT`, `MAX_DECISIONS_PER_PROJECT`, `MAX_DOCUMENTS_PER_PROJECT`), those put away counted; the task has been taken as many times as one may (`MAX_RUNS_PER_TASK`). Bounds a runaway script meets and real work does not. |
66
+ | 409 | `project.too_many_bytes` | The files of the project, handed in by runs, put in comments and attached to pages, hold as many bytes together as the deployment allows one project (`PROJECT_FILE_BYTES_AT_MOST`, [deploy/README.md](../../deploy/README.md#a-shared-console)); no bound unless set. Asked before any byte is kept. |
67
+ | 429 | `feed.too_many` | The person holds as many streams of the feed as one may at once (`MAX_STREAMS_PER_PERSON`), or the process does for everyone (`MAX_STREAMS`); `retry-after` says when to try again. |
68
+ | 403 | `picture.refused` | The console fetches no picture from that address: one of its own network, the loopback, a private or a link-local range, or a redirect to one ([ADR-0048](../adr/0048-a-picture-from-elsewhere-is-read-through-the-console-from-a-public-address-and-kept-a-while.md)). |
69
+ | 502 | `picture.unavailable` | The picture could not be fetched from where it is named: the origin answered an error or something that is not a picture, sent more than `MAX_FILE_BYTES`, or could not be reached in time. |
63
70
  | 409 | `decision.answered` | The decision has an answer already. |
64
71
  | 409 | `document.conflict` | The page has moved on since the version the write started from (`baseVersion`), or was written at that path by someone else first. |
65
72
  | 409 | `document.archived` | The page is archived: restore it before writing to it or attaching to it. |
@@ -85,7 +92,7 @@ The API is the board as its members see it: the people, the projects, and in eac
85
92
 
86
93
  ### `GET /api/v1/me`
87
94
 
88
- `{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label" }, ...], "console": { "version", "clientRange" } }`: what the board is called, the most days a token may be good for here (or `null` where a person chooses), where the organization's skills come from (`source`, the origin of the SkillCDN deployment they are read through, and `address`, the organization's, canonical, which a project without one of its own reads, or `null`; and `packToken`, when the SkillCDN token the organization's pack is read with was set, `{ setAt }`, or `null`), who sees whom on the People page (`peopleVisibility`: `everyone`, or `shared_projects`, where a member sees only the people they share a project with and an administrator everyone; [ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md); read as optional, for a console from before the setting), the day the calendars begin a week on and this week is counted by (`weekStart`: `auto`, as the language of the pages has it, or `monday`, `sunday`; [ADR-0039](../adr/0039-this-week-is-the-plans-unit-and-the-week-begins-as-the-workspace-says.md)) and the workspace's time zone (`timeZone`: an IANA name, in which the command prints moments, or `null` for none; both optional the same way), who the session cookie or the token says is asking (`id`, `login`, `name` or `null`, `avatar` or `null`, `role`), with what (`agent`: the name of the token when a token asks, as its person called it, so that a console of a person's own knows it holds one and offers nothing a token cannot do; `null` on a session), the language the person chose for the pages (`language`, a tag, or `null` for the browser's), and the identity providers people sign in through (`gh`, `google`, each with the label of its button), none where nobody can; and the console's own version with the range of command versions it takes (`console`, [ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)), for a console built on the package to compare itself with. Nobody is an answer, not an error; the pages ask this once to decide what to show, and the command once, at `login`, to see that the console knows the token.
95
+ `{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label", "admits": { "organization", "domains" } }, ...], "console": { "version", "clientRange" } }`: what the board is called, the most days a token may be good for here (or `null` where a person chooses), where the organization's skills come from (`source`, the origin of the SkillCDN deployment they are read through, and `address`, the organization's, canonical, which a project without one of its own reads, or `null`; and `packToken`, when the SkillCDN token the organization's pack is read with was set, `{ setAt }`, or `null`), who sees whom on the People page (`peopleVisibility`: `everyone`, or `shared_projects`, where a member sees only the people they share a project with and an administrator everyone; [ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md); read as optional, for a console from before the setting), the day the calendars begin a week on and this week is counted by (`weekStart`: `auto`, as the language of the pages has it, or `monday`, `sunday`; [ADR-0039](../adr/0039-this-week-is-the-plans-unit-and-the-week-begins-as-the-workspace-says.md)) and the workspace's time zone (`timeZone`: an IANA name, in which the command prints moments, or `null` for none; both optional the same way), who the session cookie or the token says is asking (`id`, `login`, `name` or `null`, `avatar` or `null`, `role`), with what (`agent`: the name of the token when a token asks, as its person called it, so that a console of a person's own knows it holds one and offers nothing a token cannot do; `null` on a session), the language the person chose for the pages (`language`, a tag, or `null` for the browser's), and the identity providers people sign in through (`gh`, `google`, each with the label of its button and, in `admits`, who gets in through it: the organization at the git host whose members are in, or `null`, and the Workspace domains whose accounts are, which a sign-in page says under the button; answered from 0.4.1 and read as optional), none where nobody can; and the console's own version with the range of command versions it takes (`console`, [ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)), for a console built on the package to compare itself with. Nobody is an answer, not an error; the pages ask this once to decide what to show, and the command once, at `login`, to see that the console knows the token. A token the console knows whose person is out is refused here as on every route, `auth.deactivated` or `auth.not_admitted`, so that the agent learns why ([ADR-0044](../adr/0044-a-token-of-a-person-who-is-out-is-kept-and-refused-with-the-reason-until-they-are-back-in.md)).
89
96
 
90
97
  ### `GET /api/v1/people?login=`
91
98
 
@@ -105,7 +112,7 @@ Who may sign in, by login ([ADR-0022](../adr/0022-who-is-in-is-the-consoles-own-
105
112
 
106
113
  ### `POST /api/v1/projects`
107
114
 
108
- Takes `key` and `name`, and optionally `description`, `visibility` (default `private`), `skillsAddress` (checked as the standard spells one, kept canonical; `project.invalid_address`), `color` and `symbol`. For whoever is signed in, or their agent with their token; they become the project's owner. Answers `201` with the project and writes a `project.created` event in it, with `key` and `name`. A key taken is `project.key_taken`.
115
+ Takes `key` and `name`, and optionally `description`, `visibility` (default `private`), `skillsAddress` (checked as the standard spells one, kept canonical; `project.invalid_address`), `color` and `symbol`. For an administrator, signed in or through their agent with their token ([ADR-0045](../adr/0045-a-project-is-made-by-an-administrator.md)); they become the project's owner, and list another as owner from there. A member is refused with `auth.forbidden`. Answers `201` with the project and writes a `project.created` event in it, with `key` and `name`. A key taken is `project.key_taken`.
109
116
 
110
117
  ### `GET /api/v1/events?after=&limit=`
111
118
 
@@ -123,9 +130,9 @@ The workspace's settings, which an administrator keeps on the Settings page ([AD
123
130
 
124
131
  The SkillCDN token the organization's pack is read with ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an administrator, signed in from the console's own pages or through their agent's token: every project that names no address of its own and holds no token of its own reads the organization's pack (`SKILLS_ADDRESS`) with it. `PUT` takes `{ "token" }` as the project's route does, tries it against the organization's pack once (`workspace.pack_token_refused` when the pack is not served to it or the console names no address for the organization's skills; `skills.unavailable` when the deployment could not be reached), seals it with the console's secret (`workspace.pack_token_unsupported` where the console has none) and keeps it on the workspace, answering the workspace as `me` carries it, with `skills.packToken: { setAt }`, and writing a `workspace.updated` event naming the field `packToken`. The token is answered nowhere and logged nowhere. `DELETE` takes it away the same way; taking away what is not there writes nothing.
125
132
 
126
- ### `POST /api/v1/workspace/pack-token/connect`, `GET .../pack-token/connect` and `DELETE .../pack-token/connect`
133
+ ### `POST /api/v1/workspace/pack-token/connect`, `GET .../pack-token/connect`, `POST .../pack-token/connect/claim` and `DELETE .../pack-token/connect`
127
134
 
128
- The workspace's token connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an administrator, as a project's is: `POST` asks the deployment to connect the organization's address and answers `201` with `{ "code", "url", "expiresAt", "intervalSeconds" }`, with `workspace.pack_connect_unsupported`, `workspace.pack_connect_refused` and `skills.unavailable` where a project's route has its own, the refusal naming a console with no address for the organization's skills; `GET` answers `{ "status": "none" }`, `pending` with what the person is shown, `connected` with `workspace` as `me` carries it once the token is kept, or `ended` with `reason`; `DELETE` gives the request up. One request for the workspace at a time.
135
+ The workspace's token connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an administrator, as a project's is: `POST .../connect` asks the deployment to connect the organization's address and answers `201` with `{ "code", "url", "expiresAt", "intervalSeconds" }`, with `workspace.pack_connect_unsupported`, `workspace.pack_connect_refused` and `skills.unavailable` where a project's route has its own, the refusal naming a console with no address for the organization's skills; `GET` reads how it stands and changes nothing: `{ "status": "none" }`, `pending` with what the person is shown, or `ended` with `reason` `expired`; `POST .../connect/claim` asks the deployment whether the person approved ([ADR-0046](../adr/0046-the-claim-of-a-pack-token-is-a-request-of-its-own-and-reading-how-the-connection-stands-changes-nothing.md)) and answers `pending` still, `connected` with `workspace` as `me` carries it once the token is kept, or `ended` with `reason`; `DELETE` gives the request up. One request for the workspace at a time.
129
136
 
130
137
  ### `GET /api/v1/categories`
131
138
 
@@ -157,7 +164,7 @@ Takes one of the asker's tokens away, whoever holds it, and answers `204`. A tok
157
164
 
158
165
  ### `DELETE /api/v1/me/token`
159
166
 
160
- The token the request is made with gives itself up, so that it is good for nothing from now on, and the answer is `204`: what `console logout` does, so that a token forgotten on a machine does not outlive it. The one thing a token does about the tokens; with a session, which has none to give, it is `request.invalid`.
167
+ The token the request is made with gives itself up, so that it is good for nothing from now on, and the answer is `204`: what `console logout` does, so that a token forgotten on a machine does not outlive it. The one thing a token does about the tokens; with a session, which has none to give, it is `request.invalid`. A token whose person is out is refused here as everywhere ([ADR-0044](../adr/0044-a-token-of-a-person-who-is-out-is-kept-and-refused-with-the-reason-until-they-are-back-in.md)): the console keeps it for when they are back in, and the command forgets it on the machine.
161
168
 
162
169
  ### `GET /api/v1/people/<id>/tokens` and `DELETE /api/v1/people/<id>/tokens/<token id>`
163
170
 
@@ -167,11 +174,15 @@ An administrator's sight of everyone's agents ([ADR-0011](../adr/0011-an-agent-c
167
174
 
168
175
  `{ "tasks": [{ "project", "task" }, ...], "closed": [{ "project", "task" }, ...] }`: what is on another person's plate, for whoever oversees them: an administrator, or an owner of a project the person is in (everyone of the workspace is in a project open to it; a private one holds those listed), signed in or through their agent. The open tasks the person is at work on, the sooner due first and those with no day last, and as `closed` those of them done or dropped and not yet put away, both within the projects the asker may see and no other, each with its project; the decisions are left out, since they wait for anyone. `auth.forbidden` for anyone else; `person.not_found` for an id that is nobody's.
169
176
 
177
+ ### `GET /api/v1/pictures?src=`
178
+
179
+ A picture from elsewhere, for a page that shows one ([ADR-0048](../adr/0048-a-picture-from-elsewhere-is-read-through-the-console-from-a-public-address-and-kept-a-while.md)): the console fetches it and hands it over, so that the person's browser reaches nobody's server. `src` is the https URL that names it, up to `MAX_URL_LENGTH`, on its default port and with no credentials in it; anything else is `request.invalid`. For whoever is signed in, with a session or a token. Fetched from a public address only, the host resolved first: an address of the console's own network, the loopback, a private or a link-local range, is `picture.refused` (`403`), and so is a redirect to one. What comes back must say it is a picture (`image/*`) and be at most `MAX_FILE_BYTES`; the origin's error, its silence or anything else is `picture.unavailable` (`502`). Answered with the picture's bytes and type, an `etag` of the bytes, `cache-control: private, max-age=86400`, so that a browser keeps it a day and asks with `if-none-match` after (`304`), and the headers a file is served with, under a policy that runs nothing. The console keeps what it fetched in memory, within a bounded room, for an hour before it asks the origin again with what the origin said the picture was; keeps serving it for a day when the origin cannot be reached; and keeps a refusal five minutes, so that a dead link costs one try in a while. The pages read every picture from elsewhere through here, a person's picture from the git host included, and load nothing from elsewhere themselves.
180
+
170
181
  ### Connecting an agent
171
182
 
172
183
  `POST /api/v1/connect`, `GET /api/v1/connect/<code>`, `POST /api/v1/connect/<code>/approve`, `POST /api/v1/connect/<code>/deny` and `POST /api/v1/connect/claim`: how an agent gets its token without anyone copying one ([ADR-0011](../adr/0011-an-agent-connects-with-a-short-code-a-person-approves-and-the-token-is-never-shown.md)).
173
184
 
174
- `POST /api/v1/connect` is nobody's: the command sends `{ "agent" }` (what the agent calls itself and where it runs, a line up to `MAX_TOKEN_NAME_LENGTH`) and is answered `201` with `{ "code", "url", "secret", "expiresAt", "interval" }`: a code of eight characters in two groups (`ABCD-EFGH`, from an alphabet without look-alikes) for a person to approve, the console's own page for it, a secret (`cns_c_...`) the command claims with and shows nowhere, when the request expires (thirty minutes after it began), and how many seconds the command waits between claims. A deployment holds a bounded number of requests at once (`connect.too_many`).
185
+ `POST /api/v1/connect` is nobody's: the command sends `{ "agent" }` (what the agent calls itself and where it runs, a line up to `MAX_TOKEN_NAME_LENGTH`) and is answered `201` with `{ "code", "url", "secret", "expiresAt", "interval" }`: a code of eight characters in two groups (`ABCD-EFGH`, from an alphabet without look-alikes) for a person to approve, the console's own page for it, a secret (`cns_c_...`) the command claims with and shows nowhere, when the request expires (thirty minutes after it began), and how many seconds the command waits between claims. A deployment holds a bounded number of requests at once (`connect.too_many`), and a bounded number from one address, as the trusted proxies resolve it: at that bound the oldest request of the address not yet approved makes way for the new one, and its claim is `connect.not_found`; an address whose requests are all approved is `connect.too_many` ([ADR-0047](../adr/0047-a-connection-request-is-bounded-per-address-and-the-oldest-not-yet-approved-makes-way.md)).
175
186
 
176
187
  `GET /api/v1/connect/<code>`, for a person signed in, answers what asks: `{ "code", "agent", "createdAt", "expiresAt", "approved" }`. A code is read as a person types it: any case, with or without the hyphen. `POST .../approve`, from the console's own pages, takes what `POST /api/v1/tokens` takes (`name`, `expiresInDays`) and answers the request, approved; `POST .../deny` takes the request away and answers `204`.
177
188
 
@@ -259,7 +270,7 @@ How the work goes, and what was made. A report takes `body` (Markdown, `MAX_BODY
259
270
 
260
271
  ### `POST .../runs/<id>/files` and `GET /api/v1/projects/<key>/files/<id>`
261
272
 
262
- A file handed in, as the parts of a form (`multipart/form-data`): the part `file` carries the bytes, with the file's name and its media type; the part `label` is optional. The name is one line without a path (`MAX_FILE_NAME_LENGTH`), the file is not empty and at most `MAX_FILE_BYTES` (`request.too_large`), and a media type that is not one, or none, is `application/octet-stream`. The bytes are kept under their SHA-256, so the same file handed in twice is kept once; the artifact on the run is `kind: "file"` with `file` filled in and `url` null. Answers `201` with the run and tells the board (`run.handed_in`, with the label or the file's name). The same rules as for a link: the run's person only, while the run is open, and only so many. A picture among the files is shown on the task's page small, and large when pressed, and a report that names it as `![words](its name)` shows it where the words are.
273
+ A file handed in, as the parts of a form (`multipart/form-data`): the part `file` carries the bytes, with the file's name and its media type; the part `label` is optional. The name is one line without a path (`MAX_FILE_NAME_LENGTH`), the file is not empty and at most `MAX_FILE_BYTES` (`request.too_large`), and a media type that is not one, or none, is `application/octet-stream`. Where the deployment bounds what one project's files hold together (`PROJECT_FILE_BYTES_AT_MOST`), a file that would take the project over it, counted with every file handed in, put in a comment or attached to a page, is `project.too_many_bytes` (`409`) and nothing of it is kept; a comment's files and a page's file are held to the same bound. The bytes are kept under their SHA-256, so the same file handed in twice is kept once; the artifact on the run is `kind: "file"` with `file` filled in and `url` null. Answers `201` with the run and tells the board (`run.handed_in`, with the label or the file's name). The same rules as for a link: the run's person only, while the run is open, and only so many. A picture among the files is shown on the task's page small, and large when pressed, and a report that names it as `![words](its name)` shows it where the words are.
263
274
 
264
275
  The bytes of any file of the project, handed in by a run, put in a comment or attached to a page, are read back at `/api/v1/projects/<key>/files/<file id>`, the project's one address for files, by whoever may see the project, with a session or a token: `content-length` and `content-disposition` name the file, the few kinds a browser may show in place (pictures, plain text, Markdown, CSV, JSON) are answered with their type and `inline`, and every other kind as `application/octet-stream` and `attachment`, never sniffed (`x-content-type-options`) and under a policy that runs nothing (`sandbox`). An id that is a link's, or nothing, or another project's, is `file.not_found`.
265
276
 
@@ -279,9 +290,9 @@ One skill of the pack, a file of it, what a folder holds and what words find, re
279
290
 
280
291
  The pack token ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)), for an owner, signed in from the console's own pages or through their agent's token. `PUT` takes `{ "token" }`, a token a person made at the deployment for the pack's repository (8 to 1024 characters, no whitespace), tries it against the deployment once (`project.pack_token_refused` when the pack is not served to it or the project names no pack; `skills.unavailable` when the deployment could not be reached), seals it with the console's secret (`project.pack_token_unsupported` where the console has none) and keeps it, answering the project with `packToken: { setAt }` and writing a `project.updated` event naming the field `packToken`. The token is answered nowhere and logged nowhere. `DELETE` takes it away the same way; taking away what is not there writes nothing.
281
292
 
282
- ### `POST /api/v1/projects/<key>/pack-token/connect`, `GET .../pack-token/connect` and `DELETE .../pack-token/connect`
293
+ ### `POST /api/v1/projects/<key>/pack-token/connect`, `GET .../pack-token/connect`, `POST .../pack-token/connect/claim` and `DELETE .../pack-token/connect`
283
294
 
284
- A private pack connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an owner. `POST` asks the deployment to connect the project's address (the project's own, or the organization's), as nobody, the console calling itself by the workspace's name with Console after it, once (`Acme Console`, whether the name ends with it or not), and answers `201` with what the person is shown: `{ "code", "url", "expiresAt", "intervalSeconds" }`, the code, the page at the deployment to approve it on, until when, and how often to ask again; the secret the console claims with is kept sealed on a row of the project's and answered never. One request per project at a time: a new one replaces the one under way. `project.pack_connect_unsupported` (`409`) where the console reaches no deployment or keeps no secret to seal with; `project.pack_connect_refused` (`400`) when the project names no pack, the pack is on the console's machine, or the deployment refused to begin, in its words (an address with a ref or a path, a public pack); `skills.unavailable` (`503`) when the deployment could not be reached. `GET` asks how it stands and answers one of `{ "status": "none" }`, nothing asked; `{ "status": "pending", "code", "url", "expiresAt", "intervalSeconds" }`, while the person has not approved; `{ "status": "connected", "project" }`, once, when the token was claimed, sealed and kept as `PUT .../pack-token` keeps one, writing `project.updated` with `packToken`, the project answered as it stands; or `{ "status": "ended", "reason": "expired" | "declined" | "refused" }`, when the code's time ran out, the person declined or the code was spent, or the deployment refused (a repository the person may not open, as many tokens as one may hold), the request gone each time. A deployment that could not be reached is `skills.unavailable`, and the request stands. `DELETE` gives the request up and answers `204`.
295
+ A private pack connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an owner. `POST` asks the deployment to connect the project's address (the project's own, or the organization's), as nobody, the console calling itself by the workspace's name with Console after it, once (`Acme Console`, whether the name ends with it or not), and answers `201` with what the person is shown: `{ "code", "url", "expiresAt", "intervalSeconds" }`, the code, the page at the deployment to approve it on, until when, and how often to ask again; the secret the console claims with is kept sealed on a row of the project's and answered never. One request per project at a time: a new one replaces the one under way. `project.pack_connect_unsupported` (`409`) where the console reaches no deployment or keeps no secret to seal with; `project.pack_connect_refused` (`400`) when the project names no pack, the pack is on the console's machine, or the deployment refused to begin, in its words (an address with a ref or a path, a public pack); `skills.unavailable` (`503`) when the deployment could not be reached. `GET` reads how it stands and changes nothing ([ADR-0046](../adr/0046-the-claim-of-a-pack-token-is-a-request-of-its-own-and-reading-how-the-connection-stands-changes-nothing.md)): `{ "status": "none" }`, nothing asked; `{ "status": "pending", "code", "url", "expiresAt", "intervalSeconds" }`, while the request stands; or `{ "status": "ended", "reason": "expired" }`, once its time ran out. `POST .../pack-token/connect/claim`, from the console's own pages or with a token, asks the deployment whether the person approved, at the interval it named, and answers `pending` still, with the same; `{ "status": "connected", "project" }`, once, when the token was claimed, sealed and kept as `PUT .../pack-token` keeps one, writing `project.updated` with `packToken`, the project answered as it stands; or `{ "status": "ended", "reason": "expired" | "declined" | "refused" }`, when the code's time ran out, the person declined or the code was spent, or the deployment refused (a repository the person may not open, as many tokens as one may hold), the request gone each time. A deployment that could not be reached is `skills.unavailable`, and the request stands. `DELETE` gives the request up and answers `204`.
285
296
 
286
297
  ### `GET /api/v1/projects/<key>/rules`
287
298
 
@@ -317,11 +328,11 @@ A file attached to the page, sent and read back exactly as a run hands one in ([
317
328
 
318
329
  ### `GET /api/v1/projects/<key>/events?after=&limit=&task=&run=&decision=&document=&goal=`
319
330
 
320
- `{ "items": [event, ...], "more": boolean }`: what happened in the project after event number `after` (`0` for the beginning), oldest first, at most `limit` (default and maximum `EVENTS_PAGE_LIMIT`); `more` says whether there is more after the last item. `task=<id>`, `run=<id>`, `decision=<id>`, `document=<id>` or `goal=<id>` keeps the events about one of them: how a page shows everything that happened to a task, a run, a decision, a document or a goal. An event carries its `id` (the number), `kind` (one of `EVENT_KINDS`), the `actor` (a person, or, for what an agent did, the person it acts for), `agent` (what the person acted through: the run's agent for what a run did, else the name of the token presented; `null` when a person acted themselves), `projectId`, `taskId`, `decisionId`, `runId`, `documentId` and `goalId` (or `null`), `data` (what a feed shows without asking for the subject: `number`, `title`, `fields`, `from`, `to`, `question`, `option`, `login`, `role`, `key`, `name`, `agent`, `status`, `label`, `excerpt`, `path`, `version`, each only when the kind has it), and `createdAt`.
331
+ `{ "items": [event, ...], "more": boolean }`: what happened in the project after event number `after` (`0` for the beginning), oldest first, at most `limit` (default and maximum `EVENTS_PAGE_LIMIT`); `more` says whether there is more after the last item. With `latest=true`, the newest `limit` events instead, oldest first, and `more` says whether there are earlier ones: how a page shows the feed as it stands without reading it from the beginning; `after` is not looked at then. The workspace's own feed takes `latest` the same way. `task=<id>`, `run=<id>`, `decision=<id>`, `document=<id>` or `goal=<id>` keeps the events about one of them: how a page shows everything that happened to a task, a run, a decision, a document or a goal. An event carries its `id` (the number), `kind` (one of `EVENT_KINDS`), the `actor` (a person, or, for what an agent did, the person it acts for), `agent` (what the person acted through: the run's agent for what a run did, else the name of the token presented; `null` when a person acted themselves), `projectId`, `taskId`, `decisionId`, `runId`, `documentId` and `goalId` (or `null`), `data` (what a feed shows without asking for the subject: `number`, `title`, `fields`, `from`, `to`, `question`, `option`, `login`, `role`, `key`, `name`, `agent`, `status`, `label`, `excerpt`, `path`, `version`, each only when the kind has it), and `createdAt`.
321
332
 
322
333
  ### `GET /api/v1/projects/<key>/events/stream?after=`
323
334
 
324
- The same events as they happen, as server-sent events: one message per event, with `id` the event's number, `event` its kind and `data` the event as JSON; a comment (`: ping`) every 25 seconds of silence. A browser that reconnects sends `Last-Event-ID`, which wins over `after`, so that nothing is missed. The stream ends when the server shuts down; an `EventSource` reconnects on its own.
335
+ The same events as they happen, as server-sent events: one message per event, with `id` the event's number, `event` its kind and `data` the event as JSON; a comment (`: ping`) every 25 seconds of silence. A browser that reconnects sends `Last-Event-ID`, which wins over `after`, so that nothing is missed. The stream ends when the server shuts down, and when the person may no longer read the project, which is asked again before every delivery and every heartbeat: a member taken off a private project, a person deactivated or a token revoked stops reading within a heartbeat. An `EventSource` reconnects on its own, and is answered as any request is. A person holds at most `MAX_STREAMS_PER_PERSON` streams at once, across their tabs, and a process of the console `MAX_STREAMS` for everyone: one more is `feed.too_many` (`429`, `retry-after`), which a browser's `EventSource` does not retry on its own; the page shows the board as it stands and loads it again on the next visit.
325
336
 
326
337
  The feed is written in the same transaction as the change it records, so what a stream carries happened, and what happened is carried; every process of the deployment learns of a change through the database's own channel, so a board with several `api` replicas is one board.
327
338
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillcdn/console",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "What a custom SkillCDN console is built from: the API client and schemas, the components, the composition of the default console, and the console command agents work the board with.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/skillcdn/console#readme",
@@ -68,7 +68,7 @@
68
68
  "vitest": "^5.0.3"
69
69
  },
70
70
  "dependencies": {
71
- "@skillcdn/core": "^0.4.0",
71
+ "@skillcdn/core": "^0.4.1",
72
72
  "react-markdown": "^10.1.0",
73
73
  "remark-gfm": "^4.0.1",
74
74
  "zod": "^4.6.5"
@@ -90,9 +90,9 @@ Produces the **checked build** and the delivery. `npx tsc --noEmit` and `npx vit
90
90
 
91
91
  ## Hard rules
92
92
 
93
- - What people and agents wrote is untrusted: shown with the package's `Markdown` (elements, never HTML), never with `dangerouslySetInnerHTML`, never as a script or a style; pictures over https only.
93
+ - What people and agents wrote is untrusted: shown with the package's `Markdown` (elements, never HTML), never with `dangerouslySetInnerHTML`, never as a script or a style; a picture from elsewhere is read through the console (`RemotePicturesContext`, which the composition provides from the client; [ADR-0048](/docs/adr/0048-a-picture-from-elsewhere-is-read-through-the-console-from-a-public-address-and-kept-a-while.md)).
94
94
  - A token never goes into a page, a bundle, a configuration file or a repository: `console serve` carries it, and a script reads it from the environment.
95
- - The policy stays: a build is served under a content security policy that runs nothing inline and loads nothing from elsewhere but pictures over https. Scripts, styles and fonts are bundled, never loaded from a CDN or written inline.
95
+ - The policy stays: a build is served under a content security policy that runs nothing inline and loads nothing from elsewhere, pictures included, which come through the console. Scripts, styles and fonts are bundled, never loaded from a CDN or written inline, and a picture of the console's own pages is in the build or reached through `GET /api/v1/pictures`.
96
96
  - The package is pinned by version: the API is `/api/v1`, the schemas in the package are the contract of the version it was built against, and a console is rebuilt to take a new one.
97
97
  - The marks of SkillCDN are trademarks, not part of the MIT license: a console of one's own shows its own marks through `brand`, or none.
98
98
  - Nothing of the server is rebuilt: a console shows the organization's board and holds nothing of its own. A second database is not a console.
@@ -1,3 +1,3 @@
1
1
  # Setting up, when told to
2
2
 
3
- A task or a person may ask for the project itself rather than work in it: a project made or changed (`console project new <key> "<name>"`, `console project edit <key>`), its mark (`console project icon <key> <file>`), its members (`console member add <login>`, `console member role`, `console member remove`), the workspace's categories (`console category new <key> "<name>"`, `console category edit`, `console category remove`), what a person is (`console person role <login> <admin|member>`), its goals (`console goal new`), and its rules (`console rules write --file <path>`, from [the example](project-rules.md); the workspace's with `--workspace`, for an administrator). The agent does these as its person may, an owner of the project or an administrator of the workspace ([ADR-0021](/docs/adr/0021-a-token-configures-as-its-person-does.md)); the feed names the agent beside the person on each. What the person may not, the console refuses (`auth.forbidden`): that step goes back to the person in the report, never worked around. `console people` and `console members` say who is who, by login; `console workspace` says the workspace's settings, and `console workspace edit` changes them for an administrator: `--people-visibility <everyone|shared-projects>`, who sees whom on the People page; `--week-start <auto|monday|sunday>`, the day the calendars begin a week on; `--time-zone <zone|none>`, the workspace's time zone, an IANA name such as `Asia/Seoul`, in which the command prints moments ([ADR-0035](/docs/adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)). In a project open to the workspace everyone is a member already: `console member add <login> --role owner` lists an owner, and `member role <login> member` takes one back; a plain `member add` is refused there (`member.open_project`). A login is found exactly, so an owner lists someone `console people` does not show them. A private pack is connected by an owner with `console project pack-token <key> --connect`, or, for the organization's pack, which projects read when they name no address of their own, by an administrator with `console workspace pack-token --connect` ([ADR-0036](/docs/adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)): the command prints a page at SkillCDN and a code, which the agent relays; the person approves there, signed in as someone who may open the repository, and the console keeps the SkillCDN token, shown to nobody. Until then `console skills` says the pack is not served to the console.
3
+ A task or a person may ask for the project itself rather than work in it: a project made or changed (`console project new <key> "<name>"`, `console project edit <key>`), its mark (`console project icon <key> <file>`), its members (`console member add <login>`, `console member role`, `console member remove`), the workspace's categories (`console category new <key> "<name>"`, `console category edit`, `console category remove`), what a person is (`console person role <login> <admin|member>`), its goals (`console goal new`), and its rules (`console rules write --file <path>`, from [the example](project-rules.md); the workspace's with `--workspace`, for an administrator). The agent does these as its person may, an owner of the project or an administrator of the workspace ([ADR-0021](/docs/adr/0021-a-token-configures-as-its-person-does.md)), and a project is made by an administrator alone ([ADR-0045](/docs/adr/0045-a-project-is-made-by-an-administrator.md)); the feed names the agent beside the person on each. What the person may not, the console refuses (`auth.forbidden`): that step goes back to the person in the report, never worked around. `console people` and `console members` say who is who, by login; `console workspace` says the workspace's settings, and `console workspace edit` changes them for an administrator: `--people-visibility <everyone|shared-projects>`, who sees whom on the People page; `--week-start <auto|monday|sunday>`, the day the calendars begin a week on; `--time-zone <zone|none>`, the workspace's time zone, an IANA name such as `Asia/Seoul`, in which the command prints moments ([ADR-0035](/docs/adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)). In a project open to the workspace everyone is a member already: `console member add <login> --role owner` lists an owner, and `member role <login> member` takes one back; a plain `member add` is refused there (`member.open_project`). A login is found exactly, so an owner lists someone `console people` does not show them. A private pack is connected by an owner with `console project pack-token <key> --connect`, or, for the organization's pack, which projects read when they name no address of their own, by an administrator with `console workspace pack-token --connect` ([ADR-0036](/docs/adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)): the command prints a page at SkillCDN and a code, which the agent relays; the person approves there, signed in as someone who may open the repository, and the console keeps the SkillCDN token, shown to nobody. Until then `console skills` says the pack is not served to the console.
package/src/api.ts CHANGED
@@ -72,13 +72,16 @@ export {
72
72
  MAX_CATEGORY_KEY_LENGTH,
73
73
  MAX_CATEGORY_NAME_LENGTH,
74
74
  MAX_COMMENTS_PER_TASK,
75
+ MAX_DECISIONS_PER_PROJECT,
75
76
  MAX_DOCUMENT_LENGTH,
76
77
  MAX_DOCUMENT_PATH_LENGTH,
77
78
  MAX_DOCUMENT_SEGMENT_LENGTH,
79
+ MAX_DOCUMENTS_PER_PROJECT,
78
80
  MAX_FILE_BYTES,
79
81
  MAX_FILE_NAME_LENGTH,
80
82
  MAX_FILES_PER_COMMENT,
81
83
  MAX_FILES_PER_DOCUMENT,
84
+ MAX_GOALS_PER_PROJECT,
82
85
  MAX_LANGUAGE_TAG_LENGTH,
83
86
  MAX_LINK_LABEL_LENGTH,
84
87
  MAX_LINKS,
@@ -91,13 +94,18 @@ export {
91
94
  MAX_PROJECT_KEY_LENGTH,
92
95
  MAX_PROJECT_NAME_LENGTH,
93
96
  MAX_PROJECT_SYMBOL_LENGTH,
97
+ MAX_PROJECTS_PER_WORKSPACE,
94
98
  MAX_QUESTION_LENGTH,
95
99
  MAX_REPORTS_PER_RUN,
96
100
  MAX_RULES_LENGTH,
101
+ MAX_RUNS_PER_TASK,
97
102
  MAX_SEARCH_LENGTH,
98
103
  MAX_SECRET_LENGTH,
99
104
  MAX_SKILLS_ADDRESS_LENGTH,
105
+ MAX_STREAMS,
106
+ MAX_STREAMS_PER_PERSON,
100
107
  MAX_SUMMARY_LENGTH,
108
+ MAX_TASKS_PER_PROJECT,
101
109
  MAX_TITLE_LENGTH,
102
110
  MAX_TOKEN_DAYS,
103
111
  MAX_TOKEN_NAME_LENGTH,
package/src/cli/cli.ts CHANGED
@@ -801,13 +801,19 @@ const logout: Command = async (args, io) => {
801
801
  }
802
802
  // The token is given up at the console first, so that it is good for nothing from now on;
803
803
  // one the console no longer knows is gone already. A console out of reach leaves it to the
804
- // Agents page, and the person is told so.
805
- let gone = true;
804
+ // Agents page, and the person is told so. A console that keeps it because the person is out
805
+ // (ADR-0044) is told so too: the token is theirs again when they are back in.
806
+ let outcome: "gone" | "kept" | "left" = "gone";
806
807
  try {
807
808
  await clientOf(io, url, login.token).giveUpToken();
808
809
  } catch (error) {
809
- if (!(error instanceof ApiError && error.code === "auth.required")) {
810
- gone = false;
810
+ if (
811
+ error instanceof ApiError &&
812
+ (error.code === "auth.deactivated" || error.code === "auth.not_admitted")
813
+ ) {
814
+ outcome = "kept";
815
+ } else if (!(error instanceof ApiError && error.code === "auth.required")) {
816
+ outcome = "left";
811
817
  }
812
818
  }
813
819
  const next = withoutLogin(credentials, url, login.account);
@@ -818,9 +824,11 @@ const logout: Command = async (args, io) => {
818
824
  }
819
825
  const whom = login.account === undefined ? "" : ` as ${login.account}`;
820
826
  io.stdout(
821
- gone
827
+ outcome === "gone"
822
828
  ? `Signed out of ${url}${whom}: the token is given up at the console and forgotten here.\n`
823
- : `Signed out of ${url}${whom} here, but the console could not be reached to give the token up: a person removes it on the Agents page.\n`,
829
+ : outcome === "kept"
830
+ ? `Signed out of ${url}${whom} here: the console keeps the token while the person is out, for when they are back in.\n`
831
+ : `Signed out of ${url}${whom} here, but the console could not be reached to give the token up: a person removes it on the Agents page.\n`,
824
832
  );
825
833
  envNote();
826
834
  return EXIT.ok;
@@ -2499,8 +2507,8 @@ const workspace: Command = async (args, io) => {
2499
2507
  set: (token) => client.setWorkspacePackToken({ token }),
2500
2508
  remove: () => client.removeWorkspacePackToken(),
2501
2509
  connect: () => client.connectWorkspacePackToken(),
2502
- async standing() {
2503
- const state = await client.workspacePackTokenConnection();
2510
+ async claim() {
2511
+ const state = await client.claimWorkspacePackTokenConnection();
2504
2512
  return state.status === "connected"
2505
2513
  ? { status: "connected", held: state.workspace }
2506
2514
  : state;
@@ -2660,7 +2668,8 @@ interface TokenHolder<T> {
2660
2668
  set(token: string): Promise<T>;
2661
2669
  remove(): Promise<T>;
2662
2670
  connect(): Promise<RestPackConnectStarted>;
2663
- standing(): Promise<
2671
+ /** Asks the console to claim the token at the deployment (ADR-0046), and answers how it stands. */
2672
+ claim(): Promise<
2664
2673
  | { readonly status: "none" }
2665
2674
  | { readonly status: "pending" }
2666
2675
  | { readonly status: "ended"; readonly reason: keyof typeof PACK_CONNECT_ENDED }
@@ -2692,7 +2701,7 @@ async function packToken<T>(
2692
2701
  );
2693
2702
  for (;;) {
2694
2703
  await io.sleep(started.intervalSeconds * 1000);
2695
- const state = await holder.standing();
2704
+ const state = await holder.claim();
2696
2705
  if (state.status === "pending") {
2697
2706
  continue;
2698
2707
  }
@@ -2740,8 +2749,8 @@ const projectPackToken: Command = async (args, io) => {
2740
2749
  set: (token) => project.setPackToken({ token }),
2741
2750
  remove: () => project.removePackToken(),
2742
2751
  connect: () => project.connectPackToken(),
2743
- async standing() {
2744
- const state = await project.packTokenConnection();
2752
+ async claim() {
2753
+ const state = await project.claimPackTokenConnection();
2745
2754
  return state.status === "connected" ? { status: "connected", held: state.project } : state;
2746
2755
  },
2747
2756
  },
@@ -3122,6 +3131,10 @@ function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
3122
3131
  return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
3123
3132
  case "auth.required":
3124
3133
  return "The token is not one the console knows, any more or at all. A person signs the command in again: console login\n";
3134
+ case "auth.deactivated":
3135
+ return "Tell the person: they are taken back in on the People page, by an administrator or by their organization, and this token works again then. console logout forgets it here meanwhile.\n";
3136
+ case "auth.not_admitted":
3137
+ return "Tell the person: they are back in when what let them in is named again, or when an administrator lists their login and they sign in again, and this token works again then. console logout forgets it here meanwhile.\n";
3125
3138
  case "project.not_found":
3126
3139
  return "No project has this key, or it is not yours to see. console projects lists the projects you may work in.\n";
3127
3140
  case "document.not_found":
@@ -3146,6 +3159,20 @@ function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
3146
3159
  return "What was asked is an administrator's, or an owner's of the project, and the person this token acts as is not one here. Hand the step to them.\n";
3147
3160
  case "project.key_taken":
3148
3161
  return "A project has that key already. console projects lists them.\n";
3162
+ case "project.too_many":
3163
+ return "The workspace holds as many projects as one may. Work goes on in a project there is.\n";
3164
+ case "project.too_many_bytes":
3165
+ return "The project's files hold as many bytes together as this console allows. Hand in a link to where the file is instead, and say so in the report.\n";
3166
+ case "task.too_many":
3167
+ return "The project holds as many tasks as one may, those put away counted. The work goes on in another project; ask the person which.\n";
3168
+ case "goal.too_many":
3169
+ return "The project holds as many goals as one may. console goals lists them; one done or dropped is still a goal.\n";
3170
+ case "decision.too_many":
3171
+ return "The project holds as many decisions as one may. Ask the person in the report instead.\n";
3172
+ case "document.too_many":
3173
+ return "The project holds as many pages as one may. Write on a page there is: console docs lists them.\n";
3174
+ case "run.too_many":
3175
+ return "The task has been taken as many times as one may. Write a new task for what is left, and take that.\n";
3149
3176
  case "project.invalid_address":
3150
3177
  return "The skills address is not one as SkillCDN spells it; none makes the organization's skills the project's.\n";
3151
3178
  case "project.invalid_icon":
package/src/cli/help.ts CHANGED
@@ -7,11 +7,14 @@ import {
7
7
  MAX_CATEGORY_KEY_LENGTH,
8
8
  MAX_CATEGORY_NAME_LENGTH,
9
9
  MAX_COMMENTS_PER_TASK,
10
+ MAX_DECISIONS_PER_PROJECT,
10
11
  MAX_DOCUMENT_LENGTH,
11
12
  MAX_DOCUMENT_PATH_LENGTH,
13
+ MAX_DOCUMENTS_PER_PROJECT,
12
14
  MAX_FILE_BYTES,
13
15
  MAX_FILES_PER_COMMENT,
14
16
  MAX_FILES_PER_DOCUMENT,
17
+ MAX_GOALS_PER_PROJECT,
15
18
  MAX_LINK_LABEL_LENGTH,
16
19
  MAX_NOTE_LENGTH,
17
20
  MAX_OPTION_LABEL_LENGTH,
@@ -21,11 +24,14 @@ import {
21
24
  MAX_PROJECT_KEY_LENGTH,
22
25
  MAX_PROJECT_NAME_LENGTH,
23
26
  MAX_PROJECT_SYMBOL_LENGTH,
27
+ MAX_PROJECTS_PER_WORKSPACE,
24
28
  MAX_QUESTION_LENGTH,
25
29
  MAX_REPORTS_PER_RUN,
26
30
  MAX_RULES_LENGTH,
31
+ MAX_RUNS_PER_TASK,
27
32
  MAX_SEARCH_LENGTH,
28
33
  MAX_SUMMARY_LENGTH,
34
+ MAX_TASKS_PER_PROJECT,
29
35
  MAX_TITLE_LENGTH,
30
36
  MAX_VERSIONS_PER_DOCUMENT,
31
37
  MAX_WORKERS,
@@ -59,6 +65,7 @@ export interface CommandHelp {
59
65
  /** The refusals any command may meet, said once in the overview. */
60
66
  const COMMON_REFUSALS = [
61
67
  "auth.required: the token is not one the console knows, any more or at all; a person signs the command in again",
68
+ "auth.deactivated, auth.not_admitted: the person this token acts as is out, deactivated or no longer let in; the console keeps the token, and it works again when they are back in",
62
69
  "request.invalid: what was sent is over a limit or malformed; the message names the field",
63
70
  "network: the console could not be reached at its address",
64
71
  ];
@@ -171,8 +178,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
171
178
  `title: one line, up to ${MAX_TITLE_LENGTH} characters, saying what is to be done on its own; what it refers to goes in the body`,
172
179
  `body: ${MARKDOWN}`,
173
180
  ...TASK_FIELD_LIMITS,
181
+ `at most ${MAX_TASKS_PER_PROJECT} tasks in one project, those put away counted`,
182
+ ],
183
+ refusals: [
184
+ "task.not_found: no task has that number or id",
185
+ ...TASK_FIELD_REFUSALS,
186
+ "task.too_many: the project holds as many tasks as one may; the work goes on in another project",
174
187
  ],
175
- refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
176
188
  },
177
189
  goals: {
178
190
  usage:
@@ -193,11 +205,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
193
205
  `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
194
206
  `body: ${MARKDOWN}`,
195
207
  `starts, due: ${DAY}`,
208
+ `at most ${MAX_GOALS_PER_PROJECT} goals in one project`,
196
209
  ],
197
210
  refusals: [
198
211
  "goal.not_found: no goal of this project has that id",
199
212
  "goal.invalid_category: no category of the workspace has that key",
200
213
  "goal.invalid_dates: the target day is before the start",
214
+ "goal.too_many: the project holds as many goals as one may",
201
215
  "auth.forbidden: only an owner of the project sets its roadmap",
202
216
  ],
203
217
  },
@@ -259,11 +273,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
259
273
  `name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
260
274
  `color: ${COLORS}; symbol: up to ${MAX_PROJECT_SYMBOL_LENGTH} characters without spaces, shown on the colour; left out, the pages choose both from the key and the name`,
261
275
  `image: a PNG, a JPEG, a GIF or a WebP of at most ${MAX_PROJECT_ICON_BYTES} bytes`,
276
+ `at most ${MAX_PROJECTS_PER_WORKSPACE} projects in the workspace`,
262
277
  ],
263
278
  refusals: [
264
279
  "project.not_found: no project has that key, or it is not yours to see",
265
- "auth.forbidden: only an owner of the project changes it",
280
+ "auth.forbidden: only an administrator makes a project, and only an owner changes it",
266
281
  "project.key_taken: a project has that key already",
282
+ "project.too_many: the workspace holds as many projects as one may",
267
283
  "project.invalid_address: the skills address is not one as SkillCDN spells it",
268
284
  "project.invalid_icon: the file is not an image of those kinds, by its bytes",
269
285
  "request.too_large: the image is over the limit",
@@ -358,10 +374,14 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
358
374
  synopsis: "Takes a task: a run begins, and the task is in progress and the person's.",
359
375
  about:
360
376
  "Takes a task: a run begins, and the task is in progress and the person's; the run's id and the task's id are printed on a line of their own, then the task, then the rules of the board. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
361
- limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
377
+ limits: [
378
+ `agent: one line, up to ${MAX_AGENT_LENGTH} characters`,
379
+ `at most ${MAX_RUNS_PER_TASK} runs on one task`,
380
+ ],
362
381
  refusals: [
363
382
  "task.not_found: no task has that number or id",
364
383
  "run.task_closed: the task is done or dropped",
384
+ "run.too_many: the task has been taken as many times as one may; write a new task for what is left",
365
385
  ],
366
386
  },
367
387
  report: {
@@ -391,6 +411,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
391
411
  ...RUN_REFUSALS,
392
412
  "run.too_many_artifacts: the run carries as many as one may",
393
413
  "request.too_large: the file is over the limit",
414
+ "project.too_many_bytes: the project's files hold as many bytes together as this console allows; hand in a link instead",
394
415
  ],
395
416
  },
396
417
  ask: {
@@ -406,11 +427,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
406
427
  `options: ${MIN_OPTIONS} to ${MAX_OPTIONS}, each one line up to ${MAX_OPTION_LABEL_LENGTH} characters`,
407
428
  `body: ${MARKDOWN}`,
408
429
  "to: a login of the project, or me",
430
+ `at most ${MAX_DECISIONS_PER_PROJECT} decisions in one project`,
409
431
  ],
410
432
  refusals: [
411
433
  ...RUN_CHOICE,
412
434
  "decision.invalid_run: the run is not yours, or is over",
413
435
  "decision.invalid_decider: the person named may not work in the project",
436
+ "decision.too_many: the project holds as many decisions as one may",
414
437
  ],
415
438
  },
416
439
  answer: {
@@ -494,6 +517,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
494
517
  "comment.too_many: the task carries as many comments as one may",
495
518
  "comment.too_many_files: more files than one comment may carry",
496
519
  "request.too_large: the files are over the limit together",
520
+ "project.too_many_bytes: the project's files hold as many bytes together as this console allows; say where the file is instead",
497
521
  ],
498
522
  },
499
523
  file: {
@@ -525,13 +549,14 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
525
549
  `path: ${DOCUMENT_PATH}`,
526
550
  `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
527
551
  `body: Markdown, up to ${MAX_DOCUMENT_LENGTH} characters`,
528
- `at most ${MAX_VERSIONS_PER_DOCUMENT} versions of one page`,
552
+ `at most ${MAX_VERSIONS_PER_DOCUMENT} versions of one page, and ${MAX_DOCUMENTS_PER_PROJECT} pages in one project`,
529
553
  ],
530
554
  refusals: [
531
555
  "document.invalid_path: the path is not one",
532
556
  "document.conflict: the page has moved on since --base; read it again",
533
557
  "document.archived: the page is archived; restore it first",
534
558
  "document.too_many_versions: the page carries as many versions as one may; write a new page",
559
+ "document.too_many: the project holds as many pages as one may; write on a page there is",
535
560
  ],
536
561
  },
537
562
  attach: {
@@ -548,6 +573,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
548
573
  "document.archived: the page is archived; restore it first",
549
574
  "document.too_many_files: the page carries as many files as one may",
550
575
  "request.too_large: the file is over the limit",
576
+ "project.too_many_bytes: the project's files hold as many bytes together as this console allows; link to where the file is instead",
551
577
  ],
552
578
  },
553
579
  archive: {
package/src/client.ts CHANGED
@@ -208,6 +208,8 @@ export interface EventFilter {
208
208
  readonly run?: string | undefined;
209
209
  readonly decision?: string | undefined;
210
210
  readonly document?: string | undefined;
211
+ /** True for the newest events, oldest first, and whether there are earlier ones; `after` is not looked at then. */
212
+ readonly latest?: boolean | undefined;
211
213
  }
212
214
 
213
215
  /** One page of a listing: how many items, from which one on; the server's bounds apply. */
@@ -364,8 +366,14 @@ export interface ProjectClient {
364
366
  removePackToken(): Promise<RestProject>;
365
367
  /** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
366
368
  connectPackToken(): Promise<RestPackConnectStarted>;
367
- /** How that connection stands; the token is kept when the person approved. For an owner. */
369
+ /** How that connection stands, read and changed nothing: pending, ended, or none; for an owner. */
368
370
  packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
371
+ /**
372
+ * Asks the console to claim the token at the deployment (ADR-0046): pending while the person
373
+ * has not approved, connected once, the token kept and the project as it stands, or ended;
374
+ * for an owner.
375
+ */
376
+ claimPackTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
369
377
  /** Gives the connection up; for an owner. */
370
378
  cancelPackTokenConnection(): Promise<void>;
371
379
  /** The rules that hold in the project (ADR-0023): the workspace's, and what the project adds. */
@@ -414,6 +422,7 @@ export interface ConsoleClient {
414
422
  removeWorkspacePackToken(): Promise<RestWorkspace>;
415
423
  connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
416
424
  workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
425
+ claimWorkspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
417
426
  cancelWorkspacePackTokenConnection(): Promise<void>;
418
427
  /** The people the asker may see: everyone, or those they share a project with (ADR-0035). */
419
428
  people(signal?: AbortSignal): Promise<RestPeople>;
@@ -436,6 +445,8 @@ export interface ConsoleClient {
436
445
  project(key: string): ProjectClient;
437
446
  /** The workspace's own events, the ones about no project, after event number `after`. */
438
447
  events(after: number, signal?: AbortSignal): Promise<RestEvents>;
448
+ /** Where a picture from elsewhere is read through the console (ADR-0048), given the https URL that names it. */
449
+ pictureUrl(url: string): string;
439
450
  /** The categories tasks are sorted into (ADR-0019), by name. */
440
451
  categories(signal?: AbortSignal): Promise<RestCategories>;
441
452
  /** The workspace's rules (ADR-0023): the console's own until an administrator writes the organization's. */
@@ -752,6 +763,7 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
752
763
  "GET",
753
764
  withQuery(at("events"), {
754
765
  after: String(after),
766
+ latest: filter.latest === true ? "true" : undefined,
755
767
  task: filter.task,
756
768
  run: filter.run,
757
769
  decision: filter.decision,
@@ -828,6 +840,14 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
828
840
  restPackConnectSchema,
829
841
  signal,
830
842
  ),
843
+ claimPackTokenConnection: (signal) =>
844
+ request(
845
+ "POST",
846
+ `${base}${at()}/pack-token/connect/claim`,
847
+ undefined,
848
+ restPackConnectSchema,
849
+ signal,
850
+ ),
831
851
  cancelPackTokenConnection: () =>
832
852
  request("DELETE", `${base}${at()}/pack-token/connect`, undefined, undefined),
833
853
  rules: (signal) =>
@@ -931,6 +951,14 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
931
951
  restWorkspacePackConnectSchema,
932
952
  signal,
933
953
  ),
954
+ claimWorkspacePackTokenConnection: (signal) =>
955
+ request(
956
+ "POST",
957
+ `${base}${REST_ROUTES.workspacePackToken}/connect/claim`,
958
+ undefined,
959
+ restWorkspacePackConnectSchema,
960
+ signal,
961
+ ),
934
962
  cancelWorkspacePackTokenConnection: () =>
935
963
  request("DELETE", `${base}${REST_ROUTES.workspacePackToken}/connect`, undefined, undefined),
936
964
  updatePerson: (id, patch) =>
@@ -952,6 +980,7 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
952
980
  restEventsSchema,
953
981
  signal,
954
982
  ),
983
+ pictureUrl: (url) => withQuery(REST_ROUTES.pictures, { src: url }),
955
984
  categories: (signal) =>
956
985
  request("GET", `${base}${REST_ROUTES.categories}`, undefined, restCategoriesSchema, signal),
957
986
  rules: (signal) =>
package/src/compat.ts CHANGED
@@ -113,7 +113,7 @@ export interface ClientPolicy {
113
113
  * last changed what the command reads or sends (ADR-0038), whose changelog entry begins with
114
114
  * `Breaking:`; `pnpm check` holds it to a version the package has, or is about to have.
115
115
  */
116
- export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.0" };
116
+ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.1" };
117
117
 
118
118
  /**
119
119
  * The oldest console this command works with (ADR-0038): raised by the release that last
@@ -122,7 +122,7 @@ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.0" };
122
122
  * itself, naming the command to install instead; `pnpm check` holds it as it holds the
123
123
  * console's floor.
124
124
  */
125
- export const CONSOLE_AT_LEAST = "0.4.0";
125
+ export const CONSOLE_AT_LEAST = "0.4.1";
126
126
 
127
127
  /** `common` from `atLeast` on, where that floor is within it; `common` as it is otherwise. */
128
128
  function narrowed(common: VersionRange, atLeast: string | undefined): VersionRange {