@hazem3500/palette 0.9.109 → 0.9.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NODE.md CHANGED
@@ -166,3 +166,28 @@ To roll back, restore the backup. Redeploying the old image is not enough: the u
166
166
  The API's storage contract is `prepare().bind().first()/all()/run()` plus atomic `batch()`. `server/postgres.ts` implements it with a PostgreSQL pool, parameterized statements, and one transaction per batch. `server/postgres/001_initial.sql` mirrors the SQLite schema and its quota, revision, retention, and deletion triggers; later numbered files, such as `005_snapshots.sql` for element snapshots (`bytea`), mirror later D1 migrations. Add a numbered PostgreSQL migration whenever the D1 schema changes. Existing D1 migrations and the Worker entry point stay separate. The Node server runs the same request handler; transport and storage are the only new adapters.
167
167
 
168
168
  The Node process runs retention cleanup after requests, as the self-hosted Worker does. It handles `SIGTERM` by closing its HTTP listener and database pool. Database connection failures prevent startup; request errors return the existing API error format. Rate limits use the direct socket IP by default, and never trust a client's `X-Forwarded-For`. Behind trusted reverse proxies, set `PALETTE_TRUSTED_PROXY_HOPS` (older name `PALETTE_PROXY_HOPS`, 0 to 8) to the number of `X-Forwarded-For` entries those proxies append before the app. Most proxies append one; Google Cloud's external Application Load Balancer appends two (see [Deploy behind Google Cloud IAP](#deploy-behind-google-cloud-iap)). The client address is then that many entries from the right; entries further left are ignored. If the header has fewer entries, or that entry isn't an IP address, the socket IP is used. Check your load balancer’s forwarding behavior for the correct value, and only set it when requests can't bypass those proxies. Client-supplied `CF-Connecting-IP` is ignored. The Cloudflare Worker uses Cloudflare's `CF-Connecting-IP`.
169
+
170
+ ### Proxy access tokens (operator opt-in)
171
+
172
+ Proxy access tokens are off by default. To enable management and use, keep
173
+ `PALETTE_PROXY_AUTH=gcp-iap`, `PALETTE_PROXY_AUDIENCE` set to your IAP audience,
174
+ and your existing `PALETTE_AUTH_PATH_PREFIX` (for example `/backoffice`); additionally set
175
+ `PALETTE_PROXY_ACCESS_TOKENS=true` (Worker vars or Node environment). The legacy
176
+ `KOMO_PROXY_ACCESS_TOKENS` alias works when the Palette setting is unset.
177
+ Preserve your existing public path and proxy routing configuration.
178
+
179
+ Mint, list and revoke require a verified Google account's valid project session
180
+ issued through the proxy. A PAT cannot manage tokens. Mint requests must supply
181
+ `projects` as a nonempty explicit grant, `expiresInDays` from 1 to 365, and at
182
+ least one of `comments:read` / `comments:write`. The account UI offers these
183
+ constraints; the list response adds `policy: { explicitProjects: true,
184
+ maxLifetimeDays: 365 }`. `config.tokens` advertises the opt-in.
185
+
186
+ The operator opts into credentials that remain usable independently of IAP access
187
+ until they expire or are revoked; removing IAP access alone does not revoke a PAT.
188
+
189
+ Existing tokens with no expiry, a lifetime over 365 days, or no explicit project
190
+ grant are refused behind the proxy even after opt-in. Bounded, explicitly scoped
191
+ existing tokens remain usable. Nonproxy token policy is unchanged. Snapshot GET
192
+ requires `comments:read`, the granted project and an allowed Origin. No deployment
193
+ is enabled by this configuration example.
package/README.md CHANGED
@@ -17,21 +17,16 @@ Review actions update immediately while saving in the background. Failed writes
17
17
 
18
18
  ```sh
19
19
  npm install @hazem3500/palette
20
- npx @hazem3500/palette init
21
20
  ```
22
21
 
23
22
  Requires Node.js 22 or newer. The unscoped `komo` package on npm is unrelated; use `@hazem3500/palette`.
24
23
 
25
- The setup command detects your Git repository and generates `palette.config.js` before sign-in. Mount that helper in your app:
24
+ Palette has no hosted default. Comments go to a Palette server that you run, and every install sets `endpoint` to it. Start a server first:
26
25
 
27
- ```js
28
- import { initPalette } from './palette.config.js';
29
- initPalette();
30
- ```
26
+ - **Node and PostgreSQL**, including Cloud Run: run `npx @hazem3500/palette init --node` and follow the [Node self-hosting guide](./NODE.md).
27
+ - **Cloudflare Worker and D1**: run `npx @hazem3500/palette init --self-host`; see [Self-host](#self-host).
31
28
 
32
- Keep the setup terminal open and start your app in another terminal. Choose **Connect** in your app’s sidebar. Google sign-in opens in a separate window; Palette creates the project and returns you to feedback on your site. The CLI saves the final public settings to `.palette/project.json`, updates the helper, and prints an inline configuration. Use `--origin` for a different dev port or deployed site. If interrupted or expired, run `palette init` again to resume; completed projects are never overwritten.
33
-
34
- Hosted onboarding is available through the dedicated Palette service. Self-hosting is available through the same CLI.
29
+ Both print the `endpoint` and project key for your site. If your team already runs a Palette server with hosted setup turned on (`PALETTE_HOSTED=true`), connect to it with `npx @hazem3500/palette init --endpoint https://palette.example.com`; see [Hosted setup](#hosted-setup).
35
30
 
36
31
  ## Usage
37
32
 
@@ -39,13 +34,17 @@ Mount after the page has loaded:
39
34
 
40
35
  ```js
41
36
  import { initPalette } from '@hazem3500/palette';
42
- initPalette({ project: 'YOUR_PROJECT_KEY' });
37
+ initPalette({
38
+ endpoint: 'https://palette.example.com',
39
+ project: 'YOUR_PROJECT_KEY',
40
+ });
43
41
  ```
44
42
 
45
43
  Optional configuration stays at the call site:
46
44
 
47
45
  ```js
48
46
  initPalette({
47
+ endpoint: 'https://palette.example.com',
49
48
  project: 'YOUR_PROJECT_KEY',
50
49
  enabled: import.meta.env.DEV || import.meta.env.PUBLIC_PREVIEW === 'true',
51
50
  pageRoot: document.querySelector('#app'),
@@ -61,14 +60,17 @@ With React 18.2 or 19, call the hook once near your app root:
61
60
  import { usePalette } from '@hazem3500/palette/react';
62
61
 
63
62
  export function Palette() {
64
- usePalette({ project: 'YOUR_PROJECT_KEY' });
63
+ usePalette({
64
+ endpoint: 'https://palette.example.com',
65
+ project: 'YOUR_PROJECT_KEY',
66
+ });
65
67
  return null;
66
68
  }
67
69
  ```
68
70
 
69
71
  Render `<Palette />` in your app or layout. The hook handles mounting, cleanup, and React Strict Mode. Inline configuration objects are supported; unchanged values do not restart Palette. Changing configuration remounts it, and `enabled: false` removes it. Memoize callback options with `useCallback` and pass stable DOM elements for `pageRoot` or `drawerContainer`. Mount only one hook per page; do not combine it with a separate `initPalette()` call. Server rendering does not mount the tool.
70
72
 
71
- The **endpoint is the comments API**, not your website or preview URL. Hosted Palette defaults to `https://komo.offbr.co`. Google sign-in returns to `https://komo.offbr.co/auth/google/callback`, regardless of the website embedding Palette. For self-hosting, pass your Worker or Node API URL as `endpoint`. Your current page comes from the browser. Setup detects repository metadata from Git; include its printed `repo` value to enrich agent prompts.
73
+ The **endpoint is the comments API**, your Palette server, not your website or preview URL. It is required: without it, Palette logs an error and does not mount. Google sign-in returns to `ENDPOINT/auth/google/callback`, regardless of the website embedding Palette. Your current page comes from the browser. Setup detects repository metadata from Git; include its printed `repo` value to enrich agent prompts.
72
74
 
73
75
  Comments are shared across deployments by default. Use `pnpm exec palette init --branch-scope` to separate them by branch. For automatic branch detection, import from the optional generated `palette.config.js` helper and run `palette sync` before builds. It detects the current branch from deployment environment variables or Git; set `PALETTE_BRANCH` if neither is available. It fails rather than silently grouping unknown branches.
74
76
 
@@ -104,7 +106,7 @@ Palette was called komo. Existing installs keep working without changes.
104
106
  - **Crowded pins** consolidate. Comments on the same target (its persistent owner, including a custom `anchorTarget`) draw as one tight avatar stack at the oldest comment's anchor; more than three collapse to the first author and a count. Hover, keyboard focus or a tap fans the stack out in creation order, and each pin opens its own thread. Pins on different targets whose 24px targets overlap at the current zoom or Frame scale merge into a count bubble; it opens a keyboard-navigable list of those comments. Grouping never changes anchors or saves anything, the open comment's pin is always drawn at its own anchor, and a pin moved with **Move pin** stays out of stacks and bubbles. Set `pins.cluster: false` to draw every pin on its own.
105
107
  - Keyboard shortcuts: C to comment, V to browse, D to toggle Design mode, L to view all comments, H to hide or show comments, Shift+K to restore. Remap or disable them with `shortcuts`; see [Choosing shortcuts](#choosing-shortcuts). In Design mode, ? or **Keyboard shortcuts** in the empty inspector lists every canvas key and gesture.
106
108
  - Copy open feedback with selectors, source paths, page URLs, replies, and geometry into an agent.
107
- - Hosted Cloudflare storage, or your own Worker/D1 or Node/PostgreSQL service.
109
+ - Your own Worker/D1 or Node/PostgreSQL service.
108
110
 
109
111
  Comment pins stay anchored during ordinary pointer drags. Authors and verified reviewers can choose **Comment actions → Move pin** to reposition one explicitly: drag it, or use arrow keys (Shift for larger steps), then Enter or **Save position**. Escape or **Cancel move** restores its previous position without saving. Completed moves use the existing thread anchor endpoint and remain attached after reload; the page element itself never moves. Dragging a floating comment card is independent of pin placement.
110
112
 
@@ -481,7 +483,7 @@ if (import.meta.env.DEV && isShell(palette.project)) {
481
483
 
482
484
  Give a bot its own revocable credential instead of sharing your login.
483
485
 
484
- 1. **Create a token.** Signed in with Google, open **Account → API tokens** in the Palette toolbar, or `/account` on your Palette server (`https://komo.offbr.co/account` when hosted). Name the token, pick an expiry, choose scopes, and optionally limit it to projects you own or have joined. The token is shown once; copy it into your secret manager. Guest sessions cannot create tokens.
486
+ 1. **Create a token.** Signed in with Google, open **Account → API tokens** in the Palette toolbar, or `/account` on your Palette server. Name the token, pick an expiry, choose scopes, and optionally limit it to projects you own or have joined. The token is shown once; copy it into your secret manager. Guest sessions cannot create tokens.
485
487
  2. **Set `PALETTE_TOKEN`.** Export it in the bot's environment, or save it once with `printf %s "$PALETTE_TOKEN" | palette login --token` (stdin only; a token passed as an argument is refused). Check it with `palette whoami`. `palette logout` removes saved credentials.
486
488
  3. **Use the CLI as usual.** The project, endpoint, and origin come from `.palette/project.json` or the `PALETTE_*` overrides.
487
489
 
@@ -525,11 +527,13 @@ Projects default to link access. Owners can restrict feedback to invited Google
525
527
 
526
528
  ## Hosted setup
527
529
 
530
+ A Palette server with `PALETTE_HOSTED=true` lets Google users create their own workspaces. Point the CLI at it:
531
+
528
532
  ```sh
529
- pnpm exec palette init
533
+ pnpm exec palette init --endpoint https://palette.example.com
530
534
  ```
531
535
 
532
- Mount the generated `palette.config.js` helper and open your app. Select **Connect** in the sidebar and sign in with Google. Review the detected production and preview addresses in the sidebar and add any others, one per line. Connecting approves the current site and those listed addresses, including production sites that have not launched. Remove a suggested address to leave it unapproved. Google credentials and account-wide management stay on the Palette service; your site receives a session for its new project.
536
+ Mount the generated `palette.config.js` helper and open your app. Select **Connect** in the sidebar and sign in with Google. Review the detected production and preview addresses in the sidebar and add any others, one per line. Connecting approves the current site and those listed addresses, including production sites that have not launched. Remove a suggested address to leave it unapproved. Google credentials and account-wide management stay on that Palette server; your site receives a session for its new project.
533
537
 
534
538
  The CLI waits up to ten minutes and replaces the temporary setup configuration after connection. Keep the terminal open. Run `palette init` again after an interruption or expiry. The temporary polling secret stays in ignored `.palette/setup.json` and is never bundled into your site. A reload can resume commenting while the CLI finishes. Run `palette sync` before builds that use the generated helper.
535
539
 
@@ -539,7 +543,7 @@ Approve your deployed website in **Approved sites**. Palette detects deployment
539
543
 
540
544
  The setup page manages your account and approved sites. Leave comments on the website where you installed Palette.
541
545
 
542
- Return to `/setup?workspace=<project key>` to approve additional sites (up to ten). Only the Google owner can approve sites. The site address controls where that owner’s project can be embedded; it does not grant ownership of a domain or access to other projects. The API endpoint is the Palette service, not your website URL.
546
+ Return to `/setup?workspace=<project key>` to approve additional sites (up to ten). Only the Google owner can approve sites. The site address controls where that owner’s project can be embedded; it does not grant ownership of a domain or access to other projects. The API endpoint is your Palette server, not your website URL.
543
547
 
544
548
  Starter limits per owner/workspace:
545
549
 
@@ -588,11 +592,11 @@ For a Node container with PostgreSQL (including Cloud Run), use the [Node self-h
588
592
 
589
593
  Pass these to `initPalette(config)` from `@hazem3500/palette` or `usePalette(config)` from `@hazem3500/palette/react`.
590
594
 
591
- `YOUR_PROJECT_KEY` is a placeholder for a string. Hosted Palette generates the key during `palette init`; copy that value. For self-hosting, use the project identifier configured on your server. Reuse the same key across sites that should share feedback.
595
+ `YOUR_PROJECT_KEY` is a placeholder for a string. Hosted setup (`palette init --endpoint`) generates the key; copy that value. For self-hosting, use the project identifier configured on your server. Reuse the same key across sites that should share feedback.
592
596
 
593
597
  | Option | Type | Default | Purpose |
594
598
  | --- | --- | --- | --- |
595
- | `endpoint` | `string` | Hosted Palette API | Comments API root; HTTPS outside localhost. |
599
+ | `endpoint` | `string` | Required | Your Palette server's API root; HTTPS outside localhost. Without it, Palette logs an error and does not mount. |
596
600
  | `project` | `string` | Required | Public project identifier returned by setup. |
597
601
  | `repo` | `string` | Project key | Repository identifier, usually `owner/repo`. |
598
602
  | `scope` | `"project" \| "branch"` | `"project"` | `"project"` shares comments; `"branch"` separates branches. |
@@ -728,12 +732,12 @@ Limits: the image shows the element at the top of its scroll position and up to
728
732
 
729
733
  | Command / flag | Purpose |
730
734
  | --- | --- |
731
- | `palette init` | Create a Google-owned hosted workspace. |
735
+ | `palette init --endpoint URL` | Create a Google-owned workspace on a server with hosted setup turned on. |
732
736
  | `palette init --self-host` | Create your Worker and D1 database. |
733
737
  | `palette sync` | Regenerate the public client helper. Run before dev/build. |
734
738
  | `palette deploy` | Resume or redeploy generated self-host infrastructure. |
735
739
  | `--origin URL` | Optional website address. Otherwise detected from Cloudflare Pages, Vercel, or Netlify; defaults to localhost. |
736
- | `--endpoint URL` | Override hosted API or self-host deployment URL. |
740
+ | `--endpoint URL` | Your Palette server (or `PALETTE_ENDPOINT`). Required; there is no default. |
737
741
  | `--repo OWNER/REPO` | Override Git detection. |
738
742
  | `--branch-scope` | Opt into separate comments per branch. |
739
743
  | `--google-client-id ID` | Self-hosted Google OAuth client ID. |
@@ -855,7 +859,7 @@ npx @hazem3500/palette project import --file /path/to/comments.json
855
859
 
856
860
  Exports include threads, replies, reactions, element snapshots, anchors, and historical profiles across all pages and branches, including resolved feedback. They exclude sessions, credentials, verified emails, and membership. Imports use the destination repository and keep imported authors unverified. Retrying the same export is safe; existing imported records are not overwritten. Destination quotas still apply. Export retries are required if feedback changes while downloading. Keep export files private.
857
861
 
858
- Upgrade self-hosted deployments with `npm install @hazem3500/palette@latest` then `npx @hazem3500/palette deploy`; the CLI applies bundled database migrations before redeploying. Self-hosted project removal is controlled by your Worker configuration. Account-wide data requests remain available at ty@offbr.co.
862
+ Upgrade self-hosted deployments with `npm install @hazem3500/palette@latest` then `npx @hazem3500/palette deploy`; the CLI applies bundled database migrations before redeploying. Self-hosted project removal is controlled by your Worker configuration.
859
863
 
860
864
  Without `pageRoot`, a sole existing content element with a layout box enables Frame. Script and style elements do not count as content roots. Multiple roots or `display: contents` use Floating; pass a suitable existing app container for Frame.
861
865
 
@@ -920,3 +924,28 @@ To: #side > nav · index 0 · inside · component SideNav
920
924
  The hidden original's `display` is not proposed as a style change. The CLI reads these lines into `geometry` DesignEdit changes with `mode` (`size`, `flex-basis` or `span`), the values, `layout` and `effects`, and moves into `move` changes (`target`, `label`, `from` and `to` with `parent`, `index`, `position` and any `component`), telling agents to move the element's markup in source. Earlier proposals with `Move (translate)` or `Move (offset)` lines still parse and render as `translate`/`offset` changes; the current UI no longer produces free-form moves.
921
925
 
922
926
  Supported targets are HTML boxes and SVG roots in normal flow, flex or grid, or relatively, absolutely, fixed or sticky positioned, through positive axis-aligned 2D scale chains and normal, reflow or explicitly scaled Frames. Pointer deltas are converted to local CSS pixels, and scroll during pointer capture is accounted for. Only boxes that can't be previewed are excluded, each with its own reason in the panel and a **Select parent** action that picks the nearest resizable ancestor: inline text boxes, SVG shapes inside an SVG, `display: contents` or unrendered elements, table cells and rows, boxes split across lines or columns, rotated/skewed/3D/mirrored chains, and geometry animations in progress. A running animation only counts when it moves or sizes the box and Palette did not start it; it is named after 400ms, as a dot by the Layout title, so brief host transitions never show and the panel never reflows. This is deliberately not a universal layout editor.
927
+
928
+ ### Proxy access tokens (operator opt-in)
929
+
930
+ Proxy access tokens are off by default. To enable management and use, keep
931
+ `PALETTE_PROXY_AUTH=gcp-iap`, `PALETTE_PROXY_AUDIENCE` set to your IAP audience,
932
+ and your existing `PALETTE_AUTH_PATH_PREFIX` (for example `/backoffice`); additionally set
933
+ `PALETTE_PROXY_ACCESS_TOKENS=true` (Worker vars or Node environment). The legacy
934
+ `KOMO_PROXY_ACCESS_TOKENS` alias works when the Palette setting is unset.
935
+ Preserve your existing public path and proxy routing configuration.
936
+
937
+ Mint, list and revoke require a verified Google account's valid project session
938
+ issued through the proxy. A PAT cannot manage tokens. Mint requests must supply
939
+ `projects` as a nonempty explicit grant, `expiresInDays` from 1 to 365, and at
940
+ least one of `comments:read` / `comments:write`. The account UI offers these
941
+ constraints; the list response adds `policy: { explicitProjects: true,
942
+ maxLifetimeDays: 365 }`. `config.tokens` advertises the opt-in.
943
+
944
+ The operator opts into credentials that remain usable independently of IAP access
945
+ until they expire or are revoked; removing IAP access alone does not revoke a PAT.
946
+
947
+ Existing tokens with no expiry, a lifetime over 365 days, or no explicit project
948
+ grant are refused behind the proxy even after opt-in. Bounded, explicitly scoped
949
+ existing tokens remain usable. Nonproxy token policy is unchanged. Snapshot GET
950
+ requires `comments:read`, the granted project and an allowed Origin. No deployment
951
+ is enabled by this configuration example.
package/cli/agent.mjs CHANGED
@@ -164,12 +164,13 @@ async function configuration(flags, cwd, env) {
164
164
  const project = flags.project || setting(env, "PROJECT") || settings.project;
165
165
  if (!project)
166
166
  throw Error("Run palette init in this repository, or pass --project KEY.");
167
- const endpoint = safeUrl(
168
- flags.endpoint ||
169
- setting(env, "ENDPOINT") ||
170
- settings.endpoint ||
171
- "https://komo.offbr.co"
172
- );
167
+ const configured =
168
+ flags.endpoint || setting(env, "ENDPOINT") || settings.endpoint;
169
+ if (!configured)
170
+ throw Error(
171
+ "No Palette server configured. Pass --endpoint URL, set PALETTE_ENDPOINT, or add endpoint to .palette/project.json."
172
+ );
173
+ const endpoint = safeUrl(configured);
173
174
  const origin = safeUrl(
174
175
  flags.origin ||
175
176
  setting(env, "ORIGIN") ||
package/cli/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { spawn } from "node:child_process";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { randomUUID, randomBytes, createHash } from "node:crypto";
8
- import { branchName, clientModule, gitValue, projectLayout, repository } from "./config.mjs";
8
+ import { branchName, clientModule, gitValue, projectLayout, repository, setting } from "./config.mjs";
9
9
 
10
10
  import { installAgentWorkflow } from "./workflow.mjs";
11
11
  import { agentCommands, agentHelp, runAgent } from "./agent.mjs";
@@ -208,8 +208,12 @@ async function init() {
208
208
  if (!selfHosted && !nodeHosted) {
209
209
  const endpoint =
210
210
  flag("--endpoint") ||
211
- (resuming ? previous.endpoint : "") ||
212
- "https://komo.offbr.co";
211
+ setting(process.env, "ENDPOINT") ||
212
+ (resuming ? previous.endpoint : "");
213
+ if (!endpoint)
214
+ throw Error(
215
+ "Palette has no default server. Pass --endpoint URL or set PALETTE_ENDPOINT to your Palette server, or self-host with palette init --node (see NODE.md) or palette init --self-host."
216
+ );
213
217
  const api = new URL(endpoint);
214
218
  if (
215
219
  api.protocol !== "https:" &&
@@ -226,7 +230,7 @@ async function init() {
226
230
  setup.expiresAt <= Date.now() ||
227
231
  flag("--origin") ||
228
232
  flag("--repo") ||
229
- flag("--endpoint")
233
+ endpoint !== previous?.endpoint
230
234
  ) {
231
235
  setup = {
232
236
  ...(await request(endpoint, "/setup/start", { repo, origins })),
@@ -381,7 +385,7 @@ async function init() {
381
385
  try {
382
386
  if (args.includes("--help") || !command)
383
387
  console.log(
384
- `Palette\n\n palette init Create a Google-owned hosted workspace\n palette init --self-host Deploy your own Worker and D1 database\n palette init --node Prepare a Node/PostgreSQL project\n palette deploy Resume self-hosted deployment\n palette sync Regenerate client settings; detect the current branch\n\nOptions: --origin URL --endpoint API_URL --repo OWNER/REPO --branch-scope\nSelf-host: --google-client-id ID; Node: --preview-origin HTTPS_WILDCARD\n\nComments are shared across deployments unless --branch-scope is set.${
388
+ `Palette\n\n palette init --endpoint API_URL Connect to an existing Palette server\n palette init --self-host Deploy your own Worker and D1 database\n palette init --node Prepare a Node/PostgreSQL project\n palette deploy Resume self-hosted deployment\n palette sync Regenerate client settings; detect the current branch\n\nOptions: --origin URL --endpoint API_URL --repo OWNER/REPO --branch-scope\nSelf-host: --google-client-id ID; Node: --preview-origin HTTPS_WILDCARD\n\nComments are shared across deployments unless --branch-scope is set.${
385
389
  agentHelp
386
390
  }`
387
391
  );
@@ -1 +1 @@
1
- import{a as e}from"./chunk-DU7H4TEX.js";import"./chunk-5HZI7HRR.js";import{E as t,F as n,G as o,y as a}from"./chunk-4HBBEVFK.js";var i=864e5,r=[["7 days",7],["30 days",30],["90 days",90],["1 year",365],["Never",null]],s={"comments:read":"Read","comments:write":"Write"},p=[["year",365*i],["month",30*i],["week",7*i],["day",i],["hour",36e5],["minute",6e4]];function d(e,t=Date.now()){let n=e-t,o=new Intl.RelativeTimeFormat(void 0,{numeric:"auto",style:"short"});for(let[e,t]of p)if(Math.abs(n)>=t)return o.format(Math.round(n/t),e);return"just now"}var l=e=>new Intl.DateTimeFormat(void 0,{dateStyle:"medium",timeStyle:"short"}).format(e),c=0;function u(i,p,u){let k=t("div","account account-tokens");k.setAttribute("role","region");let f=t("style");f.textContent=".account-layer .account.account-tokens{padding-top:16px;gap:16px}\n.api-tokens-head{display:flex;align-items:center;gap:6px;margin-right:36px;min-height:30px}\n.api-tokens-head h2{margin:0;font-size:15px;font-weight:500;letter-spacing:-.01em}\n.api-tokens-head h2:focus{outline:none}\n.account-layer .account .api-tokens-back{border-radius:8px;color:#bbb}\n.api-tokens-back svg{rotate:90deg;width:16px;height:16px}\n.api-tokens-back:hover{background:color-mix(in oklab,var(--mm-ink) 7%,transparent);color:var(--mm-ink)}\n.api-tokens-lede,.api-tokens-note{margin:0;font-size:12px;line-height:1.5;color:#999}\n.api-tokens-note code,.api-token-id{font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#aaa;overflow-wrap:anywhere}\n.api-tokens-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}\n.api-token,.api-tokens-sheet,.api-tokens-empty{display:grid;gap:8px;padding:14px;border-radius:14px;background:#ffffff05;box-shadow:inset 0 0 0 1px #ffffff09;font-size:12px;color:#ddd}\n.api-tokens-empty{gap:4px;color:#999;text-align:center;padding:20px 14px}\n.api-tokens-empty strong{color:#ddd;font-weight:500}\n.api-token-row{display:flex;align-items:center;gap:8px;min-width:0}\n.api-token-row strong{font-weight:500;color:#eee;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n.api-token-chips{display:flex;flex-wrap:wrap;gap:4px}\n.api-token-chip{padding:0 6px;border-radius:5px;background:color-mix(in oklab,var(--mm-ink) 9%,transparent);color:color-mix(in oklab,var(--mm-ink) 78%,transparent);font-size:10px;font-weight:500;line-height:18px;white-space:nowrap}\n.api-token-chip[data-tone=warn]{background:#f1b77d1f;color:#f1b77d}\n.api-token-meta{display:flex;flex-wrap:wrap;gap:2px 10px;margin:0;color:#909090;font-size:11px}\n.api-token-meta div{display:flex;gap:4px}\n.api-token-meta dd{margin:0;color:#bbb}\n.api-token-meta time{cursor:default}\n.account-layer .account .api-token-revoke{margin-left:auto;padding:4px 8px;min-height:26px;border-radius:7px;font-size:11px;background:transparent;color:#fda29b}\n.api-token-revoke:hover,.api-tokens-actions .api-tokens-danger:hover{background:#fda29b14}\n.api-token-confirm{display:grid;gap:8px;padding-top:8px;box-shadow:0 -1px #ffffff0b}\n.api-token-confirm p,.api-tokens-sheet p{margin:0;line-height:1.5;color:#bbb}\n.api-tokens-actions{display:flex;justify-content:flex-end;gap:8px}\n.account-layer .account .api-tokens-actions button{width:auto;min-height:32px;padding:6px 12px;border-radius:9px;font-size:12px}\n.account-layer .account .api-tokens-actions .api-tokens-danger{background:#fda29b1a;color:#fda29b}\n.api-tokens-sheet{gap:14px;padding:16px}\n.api-tokens-sheet h3{margin:0;font-size:13px;font-weight:500;color:#eee}\n.api-tokens-sheet fieldset{border:0;margin:0;padding:0;display:grid;gap:8px;min-width:0}\n.api-tokens-sheet legend{padding:0;margin-bottom:8px;font-size:12px;color:#999}\n.account-layer .account .api-tokens-sheet input[type=text]{min-height:38px;padding:8px 10px;font-size:13px;border-radius:10px}\n.api-tokens-choices{display:flex;flex-wrap:wrap;gap:6px}\n.api-tokens-choice{position:relative;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:9px;background:#ffffff06;box-shadow:inset 0 0 0 1px #ffffff0b;color:#bbb;cursor:pointer;font-size:12px;line-height:16px}\n.api-tokens-choice:hover{color:#eee}\n.account-layer .account .api-tokens-choice input{position:absolute;inset:0;width:100%;height:100%;min-height:0;margin:0;opacity:0;cursor:pointer}\n.api-tokens-choice:has(input:checked){background:#bda6ef24;box-shadow:inset 0 0 0 1px #bda6ef80;color:#eee}\n.api-tokens-choice:has(input:focus-visible){outline:2px solid #bda6ef;outline-offset:2px}\n.api-tokens-choice small{color:#888;font-size:10px}\n.api-tokens-sheet p.api-tokens-error,.api-tokens-error{margin:0;color:#fda29b;font-size:12px;line-height:1.5}\n.api-tokens-error:empty{display:none}\n.api-tokens-secret{display:flex;gap:8px;align-items:center}\n.api-tokens-secret code{flex:1;min-width:0;padding:8px 10px;border-radius:10px;background:#00000040;box-shadow:inset 0 0 0 1px #ffffff0b;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#eee;overflow-wrap:anywhere;user-select:all}\n.account-layer .account .api-tokens-secret button{width:auto;min-height:34px;padding:0 12px;font-size:12px}\n.api-tokens-secret svg{width:14px;height:14px}\n.api-tokens-warning{display:flex;gap:7px;align-items:flex-start}\n.api-tokens-warning svg{flex:none;width:13px;height:13px;margin-top:2px;color:#f1b77d}\n.api-tokens-skeleton{height:84px;border-radius:14px;background:linear-gradient(90deg,#ffffff05,#ffffff0d,#ffffff05) 0 0/200% 100%;animation:api-tokens-shimmer 1.4s linear infinite}\n@keyframes api-tokens-shimmer{to{background-position:-200% 0}}\n@media (prefers-reduced-motion:reduce){.api-tokens-skeleton{animation:none}}";let h=t("div","api-tokens-head"),m=o("Back to account",()=>z(),"icon api-tokens-back","chevron"),g=t("h2","","API tokens");g.id="api-tokens-"+ ++c,g.tabIndex=-1,g.dataset.focusKey="api-tokens-title",k.setAttribute("aria-labelledby",g.id),h.append(m,g);let x=t("p","api-tokens-lede","Let bots and CI read or write comments as you. Treat tokens like passwords."),y=t("p","sr-only");y.setAttribute("role","status"),y.setAttribute("aria-live","polite");let b=t("div"),v=t("div"),w=t("div","morphing-menu__tooltip sidebar-tooltip");w.setAttribute("aria-hidden","true"),k.append(f,h,x,b,v,y,w),m.dataset.focusKey="api-tokens-back";let A=null,C=null,T=!1,$=!1,E="",j=null,S=new Map,N=e=>{y.textContent="",setTimeout(()=>y.textContent=e,30)},I=e=>{a()||e.animate([{opacity:0,transform:"translateY(4px)"},{opacity:1,transform:"none"}],{duration:250,easing:"cubic-bezier(.22,1,.36,1)"})},K=e=>requestAnimationFrame(()=>k.querySelector(`[data-focus-key="${e}"]`)?.focus()),R=t=>t instanceof e&&404===t.status?"This Palette server doesn’t support API tokens.":t instanceof Error?t.message:"Something went wrong. Try again.";k.focusStart=()=>K("api-tokens-title"),k.dispose=()=>{q(),w.remove()};let z=()=>{k.dispose(),u()};k.addEventListener("keydown",e=>{if("Escape"===e.key)if(e.stopPropagation(),e.preventDefault(),E){let e=E;E="",B(),K(`api-tokens-revoke-${e}`)}else j?K("api-tokens-done"):T?H():z()});let L=0,q=()=>{clearTimeout(L),delete w.dataset.open};k.addEventListener("pointerover",e=>{let t=e.target.closest?.("[data-tip]");if(!t)return q();clearTimeout(L),L=window.setTimeout(()=>{if(!t.isConnected)return;let e=k.getRootNode();w.parentNode!==e&&e instanceof ShadowRoot&&e.append(w);let n=t.getBoundingClientRect();w.textContent=t.dataset.tip??"";let o=w.offsetWidth/2;w.style.left=`${Math.max(8+o,Math.min(document.documentElement.clientWidth-8-o,n.left+n.width/2))}px`,w.style.top=n.top-8-w.offsetHeight+"px",w.dataset.open="true"},w.dataset.open?0:400)}),k.addEventListener("pointerleave",k.dispose),k.addEventListener("pointerdown",k.dispose);let D=(e,n,o)=>{let a=t("div"),i=t("dd");if(null===n)i.textContent=o;else{let e=t("time","",d(n));e.dateTime=new Date(n).toISOString(),e.dataset.tip=l(n),i.append(e,t("span","sr-only",` (${l(n)})`))}return a.append(t("dt","",e),i),a},M=(e,n,o)=>{let a=t("span","api-token-chip",e);return n&&(a.dataset.tone=n),o&&(a.dataset.tip=o),a},P=e=>e?1===e.length&&e[0]===p?"This project":1===e.length?e[0]:`${e.length} projects`:"All projects";function F(n){let a=t("li","api-token");a.dataset.token=n.id;let r=t("div","api-token-row"),p=t("strong","",n.name);p.id=`api-token-${n.id}-${g.id}`,a.setAttribute("aria-labelledby",p.id),r.append(p),n.expired&&r.append(M("Expired","warn"));let d=o(`Revoke ${n.name}`,()=>{E=n.id,S.delete(n.id),B(),K(`api-tokens-keep-${n.id}`)},"api-token-revoke");d.textContent="Revoke",d.removeAttribute("title"),d.dataset.focusKey=`api-tokens-revoke-${n.id}`,d.disabled=E===n.id,r.append(d);let l=t("div","api-token-chips");for(let e of n.scopes)l.append(M(s[e]??e,"",e));l.append(M(P(n.projects),"",n.projects?.join(", ")??"Every project you can access"));let c=t("dl","api-token-meta");c.append(D("Created",n.createdAt,""),D("Last used",n.lastUsedAt,"Never"),D(n.expired?"Expired":"Expires",n.expiresAt,"Never")),a.append(r,t("code","api-token-id",`${n.prefix}…${n.last4}`),l,c);let u=S.get(n.id);if(u){let e=t("p","api-tokens-error",u);e.setAttribute("role","alert"),a.append(e)}if(E===n.id){let r=t("div","api-token-confirm");r.setAttribute("role","group"),r.setAttribute("aria-label",`Revoke ${n.name}`);let s=t("div","api-tokens-actions"),p=o("Keep token",()=>{E="",B(),K(`api-tokens-revoke-${n.id}`)},"secondary");p.dataset.focusKey=`api-tokens-keep-${n.id}`;let d=o("Revoke token",()=>{!async function(t){if(!A)return;let n=A,o=n.tokens.findIndex(e=>e.id===t.id);if(o<0)return;n.tokens.splice(o,1),E="",B();let a=n.tokens[o]??n.tokens[o-1];K(a?`api-tokens-revoke-${a.id}`:"api-tokens-new");try{await i.request(`account/tokens/${encodeURIComponent(t.id)}`,"DELETE"),N(`Revoked ${t.name}.`)}catch(a){if(a instanceof e&&404===a.status)return void N(`Revoked ${t.name}.`);if(A!==n)return;n.tokens.splice(Math.min(o,n.tokens.length),0,t),S.set(t.id,`Couldn’t revoke: ${R(a)}`),B(),N(`Couldn’t revoke ${t.name}.`),K(`api-tokens-revoke-${t.id}`)}}(n)},"secondary api-tokens-danger");d.dataset.focusKey=`api-tokens-confirm-${n.id}`,s.append(p,d),r.append(t("p","","Anything using this token stops working right away. This can’t be undone."),s),a.append(r),I(r)}return a}function H(){T=!1,B(),K("api-tokens-new")}let Y="";function B(){q();let a=A?j?"secret":T?"form":"list":C?"error":"loading",d=[];if(A&&"list"===a){let e=o("New token",()=>{T=!0,B(),K("api-tokens-name")},"secondary");e.replaceChildren(n("plus"),document.createTextNode("New token")),e.removeAttribute("title"),e.dataset.focusKey="api-tokens-new",d.push(e)}else A&&"form"===a?d.push(function(n){let a=t("form","api-tokens-sheet");a.noValidate=!0,a.setAttribute("aria-labelledby",`${g.id}-new`);let d=t("h3","","New token");d.id=`${g.id}-new`;let l=t("label","account-name-label","Name"),c=t("input");c.type="text",c.maxLength=60,c.required=!0,c.autocomplete="off",c.placeholder="e.g. Deploy bot",c.dataset.focusKey="api-tokens-name",l.append(c);let u=(e,n,o,a)=>{let i=t("fieldset"),r=t("div","api-tokens-choices");for(let[e,i,s,p]of a){let a=t("label","api-tokens-choice",i),d=t("input");d.type=n,d.name=o,d.value=e,d.checked=s,d.dataset.focusKey=`api-tokens-${o}-${e}`,p&&a.append(t("small","",p)),a.prepend(d),r.append(a)}return i.append(t("legend","",e),r),i},k=u("Access","checkbox","scope",n.scopes.map(e=>[e,s[e]?`${s[e]} comments`:e,"comments:read"===e])),f=n.projects.includes(p),h=u("Projects","radio","projects",[...f?[["this","This project",!0]]:[],["all","All your projects",!f]]),m=u("Expires","radio","expiry",r.map(([e,t])=>[String(t),e,30===t])),x=t("p","api-tokens-error");x.setAttribute("role","alert");let y=t("div","api-tokens-actions"),b=o("Cancel",H,"secondary");b.dataset.focusKey="api-tokens-cancel";let v=o("Create token",()=>{},"primary");return v.type="submit",v.dataset.focusKey="api-tokens-create",y.append(b,v),a.append(d,l,k,h,m,x,y),a.addEventListener("submit",async t=>{if(t.preventDefault(),$)return;let o=[...a.querySelectorAll("input[name=scope]:checked")].map(e=>e.value),r=a.querySelector("input[name=expiry]:checked")?.value,s=c.value.trim()?o.length?null:k.querySelector("input"):c;if(x.textContent=c.value.trim()?o.length?"":"Choose read, write, or both.":"Name your token.",s)s.focus();else{$=!0,v.disabled=b.disabled=!0,v.setAttribute("aria-busy","true");try{let e=await i.request("account/tokens","POST",{name:c.value.trim(),scopes:o,expiresInDays:r&&"null"!==r?Number(r):null,projects:"this"===a.querySelector("input[name=projects]:checked")?.value?[p]:[]});if(A!==n)return;n.tokens.unshift(e.item),T=!1,j=e,B(),N("Token created. Copy it now. You won’t see it again."),K("api-tokens-copy")}catch(t){x.textContent=t instanceof e&&429===t.status?"You’ve created several tokens recently. Try again later.":R(t),v.focus()}finally{$=!1,v.disabled=b.disabled=!1,v.removeAttribute("aria-busy")}}}),a}(A)):j&&d.push(function(e){let a=t("section","api-tokens-sheet");a.setAttribute("aria-labelledby",`${g.id}-secret`);let i=t("h3","",`Copy “${e.item.name}”`);i.id=`${g.id}-secret`;let r=t("p","api-tokens-warning");r.append(n("info"),t("span","","You won’t see this token again. Store it in a secret manager, never in source control."));let s=t("div","api-tokens-secret"),p=t("code","",e.token);p.setAttribute("aria-label","New API token"),p.tabIndex=0,p.dataset.focusKey="api-tokens-secret";let d=o("Copy token",async()=>{try{await navigator.clipboard.writeText(e.token),d.replaceChildren(n("check"),document.createTextNode("Copied")),N("Token copied to the clipboard.")}catch{getSelection()?.selectAllChildren(p),N("Couldn’t copy. The token is selected; copy it with your keyboard.")}},"primary");d.replaceChildren(n("copy"),document.createTextNode("Copy")),d.removeAttribute("title"),d.dataset.focusKey="api-tokens-copy",s.append(p,d);let l=t("div","api-tokens-actions"),c=o("Done",()=>{j=null,B(),K("api-tokens-new")},"secondary");return c.dataset.focusKey="api-tokens-done",l.append(c),a.append(i,r,s,l),a}(j));if(a!==Y&&d[0]&&I(d[0]),Y=a,b.replaceChildren(...d),v.setAttribute("aria-busy",String(!A&&!C)),!A){if(C){let n=t("p","api-tokens-error",R(C));n.setAttribute("role","alert");let a=C instanceof e&&[403,404].includes(C.status),i=o("Try again",()=>{U()},"secondary");i.dataset.focusKey="api-tokens-retry",v.replaceChildren(n,...a?[]:[i])}else{let e=t("div","api-tokens-list");e.setAttribute("aria-hidden","true"),e.append(t("div","api-tokens-skeleton"),t("div","api-tokens-skeleton"));let n=t("span","sr-only","Loading API tokens");n.setAttribute("role","status"),v.replaceChildren(e,n)}return}if(!A.tokens.length){let e=t("p","api-tokens-empty");return e.append(t("strong","","No tokens yet"),t("span","","Create one for a bot, a script, or a CI job.")),void v.replaceChildren(e)}let l=t("ul","api-tokens-list");l.setAttribute("aria-label","Your API tokens"),l.append(...A.tokens.map(F));let c=t("p","api-tokens-note");c.append(document.createTextNode("Use a token as "),t("code","","PALETTE_TOKEN"),document.createTextNode(" with the palette CLI.")),v.replaceChildren(l,c)}async function U(){C=null,A=null,B();try{let t=await i.request("account/tokens");if(!Array.isArray(t.tokens)||!Array.isArray(t.projects))throw new e(0,"Couldn’t read your tokens. Try again.");A={tokens:t.tokens,projects:t.projects,scopes:Array.isArray(t.scopes)?t.scopes:["comments:read","comments:write"]}}catch(e){C=e instanceof Error?e:new Error(String(e))}B()}return U(),k}export{u as accountTokens,d as relativeTime};
1
+ import{a as e}from"./chunk-DU7H4TEX.js";import"./chunk-5HZI7HRR.js";import{E as t,F as n,G as o,y as a}from"./chunk-4HBBEVFK.js";var i=864e5,r=[["7 days",7],["30 days",30],["90 days",90],["1 year",365],["Never",null]],s={"comments:read":"Read","comments:write":"Write"},p=[["year",365*i],["month",30*i],["week",7*i],["day",i],["hour",36e5],["minute",6e4]];function d(e,t=Date.now()){let n=e-t,o=new Intl.RelativeTimeFormat(void 0,{numeric:"auto",style:"short"});for(let[e,t]of p)if(Math.abs(n)>=t)return o.format(Math.round(n/t),e);return"just now"}var l=e=>new Intl.DateTimeFormat(void 0,{dateStyle:"medium",timeStyle:"short"}).format(e),c=0;function u(i,p,u){let k=t("div","account account-tokens");k.setAttribute("role","region");let f=t("style");f.textContent=".account-layer .account.account-tokens{padding-top:16px;gap:16px}\n.api-tokens-head{display:flex;align-items:center;gap:6px;margin-right:36px;min-height:30px}\n.api-tokens-head h2{margin:0;font-size:15px;font-weight:500;letter-spacing:-.01em}\n.api-tokens-head h2:focus{outline:none}\n.account-layer .account .api-tokens-back{border-radius:8px;color:#bbb}\n.api-tokens-back svg{rotate:90deg;width:16px;height:16px}\n.api-tokens-back:hover{background:color-mix(in oklab,var(--mm-ink) 7%,transparent);color:var(--mm-ink)}\n.api-tokens-lede,.api-tokens-note{margin:0;font-size:12px;line-height:1.5;color:#999}\n.api-tokens-note code,.api-token-id{font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#aaa;overflow-wrap:anywhere}\n.api-tokens-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}\n.api-token,.api-tokens-sheet,.api-tokens-empty{display:grid;gap:8px;padding:14px;border-radius:14px;background:#ffffff05;box-shadow:inset 0 0 0 1px #ffffff09;font-size:12px;color:#ddd}\n.api-tokens-empty{gap:4px;color:#999;text-align:center;padding:20px 14px}\n.api-tokens-empty strong{color:#ddd;font-weight:500}\n.api-token-row{display:flex;align-items:center;gap:8px;min-width:0}\n.api-token-row strong{font-weight:500;color:#eee;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n.api-token-chips{display:flex;flex-wrap:wrap;gap:4px}\n.api-token-chip{padding:0 6px;border-radius:5px;background:color-mix(in oklab,var(--mm-ink) 9%,transparent);color:color-mix(in oklab,var(--mm-ink) 78%,transparent);font-size:10px;font-weight:500;line-height:18px;white-space:nowrap}\n.api-token-chip[data-tone=warn]{background:#f1b77d1f;color:#f1b77d}\n.api-token-meta{display:flex;flex-wrap:wrap;gap:2px 10px;margin:0;color:#909090;font-size:11px}\n.api-token-meta div{display:flex;gap:4px}\n.api-token-meta dd{margin:0;color:#bbb}\n.api-token-meta time{cursor:default}\n.account-layer .account .api-token-revoke{margin-left:auto;padding:4px 8px;min-height:26px;border-radius:7px;font-size:11px;background:transparent;color:#fda29b}\n.api-token-revoke:hover,.api-tokens-actions .api-tokens-danger:hover{background:#fda29b14}\n.api-token-confirm{display:grid;gap:8px;padding-top:8px;box-shadow:0 -1px #ffffff0b}\n.api-token-confirm p,.api-tokens-sheet p{margin:0;line-height:1.5;color:#bbb}\n.api-tokens-actions{display:flex;justify-content:flex-end;gap:8px}\n.account-layer .account .api-tokens-actions button{width:auto;min-height:32px;padding:6px 12px;border-radius:9px;font-size:12px}\n.account-layer .account .api-tokens-actions .api-tokens-danger{background:#fda29b1a;color:#fda29b}\n.api-tokens-sheet{gap:14px;padding:16px}\n.api-tokens-sheet h3{margin:0;font-size:13px;font-weight:500;color:#eee}\n.api-tokens-sheet fieldset{border:0;margin:0;padding:0;display:grid;gap:8px;min-width:0}\n.api-tokens-sheet legend{padding:0;margin-bottom:8px;font-size:12px;color:#999}\n.account-layer .account .api-tokens-sheet input[type=text]{min-height:38px;padding:8px 10px;font-size:13px;border-radius:10px}\n.api-tokens-choices{display:flex;flex-wrap:wrap;gap:6px}\n.api-tokens-choice{position:relative;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:9px;background:#ffffff06;box-shadow:inset 0 0 0 1px #ffffff0b;color:#bbb;cursor:pointer;font-size:12px;line-height:16px}\n.api-tokens-choice:hover{color:#eee}\n.account-layer .account .api-tokens-choice input{position:absolute;inset:0;width:100%;height:100%;min-height:0;margin:0;opacity:0;cursor:pointer}\n.api-tokens-choice:has(input:checked){background:#bda6ef24;box-shadow:inset 0 0 0 1px #bda6ef80;color:#eee}\n.api-tokens-choice:has(input:focus-visible){outline:2px solid #bda6ef;outline-offset:2px}\n.api-tokens-choice small{color:#888;font-size:10px}\n.api-tokens-sheet p.api-tokens-error,.api-tokens-error{margin:0;color:#fda29b;font-size:12px;line-height:1.5}\n.api-tokens-error:empty{display:none}\n.api-tokens-secret{display:flex;gap:8px;align-items:center}\n.api-tokens-secret code{flex:1;min-width:0;padding:8px 10px;border-radius:10px;background:#00000040;box-shadow:inset 0 0 0 1px #ffffff0b;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#eee;overflow-wrap:anywhere;user-select:all}\n.account-layer .account .api-tokens-secret button{width:auto;min-height:34px;padding:0 12px;font-size:12px}\n.api-tokens-secret svg{width:14px;height:14px}\n.api-tokens-warning{display:flex;gap:7px;align-items:flex-start}\n.api-tokens-warning svg{flex:none;width:13px;height:13px;margin-top:2px;color:#f1b77d}\n.api-tokens-skeleton{height:84px;border-radius:14px;background:linear-gradient(90deg,#ffffff05,#ffffff0d,#ffffff05) 0 0/200% 100%;animation:api-tokens-shimmer 1.4s linear infinite}\n@keyframes api-tokens-shimmer{to{background-position:-200% 0}}\n@media (prefers-reduced-motion:reduce){.api-tokens-skeleton{animation:none}}";let m=t("div","api-tokens-head"),h=o("Back to account",()=>z(),"icon api-tokens-back","chevron"),g=t("h2","","API tokens");g.id="api-tokens-"+ ++c,g.tabIndex=-1,g.dataset.focusKey="api-tokens-title",k.setAttribute("aria-labelledby",g.id),m.append(h,g);let x=t("p","api-tokens-lede","Let bots and CI read or write comments as you. Treat tokens like passwords."),y=t("p","sr-only");y.setAttribute("role","status"),y.setAttribute("aria-live","polite");let b=t("div"),v=t("div"),w=t("div","morphing-menu__tooltip sidebar-tooltip");w.setAttribute("aria-hidden","true"),k.append(f,m,x,b,v,y,w),h.dataset.focusKey="api-tokens-back";let A=null,C=null,T=!1,$=!1,j="",E=null,S=new Map,N=e=>{y.textContent="",setTimeout(()=>y.textContent=e,30)},I=e=>{a()||e.animate([{opacity:0,transform:"translateY(4px)"},{opacity:1,transform:"none"}],{duration:250,easing:"cubic-bezier(.22,1,.36,1)"})},K=e=>requestAnimationFrame(()=>k.querySelector(`[data-focus-key="${e}"]`)?.focus()),R=t=>t instanceof e&&404===t.status?"This Palette server doesn’t support API tokens.":t instanceof Error?t.message:"Something went wrong. Try again.";k.focusStart=()=>K("api-tokens-title"),k.dispose=()=>{P(),w.remove()};let z=()=>{k.dispose(),u()};k.addEventListener("keydown",e=>{if("Escape"===e.key)if(e.stopPropagation(),e.preventDefault(),j){let e=j;j="",B(),K(`api-tokens-revoke-${e}`)}else E?K("api-tokens-done"):T?H():z()});let L=0,P=()=>{clearTimeout(L),delete w.dataset.open};k.addEventListener("pointerover",e=>{let t=e.target.closest?.("[data-tip]");if(!t)return P();clearTimeout(L),L=window.setTimeout(()=>{if(!t.isConnected)return;let e=k.getRootNode();w.parentNode!==e&&e instanceof ShadowRoot&&e.append(w);let n=t.getBoundingClientRect();w.textContent=t.dataset.tip??"";let o=w.offsetWidth/2;w.style.left=`${Math.max(8+o,Math.min(document.documentElement.clientWidth-8-o,n.left+n.width/2))}px`,w.style.top=n.top-8-w.offsetHeight+"px",w.dataset.open="true"},w.dataset.open?0:400)}),k.addEventListener("pointerleave",k.dispose),k.addEventListener("pointerdown",k.dispose);let q=(e,n,o)=>{let a=t("div"),i=t("dd");if(null===n)i.textContent=o;else{let e=t("time","",d(n));e.dateTime=new Date(n).toISOString(),e.dataset.tip=l(n),i.append(e,t("span","sr-only",` (${l(n)})`))}return a.append(t("dt","",e),i),a},D=(e,n,o)=>{let a=t("span","api-token-chip",e);return n&&(a.dataset.tone=n),o&&(a.dataset.tip=o),a},M=e=>e?1===e.length&&e[0]===p?"This project":1===e.length?e[0]:`${e.length} projects`:"All projects";function F(n){let a=t("li","api-token");a.dataset.token=n.id;let r=t("div","api-token-row"),p=t("strong","",n.name);p.id=`api-token-${n.id}-${g.id}`,a.setAttribute("aria-labelledby",p.id),r.append(p),n.expired&&r.append(D("Expired","warn"));let d=o(`Revoke ${n.name}`,()=>{j=n.id,S.delete(n.id),B(),K(`api-tokens-keep-${n.id}`)},"api-token-revoke");d.textContent="Revoke",d.removeAttribute("title"),d.dataset.focusKey=`api-tokens-revoke-${n.id}`,d.disabled=j===n.id,r.append(d);let l=t("div","api-token-chips");for(let e of n.scopes)l.append(D(s[e]??e,"",e));l.append(D(M(n.projects),"",n.projects?.join(", ")??"Every project you can access"));let c=t("dl","api-token-meta");c.append(q("Created",n.createdAt,""),q("Last used",n.lastUsedAt,"Never"),q(n.expired?"Expired":"Expires",n.expiresAt,"Never")),a.append(r,t("code","api-token-id",`${n.prefix}…${n.last4}`),l,c);let u=S.get(n.id);if(u){let e=t("p","api-tokens-error",u);e.setAttribute("role","alert"),a.append(e)}if(j===n.id){let r=t("div","api-token-confirm");r.setAttribute("role","group"),r.setAttribute("aria-label",`Revoke ${n.name}`);let s=t("div","api-tokens-actions"),p=o("Keep token",()=>{j="",B(),K(`api-tokens-revoke-${n.id}`)},"secondary");p.dataset.focusKey=`api-tokens-keep-${n.id}`;let d=o("Revoke token",()=>{!async function(t){if(!A)return;let n=A,o=n.tokens.findIndex(e=>e.id===t.id);if(o<0)return;n.tokens.splice(o,1),j="",B();let a=n.tokens[o]??n.tokens[o-1];K(a?`api-tokens-revoke-${a.id}`:"api-tokens-new");try{await i.request(`account/tokens/${encodeURIComponent(t.id)}`,"DELETE"),N(`Revoked ${t.name}.`)}catch(a){if(a instanceof e&&404===a.status)return void N(`Revoked ${t.name}.`);if(A!==n)return;n.tokens.splice(Math.min(o,n.tokens.length),0,t),S.set(t.id,`Couldn’t revoke: ${R(a)}`),B(),N(`Couldn’t revoke ${t.name}.`),K(`api-tokens-revoke-${t.id}`)}}(n)},"secondary api-tokens-danger");d.dataset.focusKey=`api-tokens-confirm-${n.id}`,s.append(p,d),r.append(t("p","","Anything using this token stops working right away. This can’t be undone."),s),a.append(r),I(r)}return a}function H(){T=!1,B(),K("api-tokens-new")}let Y="";function B(){P();let a=A?E?"secret":T?"form":"list":C?"error":"loading",d=[];if(A&&"list"===a){let e=o("New token",()=>{T=!0,B(),K("api-tokens-name")},"secondary");e.replaceChildren(n("plus"),document.createTextNode("New token")),e.removeAttribute("title"),e.dataset.focusKey="api-tokens-new",d.push(e)}else A&&"form"===a?d.push(function(n){let a=t("form","api-tokens-sheet");a.noValidate=!0,a.setAttribute("aria-labelledby",`${g.id}-new`);let d=t("h3","","New token");d.id=`${g.id}-new`;let l=t("label","account-name-label","Name"),c=t("input");c.type="text",c.maxLength=60,c.required=!0,c.autocomplete="off",c.placeholder="e.g. Deploy bot",c.dataset.focusKey="api-tokens-name",l.append(c);let u=(e,n,o,a)=>{let i=t("fieldset"),r=t("div","api-tokens-choices");for(let[e,i,s,p]of a){let a=t("label","api-tokens-choice",i),d=t("input");d.type=n,d.name=o,d.value=e,d.checked=s,d.dataset.focusKey=`api-tokens-${o}-${e}`,p&&a.append(t("small","",p)),a.prepend(d),r.append(a)}return i.append(t("legend","",e),r),i},k=u("Access","checkbox","scope",n.scopes.map(e=>[e,s[e]?`${s[e]} comments`:e,"comments:read"===e])),f=n.projects.includes(p),m=u("Projects","radio","projects",n.policy?.explicitProjects?n.projects.map(e=>[e,e===p?"This project":e,e===(f?p:n.projects[0])]):[...f?[["this","This project",!0]]:[],["all","All your projects",!f]]),h=u("Expires","radio","expiry",r.filter(([,e])=>!n.policy||null!==e&&e<=n.policy.maxLifetimeDays).map(([e,t])=>[String(t),e,30===t])),x=t("p","api-tokens-error");x.setAttribute("role","alert");let y=t("div","api-tokens-actions"),b=o("Cancel",H,"secondary");b.dataset.focusKey="api-tokens-cancel";let v=o("Create token",()=>{},"primary");return v.type="submit",v.dataset.focusKey="api-tokens-create",y.append(b,v),a.append(d,l,k,m,h,x,y),a.addEventListener("submit",async t=>{if(t.preventDefault(),$)return;let o=[...a.querySelectorAll("input[name=scope]:checked")].map(e=>e.value),r=a.querySelector("input[name=expiry]:checked")?.value,s=c.value.trim()?o.length?null:k.querySelector("input"):c;if(x.textContent=c.value.trim()?o.length?"":"Choose read, write, or both.":"Name your token.",s)s.focus();else{$=!0,v.disabled=b.disabled=!0,v.setAttribute("aria-busy","true");try{let e=await i.request("account/tokens","POST",{name:c.value.trim(),scopes:o,expiresInDays:r&&"null"!==r?Number(r):null,projects:n.policy?.explicitProjects?[a.querySelector("input[name=projects]:checked")?.value].filter(e=>!!e):"this"===a.querySelector("input[name=projects]:checked")?.value?[p]:[]});if(A!==n)return;n.tokens.unshift(e.item),T=!1,E=e,B(),N("Token created. Copy it now. You won’t see it again."),K("api-tokens-copy")}catch(t){x.textContent=t instanceof e&&429===t.status?"You’ve created several tokens recently. Try again later.":R(t),v.focus()}finally{$=!1,v.disabled=b.disabled=!1,v.removeAttribute("aria-busy")}}}),a}(A)):E&&d.push(function(e){let a=t("section","api-tokens-sheet");a.setAttribute("aria-labelledby",`${g.id}-secret`);let i=t("h3","",`Copy “${e.item.name}”`);i.id=`${g.id}-secret`;let r=t("p","api-tokens-warning");r.append(n("info"),t("span","","You won’t see this token again. Store it in a secret manager, never in source control."));let s=t("div","api-tokens-secret"),p=t("code","",e.token);p.setAttribute("aria-label","New API token"),p.tabIndex=0,p.dataset.focusKey="api-tokens-secret";let d=o("Copy token",async()=>{try{await navigator.clipboard.writeText(e.token),d.replaceChildren(n("check"),document.createTextNode("Copied")),N("Token copied to the clipboard.")}catch{getSelection()?.selectAllChildren(p),N("Couldn’t copy. The token is selected; copy it with your keyboard.")}},"primary");d.replaceChildren(n("copy"),document.createTextNode("Copy")),d.removeAttribute("title"),d.dataset.focusKey="api-tokens-copy",s.append(p,d);let l=t("div","api-tokens-actions"),c=o("Done",()=>{E=null,B(),K("api-tokens-new")},"secondary");return c.dataset.focusKey="api-tokens-done",l.append(c),a.append(i,r,s,l),a}(E));if(a!==Y&&d[0]&&I(d[0]),Y=a,b.replaceChildren(...d),v.setAttribute("aria-busy",String(!A&&!C)),!A){if(C){let n=t("p","api-tokens-error",R(C));n.setAttribute("role","alert");let a=C instanceof e&&[403,404].includes(C.status),i=o("Try again",()=>{U()},"secondary");i.dataset.focusKey="api-tokens-retry",v.replaceChildren(n,...a?[]:[i])}else{let e=t("div","api-tokens-list");e.setAttribute("aria-hidden","true"),e.append(t("div","api-tokens-skeleton"),t("div","api-tokens-skeleton"));let n=t("span","sr-only","Loading API tokens");n.setAttribute("role","status"),v.replaceChildren(e,n)}return}if(!A.tokens.length){let e=t("p","api-tokens-empty");return e.append(t("strong","","No tokens yet"),t("span","","Create one for a bot, a script, or a CI job.")),void v.replaceChildren(e)}let l=t("ul","api-tokens-list");l.setAttribute("aria-label","Your API tokens"),l.append(...A.tokens.map(F));let c=t("p","api-tokens-note");c.append(document.createTextNode("Use a token as "),t("code","","PALETTE_TOKEN"),document.createTextNode(" with the palette CLI.")),v.replaceChildren(l,c)}async function U(){C=null,A=null,B();try{let t=await i.request("account/tokens");if(!Array.isArray(t.tokens)||!Array.isArray(t.projects))throw new e(0,"Couldn’t read your tokens. Try again.");A={...t,scopes:Array.isArray(t.scopes)?t.scopes:["comments:read","comments:write"]}}catch(e){C=e instanceof Error?e:new Error(String(e))}B()}return U(),k}export{u as accountTokens,d as relativeTime};