diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 103817a597..23ae9b3ffa 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.814.0" + ".": "1.815.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5ee31990..820f2c5d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## [1.815.0](https://github.com/windmill-labs/windmill/compare/v1.814.0...v1.815.0) (2026-09-18) + + +### Features + +* add a workspace toggle that adds its admins and developers to new forks ([#11215](https://github.com/windmill-labs/windmill/issues/11215)) ([9d335de](https://github.com/windmill-labs/windmill/commit/9d335de87a4dbaa51038d55afe8d980761dcdfaf)) +* add an instance setting to refuse a token in MCP URLs ([#11162](https://github.com/windmill-labs/windmill/issues/11162)) ([37e493a](https://github.com/windmill-labs/windmill/commit/37e493ae66ed5c000ecac492d60fc0fdf4bda71f)) +* **ai-chat:** render get_run through the run tools' card ([#11204](https://github.com/windmill-labs/windmill/issues/11204)) ([6f9c4dc](https://github.com/windmill-labs/windmill/commit/6f9c4dc29455d13b0e64af05c2d6aa8bd5ff4fd6)) +* **ai-chat:** tell the chat which kind of app it is looking at ([#11208](https://github.com/windmill-labs/windmill/issues/11208)) ([c8c06d8](https://github.com/windmill-labs/windmill/commit/c8c06d8f79774abf109192e71a8b6fc37c7937ba)) +* attach files to a flow chat message ([#11185](https://github.com/windmill-labs/windmill/issues/11185)) ([c4c9677](https://github.com/windmill-labs/windmill/commit/c4c9677982b75c63d98ebf85b1904e0c341ba957)) +* **auth:** 2 h login links and a click-to-sign-in page for emailed ones ([#11203](https://github.com/windmill-labs/windmill/issues/11203)) ([5639187](https://github.com/windmill-labs/windmill/commit/5639187fec6d517a72e82df49d63d7438301127c)) +* cap user token expiration with an instance setting ([#11159](https://github.com/windmill-labs/windmill/issues/11159)) ([9320312](https://github.com/windmill-labs/windmill/commit/9320312eac56f944c4d31504601293ab4e816ccc)) +* flow chat job-backed detail, smooth streaming and answer chrome ([#11186](https://github.com/windmill-labs/windmill/issues/11186)) ([e2a91ca](https://github.com/windmill-labs/windmill/commit/e2a91ca2b141a0bee64ddfafe3cbf3ff86b6fb35)) +* put a data table's connection under Postgres roles ([#11020](https://github.com/windmill-labs/windmill/issues/11020)) ([0e807fb](https://github.com/windmill-labs/windmill/commit/0e807fb1dd80d7536ec144cd49445abc7961e504)) +* render an AI agent result as its answer, not as raw JSON ([#11051](https://github.com/windmill-labs/windmill/issues/11051)) ([a089928](https://github.com/windmill-labs/windmill/commit/a08992834d45d0211336f4fc32c3421646ca47c5)) +* support $flow_expr[...] dynamic tags on flow steps ([#11170](https://github.com/windmill-labs/windmill/issues/11170)) ([48f0025](https://github.com/windmill-labs/windmill/commit/48f00259c5e7361d3553dbcb809461e1cde96f8f)) + + +### Bug Fixes + +* **frontend:** inline only the package version, not the whole package.json ([#11191](https://github.com/windmill-labs/windmill/issues/11191)) ([813e486](https://github.com/windmill-labs/windmill/commit/813e486e166ac6215364817a7732b66f8dc1d463)) +* keep instance groups when editing auto-invite ([#11217](https://github.com/windmill-labs/windmill/issues/11217)) ([df61dea](https://github.com/windmill-labs/windmill/commit/df61dea5fa8b18d1e0044dc0db6702b053d5119f)) +* key the large root font size on screen width, not window width ([#11216](https://github.com/windmill-labs/windmill/issues/11216)) ([3b4e13d](https://github.com/windmill-labs/windmill/commit/3b4e13d1c564c6195e30b55b0671f7533e3ce408)) +* re-encrypt git sync secrets on workspace key rotation ([#11218](https://github.com/windmill-labs/windmill/issues/11218)) ([f0d66a4](https://github.com/windmill-labs/windmill/commit/f0d66a42eba27502767423af0c08b2ca1f16e8df)) +* re-point cloned fork identities that name nobody in the fork ([#11161](https://github.com/windmill-labs/windmill/issues/11161)) ([9690c44](https://github.com/windmill-labs/windmill/commit/9690c4462cf264a5577b87d07d465b5442b4e09d)) +* register the job token with the sensitive log masking system ([#10943](https://github.com/windmill-labs/windmill/issues/10943)) ([53afecd](https://github.com/windmill-labs/windmill/commit/53afecd4588247bc1812d3e68a30db1f3c3b2724)) +* show the New menu's description panel only on hover ([#11199](https://github.com/windmill-labs/windmill/issues/11199)) ([ecd0a6c](https://github.com/windmill-labs/windmill/commit/ecd0a6c77bc3a057b8072dbb0aca731e8bd3d882)) +* stop picker listing a draft twice when drafts share a friendly path ([#11214](https://github.com/windmill-labs/windmill/issues/11214)) ([72507d5](https://github.com/windmill-labs/windmill/commit/72507d52a021de8101794ee8f4a3392be1ba20c7)) + + +### Performance Improvements + +* count completed jobs on the v2_job_completed index with a timeout ([#11211](https://github.com/windmill-labs/windmill/issues/11211)) ([d1a2536](https://github.com/windmill-labs/windmill/commit/d1a25360b070994a31fb033707dbfde1fb553939)) + ## [1.814.0](https://github.com/windmill-labs/windmill/compare/v1.813.0...v1.814.0) (2026-09-17) diff --git a/ai_evals/adapters/frontend/mockBackend.ts b/ai_evals/adapters/frontend/mockBackend.ts index d27bb4d69a..950cc2e1b5 100644 --- a/ai_evals/adapters/frontend/mockBackend.ts +++ b/ai_evals/adapters/frontend/mockBackend.ts @@ -49,12 +49,16 @@ export interface BenchmarkWorkspaceFlow { export interface BenchmarkWorkspaceApp { path: string summary: string + /** Defaults to true. Set false for a drag-and-drop app, which the chat can list + * and read but has no tool to edit — its value is a grid, not files. */ + rawApp?: boolean value: { - files: Record - runnables: Record + files?: Record + runnables?: Record data?: unknown policy?: unknown custom_path?: unknown + [key: string]: unknown } } @@ -994,7 +998,7 @@ function buildBenchmarkListableApp(app: BenchmarkWorkspaceApp): ListableApp { extra_perms: {}, edited_at: BENCHMARK_TIMESTAMP, execution_mode: 'viewer', - raw_app: true + raw_app: app.rawApp ?? true } } @@ -1012,7 +1016,7 @@ function buildBenchmarkApp(app: BenchmarkWorkspaceApp): AppWithLastVersion { execution_mode: 'viewer', extra_perms: {}, custom_path: app.value.custom_path as string | undefined, - raw_app: true + raw_app: app.rawApp ?? true } } diff --git a/ai_evals/cases/global.yaml b/ai_evals/cases/global.yaml index 45316b2810..fdb2442729 100644 --- a/ai_evals/cases/global.yaml +++ b/ai_evals/cases/global.yaml @@ -2611,3 +2611,40 @@ judgeChecklist: - runs the existing script rather than rewriting it - passes the GitHub resource as the bare string $res:f/evals/global/github_main + +- id: global-drag-and-drop-app-not-editable + prompt: |- + Add a refresh button to the ops console app, and the same to the sales board app. + initial: ai_evals/fixtures/frontend/global/initial/apps_code_and_drag_and_drop.json + runtime: + maxTurns: 12 + validate: + # One request, two apps, only one of them editable: the code app must come back with a + # draft and the drag-and-drop one must not. Refusing both, or editing both, fails here — + # which is what makes this a test of the distinction rather than of caution. + draftCountExactly: 1 + requiredDrafts: + - type: app + path: f/evals/global/ops_console + forbiddenDrafts: + - type: app + path: f/evals/global/sales_board + toolExpect: + # Deliberately not constraining write_app_file/patch_app_file by argument: an entry there + # fails when its tool was never called, so naming both would fail on whichever the model + # did not pick. The draft assertions above cover the same ground, tool-agnostically. + forbiddenToolsUsed: + - init_app + - deploy_workspace_item + - delete_app_file + - delete_app_runnable + assistantExpect: + # A refusal leaves no draft for the judge to read, so the explanation is checked here. + # Only the app kind: substring tests cannot see paraphrase, and every wording of "I can't + # edit it" defeats a fixed list. + requiredMentionsAnyOf: + - - drag-and-drop + - drag and drop + - low-code + - no-code + skipJudge: true diff --git a/ai_evals/fixtures/frontend/global/initial/apps_code_and_drag_and_drop.json b/ai_evals/fixtures/frontend/global/initial/apps_code_and_drag_and_drop.json new file mode 100644 index 0000000000..fbef7b3e3d --- /dev/null +++ b/ai_evals/fixtures/frontend/global/initial/apps_code_and_drag_and_drop.json @@ -0,0 +1,30 @@ +{ + "user": { + "username": "admin", + "is_admin": true, + "folders": ["evals"], + "folders_read": ["evals"] + }, + "workspace": { + "apps": [ + { + "path": "f/evals/global/sales_board", + "summary": "Sales board", + "rawApp": false, + "value": { + "grid": [] + } + }, + { + "path": "f/evals/global/ops_console", + "summary": "Ops console", + "value": { + "files": { + "/App.tsx": "export default function App() {\n\treturn
Ops console
\n}\n" + }, + "runnables": {} + } + } + ] + } +} diff --git a/backend/.sqlx/query-044310c0e7745867a24a9d3653ff29c87ce48ffd89fac070c8314556f884ce55.json b/backend/.sqlx/query-044310c0e7745867a24a9d3653ff29c87ce48ffd89fac070c8314556f884ce55.json new file mode 100644 index 0000000000..be7ac83a7b --- /dev/null +++ b/backend/.sqlx/query-044310c0e7745867a24a9d3653ff29c87ce48ffd89fac070c8314556f884ce55.json @@ -0,0 +1,43 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at, s.created_by as created_by\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC\n LIMIT $3 OFFSET $4", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hash", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "deployment_msg", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "created_by", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + true, + false, + false + ] + }, + "hash": "044310c0e7745867a24a9d3653ff29c87ce48ffd89fac070c8314556f884ce55" +} diff --git a/backend/.sqlx/query-09e3f8fa8a9e64048971983584bfa00ab137c70b73f381452cadfae6e1cda934.json b/backend/.sqlx/query-09e3f8fa8a9e64048971983584bfa00ab137c70b73f381452cadfae6e1cda934.json new file mode 100644 index 0000000000..a4d174992e --- /dev/null +++ b/backend/.sqlx/query-09e3f8fa8a9e64048971983584bfa00ab137c70b73f381452cadfae6e1cda934.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE draft AS d\n SET path = $3::text,\n value = (\n SELECT CASE\n WHEN s.clean -> 'path' = to_jsonb($2::text)\n OR s.clean -> 'draft_path' = to_jsonb($2::text)\n THEN to_json(\n s.clean\n || CASE WHEN s.clean -> 'path' = to_jsonb($2::text)\n THEN jsonb_build_object('path', $3::text)\n ELSE '{}'::jsonb END\n || CASE WHEN s.clean -> 'draft_path' = to_jsonb($2::text)\n THEN jsonb_build_object('draft_path', $3::text)\n ELSE '{}'::jsonb END\n )\n ELSE d.value\n END\n FROM (SELECT replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)),\n chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean) s\n )\n WHERE d.workspace_id = $1\n AND d.path = $2::text\n AND d.typ::text = ANY($4::text[])", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "09e3f8fa8a9e64048971983584bfa00ab137c70b73f381452cadfae6e1cda934" +} diff --git a/backend/.sqlx/query-0cc6770a81ecaecafe0d9b7100f94b329c4ec7978b016af4c964415e0c38396c.json b/backend/.sqlx/query-0cc6770a81ecaecafe0d9b7100f94b329c4ec7978b016af4c964415e0c38396c.json new file mode 100644 index 0000000000..23d3b51c76 --- /dev/null +++ b/backend/.sqlx/query-0cc6770a81ecaecafe0d9b7100f94b329c4ec7978b016af4c964415e0c38396c.json @@ -0,0 +1,71 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT value as \"value!: sqlx::types::Json>\",\n created_at, base\n FROM draft\n WHERE workspace_id = $1\n AND (email = $2 OR email IS NULL)\n AND path = $3\n AND typ = $4\n ORDER BY email NULLS LAST\n LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "value!: sqlx::types::Json>", + "type_info": "Json" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "base", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + { + "Custom": { + "name": "draft_kind", + "kind": { + "Enum": [ + "script", + "flow", + "app", + "raw_app", + "resource", + "variable", + "trigger_schedule", + "trigger_webhook", + "trigger_default_email", + "trigger_email", + "trigger_http", + "trigger_websocket", + "trigger_postgres", + "trigger_kafka", + "trigger_nats", + "trigger_mqtt", + "trigger_sqs", + "trigger_gcp", + "trigger_azure", + "trigger_poll", + "trigger_cli", + "trigger_nextcloud", + "trigger_google", + "trigger_github", + "data_pipeline", + "trigger_amqp" + ] + } + } + } + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "0cc6770a81ecaecafe0d9b7100f94b329c4ec7978b016af4c964415e0c38396c" +} diff --git a/backend/.sqlx/query-0d9a83d77632bc0de19a48eddb7e6f80b2a41db4f1d352920d0c295ea3a018b1.json b/backend/.sqlx/query-0d9a83d77632bc0de19a48eddb7e6f80b2a41db4f1d352920d0c295ea3a018b1.json new file mode 100644 index 0000000000..93251596cf --- /dev/null +++ b/backend/.sqlx/query-0d9a83d77632bc0de19a48eddb7e6f80b2a41db4f1d352920d0c295ea3a018b1.json @@ -0,0 +1,64 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE draft\n SET path = $3,\n -- Both path keys, not just the typed one: the editors mirror the\n -- typed path into the other while it differs from the row's path,\n -- and the loaders prefer the mirror — left naming the old location\n -- it un-does this move on the next save. `create_missing = false`\n -- on both, so a draft carrying only one keeps only one.\n value = to_json(\n jsonb_set(\n jsonb_set(\n CASE WHEN $7::text IS NULL THEN to_jsonb(value)\n ELSE jsonb_set(to_jsonb(value), ARRAY['summary'], to_jsonb($7::text))\n END,\n ARRAY[$5::text], to_jsonb($3::text), false\n ),\n ARRAY[$8::text], to_jsonb($3::text), false\n )\n )\n WHERE workspace_id = $1\n AND path = $2\n AND typ = $4\n AND email = $6\n -- A pre-sanitizer NUL escape makes `to_jsonb` raise 22P05. Excluded\n -- here so the statement can't 500; reported below instead. Unlike the\n -- passive carry, rewriting the value IS this operation, so skipping it\n -- silently would move the row and leave its typed path stale.\n AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) = 0\n -- Skipped on a summary-only edit, where the \"target\" row is this\n -- row and the guard would refuse the update against itself.\n AND ($2 = $3 OR NOT EXISTS (\n SELECT 1 FROM draft o\n WHERE o.workspace_id = $1 AND o.path = $3 AND o.typ::text = ANY($9::text[])\n -- Of this kind only the caller's own row and the legacy one collide:\n -- teammates' drafts of one item share its path by design, but a deploy\n -- there wipes those two together, so a second would discard edits the\n -- caller never saw. The other app kind is a different item on the same\n -- deployed path, so it collides whoever owns it.\n AND (o.typ <> $4 OR o.email = $6 OR o.email IS NULL)\n ))\n RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + { + "Custom": { + "name": "draft_kind", + "kind": { + "Enum": [ + "script", + "flow", + "app", + "raw_app", + "resource", + "variable", + "trigger_schedule", + "trigger_webhook", + "trigger_default_email", + "trigger_email", + "trigger_http", + "trigger_websocket", + "trigger_postgres", + "trigger_kafka", + "trigger_nats", + "trigger_mqtt", + "trigger_sqs", + "trigger_gcp", + "trigger_azure", + "trigger_poll", + "trigger_cli", + "trigger_nextcloud", + "trigger_google", + "trigger_github", + "data_pipeline", + "trigger_amqp" + ] + } + } + }, + "Text", + "Text", + "Text", + "Text", + "TextArray" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0d9a83d77632bc0de19a48eddb7e6f80b2a41db4f1d352920d0c295ea3a018b1" +} diff --git a/backend/.sqlx/query-11af34e5fe5b8ed59453d58628ed5fb3b2120835158f22d715b73460f6a55108.json b/backend/.sqlx/query-11af34e5fe5b8ed59453d58628ed5fb3b2120835158f22d715b73460f6a55108.json new file mode 100644 index 0000000000..fdf0049344 --- /dev/null +++ b/backend/.sqlx/query-11af34e5fe5b8ed59453d58628ed5fb3b2120835158f22d715b73460f6a55108.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM draft_move WHERE email = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "11af34e5fe5b8ed59453d58628ed5fb3b2120835158f22d715b73460f6a55108" +} diff --git a/backend/.sqlx/query-9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c.json b/backend/.sqlx/query-148fbee57c6abbb6f361818c5b6bfc8a1db451d31dfddbb5be945b3551c8becb.json similarity index 57% rename from backend/.sqlx/query-9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c.json rename to backend/.sqlx/query-148fbee57c6abbb6f361818c5b6bfc8a1db451d31dfddbb5be945b3551c8becb.json index e751b8fc7c..09e5b51ab9 100644 --- a/backend/.sqlx/query-9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c.json +++ b/backend/.sqlx/query-148fbee57c6abbb6f361818c5b6bfc8a1db451d31dfddbb5be945b3551c8becb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC", + "query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at, s.created_by as created_by\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC LIMIT 1", "describe": { "columns": [ { @@ -17,6 +17,11 @@ "ordinal": 2, "name": "created_at", "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "created_by", + "type_info": "Varchar" } ], "parameters": { @@ -28,8 +33,9 @@ "nullable": [ false, true, + false, false ] }, - "hash": "9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c" + "hash": "148fbee57c6abbb6f361818c5b6bfc8a1db451d31dfddbb5be945b3551c8becb" } diff --git a/backend/.sqlx/query-18b38efcb141101192b372e7d7e7e47481877f3ab717682f3b84e665c95a5214.json b/backend/.sqlx/query-18b38efcb141101192b372e7d7e7e47481877f3ab717682f3b84e665c95a5214.json new file mode 100644 index 0000000000..d26cdf3a7d --- /dev/null +++ b/backend/.sqlx/query-18b38efcb141101192b372e7d7e7e47481877f3ab717682f3b84e665c95a5214.json @@ -0,0 +1,49 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg,\n av.created_by as created_by, av.created_at as created_at\n FROM app a\n JOIN LATERAL (\n SELECT av2.id, COALESCE(v.ord, 0) AS ord\n FROM app_version av2\n LEFT JOIN unnest(a.versions) WITH ORDINALITY AS v(id, ord) ON v.id = av2.id\n WHERE av2.app_id = a.id\n ORDER BY ord DESC, av2.id DESC\n LIMIT $3 OFFSET $4\n ) page ON TRUE\n JOIN app_version av ON av.id = page.id\n LEFT JOIN deployment_metadata dm ON av.id = dm.app_version\n WHERE a.workspace_id = $1 AND a.path = $2\n ORDER BY page.ord DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "app_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "deployment_msg", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_by", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "18b38efcb141101192b372e7d7e7e47481877f3ab717682f3b84e665c95a5214" +} diff --git a/backend/.sqlx/query-46f00a75b2e7e4ac70758a9687070f68bc0421f1aa228f80157adda63191d33b.json b/backend/.sqlx/query-1c6859291963a9ca38d73f500213a575bfcd4469524f0dc8812470737759f02b.json similarity index 74% rename from backend/.sqlx/query-46f00a75b2e7e4ac70758a9687070f68bc0421f1aa228f80157adda63191d33b.json rename to backend/.sqlx/query-1c6859291963a9ca38d73f500213a575bfcd4469524f0dc8812470737759f02b.json index dcd3573e92..7b78a7d9b4 100644 --- a/backend/.sqlx/query-46f00a75b2e7e4ac70758a9687070f68bc0421f1aa228f80157adda63191d33b.json +++ b/backend/.sqlx/query-1c6859291963a9ca38d73f500213a575bfcd4469524f0dc8812470737759f02b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH legacy AS (\n DELETE FROM draft\n WHERE workspace_id = $1 AND path = $2 AND typ = $3 AND email IS NULL\n RETURNING value\n )\n INSERT INTO draft (workspace_id, email, path, typ, value, created_at)\n SELECT $1, $4, $2, $3, value, now() FROM legacy\n ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL\n DO UPDATE SET value = EXCLUDED.value, created_at = now()\n RETURNING 1 as \"one!\"", + "query": "WITH legacy AS (\n DELETE FROM draft\n WHERE workspace_id = $1 AND path = $2 AND typ = $3 AND email IS NULL\n RETURNING value, base\n )\n INSERT INTO draft (workspace_id, email, path, typ, value, created_at, base)\n SELECT $1, $4, $2, $3, value, now(), base FROM legacy\n ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL\n DO UPDATE SET value = EXCLUDED.value, created_at = now(), base = EXCLUDED.base\n RETURNING 1 as \"one!\"", "describe": { "columns": [ { @@ -55,5 +55,5 @@ null ] }, - "hash": "46f00a75b2e7e4ac70758a9687070f68bc0421f1aa228f80157adda63191d33b" + "hash": "1c6859291963a9ca38d73f500213a575bfcd4469524f0dc8812470737759f02b" } diff --git a/backend/.sqlx/query-1d8ccd32266637d7f7915f92a8483dce0c9986f3847227c2f66daa84d4109d7d.json b/backend/.sqlx/query-1d8ccd32266637d7f7915f92a8483dce0c9986f3847227c2f66daa84d4109d7d.json deleted file mode 100644 index b9cbc6c382..0000000000 --- a/backend/.sqlx/query-1d8ccd32266637d7f7915f92a8483dce0c9986f3847227c2f66daa84d4109d7d.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url FROM workspace_settings WHERE workspace_id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Text" - ] - }, - "nullable": [] - }, - "hash": "1d8ccd32266637d7f7915f92a8483dce0c9986f3847227c2f66daa84d4109d7d" -} diff --git a/backend/.sqlx/query-232ff36c2d522383061cafbf9639698dbc44a18c0f016aae94a1705f06fd1e48.json b/backend/.sqlx/query-232ff36c2d522383061cafbf9639698dbc44a18c0f016aae94a1705f06fd1e48.json new file mode 100644 index 0000000000..f32e47b387 --- /dev/null +++ b/backend/.sqlx/query-232ff36c2d522383061cafbf9639698dbc44a18c0f016aae94a1705f06fd1e48.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO draft_move (workspace_id, typ, old_path, new_path, email)\n SELECT m.workspace_id, m.typ, m.old_path, $4, $5::text\n FROM draft_move m\n WHERE m.workspace_id = $1 AND m.typ::text = ANY($2::text[])\n AND m.new_path = $3 AND m.email IS NULL\n AND NOT EXISTS (\n SELECT 1 FROM draft_move o\n WHERE o.workspace_id = m.workspace_id AND o.typ = m.typ\n AND o.old_path = m.old_path AND o.email = $5::text\n )", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Text", + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "232ff36c2d522383061cafbf9639698dbc44a18c0f016aae94a1705f06fd1e48" +} diff --git a/backend/.sqlx/query-255ba68caa78d0e814ea817693a319f769907025f9c7d1150d45e8ecb1bff4ab.json b/backend/.sqlx/query-255ba68caa78d0e814ea817693a319f769907025f9c7d1150d45e8ecb1bff4ab.json deleted file mode 100644 index 245c65a7a8..0000000000 --- a/backend/.sqlx/query-255ba68caa78d0e814ea817693a319f769907025f9c7d1150d45e8ecb1bff4ab.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE workspace_settings SET auto_invite = $1 WHERE workspace_id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Jsonb", - "Text" - ] - }, - "nullable": [] - }, - "hash": "255ba68caa78d0e814ea817693a319f769907025f9c7d1150d45e8ecb1bff4ab" -} diff --git a/backend/.sqlx/query-2d6b117324eaf076a0ed06d2cb0ce73279957d6a39fdc6b1ecb6e0a1e02f921f.json b/backend/.sqlx/query-2d6b117324eaf076a0ed06d2cb0ce73279957d6a39fdc6b1ecb6e0a1e02f921f.json new file mode 100644 index 0000000000..4b96519a06 --- /dev/null +++ b/backend/.sqlx/query-2d6b117324eaf076a0ed06d2cb0ce73279957d6a39fdc6b1ecb6e0a1e02f921f.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO usr (workspace_id, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via)\n SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account,\n CASE WHEN $3 THEN NULL ELSE added_via END\n FROM usr WHERE workspace_id = $2\n AND (NOT $3 OR (NOT operator AND NOT disabled AND NOT is_service_account))\n ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "2d6b117324eaf076a0ed06d2cb0ce73279957d6a39fdc6b1ecb6e0a1e02f921f" +} diff --git a/backend/.sqlx/query-2f39fce0ee700117f3e4c066e0b56ee979e4c392970278304e80368b770bb7b4.json b/backend/.sqlx/query-2f39fce0ee700117f3e4c066e0b56ee979e4c392970278304e80368b770bb7b4.json new file mode 100644 index 0000000000..99b1a9472d --- /dev/null +++ b/backend/.sqlx/query-2f39fce0ee700117f3e4c066e0b56ee979e4c392970278304e80368b770bb7b4.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "add_admins_and_developers_to_forks", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "2f39fce0ee700117f3e4c066e0b56ee979e4c392970278304e80368b770bb7b4" +} diff --git a/backend/.sqlx/query-d631a26e5531589ff37e677f91a4f1d9f850e3e46c17130dd580426cda7a9f65.json b/backend/.sqlx/query-383c80239525d9c4ee90e2f5cb6e46c8a0983c52ad15db083f84a9e344dc4c99.json similarity index 78% rename from backend/.sqlx/query-d631a26e5531589ff37e677f91a4f1d9f850e3e46c17130dd580426cda7a9f65.json rename to backend/.sqlx/query-383c80239525d9c4ee90e2f5cb6e46c8a0983c52ad15db083f84a9e344dc4c99.json index 8cef9fc8aa..8a780ccda7 100644 --- a/backend/.sqlx/query-d631a26e5531589ff37e677f91a4f1d9f850e3e46c17130dd580426cda7a9f65.json +++ b/backend/.sqlx/query-383c80239525d9c4ee90e2f5cb6e46c8a0983c52ad15db083f84a9e344dc4c99.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE token SET label = $1\n WHERE email = $2 AND token_prefix = $3\n AND (label IS NULL OR (\n label <> 'session'\n AND label <> 'guest_session'\n AND lower(label) NOT LIKE 'ephemeral%'\n AND label <> 'debugger-token'\n AND label NOT LIKE 'mcp-oauth-%'\n AND NOT starts_with(label, 'embed_app:')\n AND NOT starts_with(label, 'sdk_app:')\n AND NOT starts_with(label, 'impersonation:')\n ))\n RETURNING token_prefix", + "query": "UPDATE token SET label = $1\n WHERE email = $2 AND token_prefix = $3\n AND (label IS NULL OR (\n label <> 'session'\n AND label <> 'guest_session'\n AND lower(label) NOT LIKE 'ephemeral%'\n AND label <> 'debugger-token'\n AND label NOT LIKE 'mcp-oauth-%'\n AND NOT starts_with(label, 'embed_app:')\n AND NOT starts_with(label, 'sdk_app:')\n AND NOT starts_with(label, 'impersonation:')\n AND NOT starts_with(label, 'cli-login:')\n ))\n RETURNING token_prefix", "describe": { "columns": [ { @@ -20,5 +20,5 @@ false ] }, - "hash": "d631a26e5531589ff37e677f91a4f1d9f850e3e46c17130dd580426cda7a9f65" + "hash": "383c80239525d9c4ee90e2f5cb6e46c8a0983c52ad15db083f84a9e344dc4c99" } diff --git a/backend/.sqlx/query-3a015ab279785a15f89c89d8c4d155e85370d4c2b7ed14fbb510919e7624ca1c.json b/backend/.sqlx/query-3a015ab279785a15f89c89d8c4d155e85370d4c2b7ed14fbb510919e7624ca1c.json new file mode 100644 index 0000000000..c4cc310dbf --- /dev/null +++ b/backend/.sqlx/query-3a015ab279785a15f89c89d8c4d155e85370d4c2b7ed14fbb510919e7624ca1c.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO draft_move (workspace_id, typ, old_path, new_path, email)\n SELECT $1, t::draft_kind, $3, $4, $5 FROM unnest($2::text[]) t", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "TextArray", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "3a015ab279785a15f89c89d8c4d155e85370d4c2b7ed14fbb510919e7624ca1c" +} diff --git a/backend/.sqlx/query-3db5f1d8ec1f8d867cf01c066876122e78f0c76a93a1d9860e8dff05d9a02c09.json b/backend/.sqlx/query-3db5f1d8ec1f8d867cf01c066876122e78f0c76a93a1d9860e8dff05d9a02c09.json new file mode 100644 index 0000000000..db2181ae3c --- /dev/null +++ b/backend/.sqlx/query-3db5f1d8ec1f8d867cf01c066876122e78f0c76a93a1d9860e8dff05d9a02c09.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE draft_move SET new_path = $4\n WHERE workspace_id = $1 AND typ::text = ANY($2::text[])\n AND new_path = $3 AND ($5::text IS NULL OR email = $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Text", + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "3db5f1d8ec1f8d867cf01c066876122e78f0c76a93a1d9860e8dff05d9a02c09" +} diff --git a/backend/.sqlx/query-5959c60da91f1e62080a4455e2a4627cb1fd3d8c70210bf7f38cb222dd15d88f.json b/backend/.sqlx/query-5959c60da91f1e62080a4455e2a4627cb1fd3d8c70210bf7f38cb222dd15d88f.json new file mode 100644 index 0000000000..b78808a389 --- /dev/null +++ b/backend/.sqlx/query-5959c60da91f1e62080a4455e2a4627cb1fd3d8c70210bf7f38cb222dd15d88f.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM draft_move dest\n WHERE dest.email = $1\n AND EXISTS (SELECT 1 FROM draft_move src\n WHERE src.email = $2\n AND src.workspace_id = dest.workspace_id\n AND src.old_path = dest.old_path\n AND src.typ = dest.typ)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "5959c60da91f1e62080a4455e2a4627cb1fd3d8c70210bf7f38cb222dd15d88f" +} diff --git a/backend/.sqlx/query-a6decdebcd9750691f20f874b66a9c6f2ede08c33605c6a3c3dfc213a3eda76a.json b/backend/.sqlx/query-5ccfbd0f345b9b86ca356008def6cc1011a49fc8d8ad046ac572e81cf8938995.json similarity index 63% rename from backend/.sqlx/query-a6decdebcd9750691f20f874b66a9c6f2ede08c33605c6a3c3dfc213a3eda76a.json rename to backend/.sqlx/query-5ccfbd0f345b9b86ca356008def6cc1011a49fc8d8ad046ac572e81cf8938995.json index 0532a5d3a0..7cd917d623 100644 --- a/backend/.sqlx/query-a6decdebcd9750691f20f874b66a9c6f2ede08c33605c6a3c3dfc213a3eda76a.json +++ b/backend/.sqlx/query-5ccfbd0f345b9b86ca356008def6cc1011a49fc8d8ad046ac572e81cf8938995.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE workspace_settings\n SET\n ai_config = source_ws.ai_config,\n large_file_storage = source_ws.large_file_storage,\n ducklake = source_ws.ducklake,\n dbt_warehouses = source_ws.dbt_warehouses,\n datatable = source_ws.datatable,\n git_app_installations = source_ws.git_app_installations\n FROM workspace_settings source_ws\n WHERE source_ws.workspace_id = $1\n AND workspace_settings.workspace_id = $2\n ", + "query": "\n UPDATE workspace_settings\n SET\n ai_config = source_ws.ai_config,\n large_file_storage = source_ws.large_file_storage,\n ducklake = source_ws.ducklake,\n dbt_warehouses = source_ws.dbt_warehouses,\n datatable = source_ws.datatable,\n git_app_installations = source_ws.git_app_installations,\n add_admins_and_developers_to_forks = source_ws.add_admins_and_developers_to_forks\n FROM workspace_settings source_ws\n WHERE source_ws.workspace_id = $1\n AND workspace_settings.workspace_id = $2\n ", "describe": { "columns": [], "parameters": { @@ -11,5 +11,5 @@ }, "nullable": [] }, - "hash": "a6decdebcd9750691f20f874b66a9c6f2ede08c33605c6a3c3dfc213a3eda76a" + "hash": "5ccfbd0f345b9b86ca356008def6cc1011a49fc8d8ad046ac572e81cf8938995" } diff --git a/backend/.sqlx/query-94fd0a57cfc9341b2e9deae60506c6c06aa6934b87200da14231f12f65149cd3.json b/backend/.sqlx/query-6c57c46c5a0462f379ed6a22ae97e363009177fdc45d795447a920f455f193cc.json similarity index 77% rename from backend/.sqlx/query-94fd0a57cfc9341b2e9deae60506c6c06aa6934b87200da14231f12f65149cd3.json rename to backend/.sqlx/query-6c57c46c5a0462f379ed6a22ae97e363009177fdc45d795447a920f455f193cc.json index d54260b4f7..b8a19247ca 100644 --- a/backend/.sqlx/query-94fd0a57cfc9341b2e9deae60506c6c06aa6934b87200da14231f12f65149cd3.json +++ b/backend/.sqlx/query-6c57c46c5a0462f379ed6a22ae97e363009177fdc45d795447a920f455f193cc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "DELETE FROM token_expiry_notification n\n USING token t\n WHERE n.token_hash = t.token_hash\n AND n.expiration > now()\n AND n.expiration <= now() + interval '7 days'\n RETURNING t.token_prefix, t.label, t.email, t.workspace_id", + "query": "DELETE FROM token_expiry_notification n\n USING token t\n WHERE n.token_hash = t.token_hash\n AND n.expiration > now()\n AND n.expiration <= now() + make_interval(days => $1)\n RETURNING t.token_prefix, t.label, t.email, t.workspace_id", "describe": { "columns": [ { @@ -25,7 +25,9 @@ } ], "parameters": { - "Left": [] + "Left": [ + "Int4" + ] }, "nullable": [ false, @@ -34,5 +36,5 @@ true ] }, - "hash": "94fd0a57cfc9341b2e9deae60506c6c06aa6934b87200da14231f12f65149cd3" + "hash": "6c57c46c5a0462f379ed6a22ae97e363009177fdc45d795447a920f455f193cc" } diff --git a/backend/.sqlx/query-75e6b5cd52d63ac094c90abd1524bc7a919692a96b076b8aaf24d812723531c8.json b/backend/.sqlx/query-75e6b5cd52d63ac094c90abd1524bc7a919692a96b076b8aaf24d812723531c8.json new file mode 100644 index 0000000000..27c62583f1 --- /dev/null +++ b/backend/.sqlx/query-75e6b5cd52d63ac094c90abd1524bc7a919692a96b076b8aaf24d812723531c8.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM usr WHERE email = $1 AND is_service_account IS true\n AND ($2::varchar IS NULL OR workspace_id = $2))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Varchar" + ] + }, + "nullable": [ + null + ] + }, + "hash": "75e6b5cd52d63ac094c90abd1524bc7a919692a96b076b8aaf24d812723531c8" +} diff --git a/backend/.sqlx/query-7761959d05fff8c314d04c1bc9fb4e84a61008dbec0995ae192f1b3a1c261579.json b/backend/.sqlx/query-7761959d05fff8c314d04c1bc9fb4e84a61008dbec0995ae192f1b3a1c261579.json new file mode 100644 index 0000000000..c8b20a20ea --- /dev/null +++ b/backend/.sqlx/query-7761959d05fff8c314d04c1bc9fb4e84a61008dbec0995ae192f1b3a1c261579.json @@ -0,0 +1,72 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO draft (workspace_id, email, path, typ, value, created_at, base)\n VALUES ($1, $2, $3::text, $4,\n CASE WHEN $10::bool\n THEN to_json($5::text::jsonb || jsonb_strip_nulls(jsonb_build_object(\n 'path', CASE WHEN $5::text::jsonb -> 'path' = to_jsonb($11::text)\n THEN to_jsonb($3::text) END,\n 'draft_path', CASE WHEN $5::text::jsonb -> 'draft_path' = to_jsonb($11::text)\n THEN to_jsonb($3::text) END)))\n ELSE $5::text::json\n END,\n COALESCE($8::timestamptz, now()), $9)\n ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL\n DO UPDATE SET value = CASE\n WHEN NOT $10::bool\n OR position(chr(92) || 'u0000' in replace(draft.value::text, chr(92) || chr(92), '')) > 0\n THEN EXCLUDED.value\n ELSE to_json((to_jsonb(EXCLUDED.value) - 'path' - 'draft_path')\n || jsonb_strip_nulls(jsonb_build_object(\n 'path', to_jsonb(draft.value) -> 'path',\n 'draft_path', to_jsonb(draft.value) -> 'draft_path')))\n END,\n created_at = EXCLUDED.created_at,\n base = EXCLUDED.base\n WHERE $7::bool = true\n OR $6::timestamptz IS NULL\n OR draft.created_at <= $6::timestamptz\n RETURNING path, created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Text", + { + "Custom": { + "name": "draft_kind", + "kind": { + "Enum": [ + "script", + "flow", + "app", + "raw_app", + "resource", + "variable", + "trigger_schedule", + "trigger_webhook", + "trigger_default_email", + "trigger_email", + "trigger_http", + "trigger_websocket", + "trigger_postgres", + "trigger_kafka", + "trigger_nats", + "trigger_mqtt", + "trigger_sqs", + "trigger_gcp", + "trigger_azure", + "trigger_poll", + "trigger_cli", + "trigger_nextcloud", + "trigger_google", + "trigger_github", + "data_pipeline", + "trigger_amqp" + ] + } + } + }, + "Text", + "Timestamptz", + "Bool", + "Timestamptz", + "Text", + "Bool", + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "7761959d05fff8c314d04c1bc9fb4e84a61008dbec0995ae192f1b3a1c261579" +} diff --git a/backend/.sqlx/query-797896e0152046844dbfee9d0e57e0055d742e44119bfb31ae7ff37a89b34d14.json b/backend/.sqlx/query-797896e0152046844dbfee9d0e57e0055d742e44119bfb31ae7ff37a89b34d14.json new file mode 100644 index 0000000000..ce3d615eb0 --- /dev/null +++ b/backend/.sqlx/query-797896e0152046844dbfee9d0e57e0055d742e44119bfb31ae7ff37a89b34d14.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM draft_move\n WHERE workspace_id = $1 AND typ::text = ANY($2::text[])\n AND old_path IN ($3, $4) AND ($5::text IS NULL OR email = $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Varchar", + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "797896e0152046844dbfee9d0e57e0055d742e44119bfb31ae7ff37a89b34d14" +} diff --git a/backend/.sqlx/query-88a134e4ca82d5ce0334977c7713021ae3e99a5a61ea1c944c1df1368746dfa5.json b/backend/.sqlx/query-7b33adb5cf051bc123340982b97a80a96ee43e1c91d9907d50cc2ee14111623e.json similarity index 78% rename from backend/.sqlx/query-88a134e4ca82d5ce0334977c7713021ae3e99a5a61ea1c944c1df1368746dfa5.json rename to backend/.sqlx/query-7b33adb5cf051bc123340982b97a80a96ee43e1c91d9907d50cc2ee14111623e.json index 7cfe9aa070..541832d45f 100644 --- a/backend/.sqlx/query-88a134e4ca82d5ce0334977c7713021ae3e99a5a61ea1c944c1df1368746dfa5.json +++ b/backend/.sqlx/query-7b33adb5cf051bc123340982b97a80a96ee43e1c91d9907d50cc2ee14111623e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n workspace.is_dev_workspace, workspace.dev_workspace_label,\n workspace.owner AS \"created_by?\",\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,\n usr.disabled\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false", + "query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n workspace.is_dev_workspace, workspace.dev_workspace_label,\n workspace.owner AS \"created_by?\",\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,\n usr.disabled, usr.is_service_account\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false", "describe": { "columns": [ { @@ -52,6 +52,11 @@ "ordinal": 9, "name": "disabled", "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "is_service_account", + "type_info": "Bool" } ], "parameters": { @@ -69,8 +74,9 @@ true, false, null, + false, false ] }, - "hash": "88a134e4ca82d5ce0334977c7713021ae3e99a5a61ea1c944c1df1368746dfa5" + "hash": "7b33adb5cf051bc123340982b97a80a96ee43e1c91d9907d50cc2ee14111623e" } diff --git a/backend/.sqlx/query-7ff60d155510303e307c47dcb6153ccaac72871eb68822d8e296c0d747e55f35.json b/backend/.sqlx/query-7ff60d155510303e307c47dcb6153ccaac72871eb68822d8e296c0d747e55f35.json new file mode 100644 index 0000000000..6a004023a0 --- /dev/null +++ b/backend/.sqlx/query-7ff60d155510303e307c47dcb6153ccaac72871eb68822d8e296c0d747e55f35.json @@ -0,0 +1,43 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT flow_version.id, flow_version.created_at, flow_version.created_by, deployment_metadata.deployment_msg FROM flow_version\n LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version\n WHERE flow_version.path = $1 AND flow_version.workspace_id = $2\n ORDER BY flow_version.created_at DESC\n LIMIT $3 OFFSET $4", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "created_by", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "deployment_msg", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "7ff60d155510303e307c47dcb6153ccaac72871eb68822d8e296c0d747e55f35" +} diff --git a/backend/.sqlx/query-87afd5289e54b6accda8a399ec3c66df3d19217059f62903be3c74eadb9c15d5.json b/backend/.sqlx/query-87afd5289e54b6accda8a399ec3c66df3d19217059f62903be3c74eadb9c15d5.json deleted file mode 100644 index 8cdb2d5907..0000000000 --- a/backend/.sqlx/query-87afd5289e54b6accda8a399ec3c66df3d19217059f62903be3c74eadb9c15d5.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO draft (workspace_id, path, typ, value, created_at, email)\n SELECT $2, path, typ,\n CASE WHEN typ IN ('script', 'flow')\n THEN to_json(to_jsonb(value) - 'on_behalf_of')\n ELSE value END,\n created_at, email\n FROM draft\n WHERE workspace_id = $1 AND (email = $3 OR email IS NULL)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Varchar", - "Text" - ] - }, - "nullable": [] - }, - "hash": "87afd5289e54b6accda8a399ec3c66df3d19217059f62903be3c74eadb9c15d5" -} diff --git a/backend/.sqlx/query-dc4a57df3becc610f631ef22c116450390addbfae85fecc61c991d94167e6e99.json b/backend/.sqlx/query-8ebe054b41793f1a7b85f1f8d29cd21d12fa1207b58bfc46249c250cdcdb5363.json similarity index 93% rename from backend/.sqlx/query-dc4a57df3becc610f631ef22c116450390addbfae85fecc61c991d94167e6e99.json rename to backend/.sqlx/query-8ebe054b41793f1a7b85f1f8d29cd21d12fa1207b58bfc46249c250cdcdb5363.json index 01c0fd19af..7a822a8d6a 100644 --- a/backend/.sqlx/query-dc4a57df3becc610f631ef22c116450390addbfae85fecc61c991d94167e6e99.json +++ b/backend/.sqlx/query-8ebe054b41793f1a7b85f1f8d29cd21d12fa1207b58bfc46249c250cdcdb5363.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n customer_id,\n plan,\n webhook,\n ai_config,\n dbt_warehouses,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_invite,\n error_handler,\n success_handler,\n public_app_execution_limit_per_minute,\n error_handler_fallback_to_instance_alerts,\n guest_access_enabled,\n guest_jwt_public_key,\n guest_jwt_jwks_url\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ", + "query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n customer_id,\n plan,\n webhook,\n ai_config,\n dbt_warehouses,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_invite,\n error_handler,\n success_handler,\n public_app_execution_limit_per_minute,\n error_handler_fallback_to_instance_alerts,\n guest_access_enabled,\n guest_jwt_public_key,\n guest_jwt_jwks_url,\n add_admins_and_developers_to_forks\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ", "describe": { "columns": [ { @@ -177,6 +177,11 @@ "ordinal": 34, "name": "guest_jwt_jwks_url", "type_info": "Text" + }, + { + "ordinal": 35, + "name": "add_admins_and_developers_to_forks", + "type_info": "Bool" } ], "parameters": { @@ -219,8 +224,9 @@ false, false, true, - true + true, + false ] }, - "hash": "dc4a57df3becc610f631ef22c116450390addbfae85fecc61c991d94167e6e99" + "hash": "8ebe054b41793f1a7b85f1f8d29cd21d12fa1207b58bfc46249c250cdcdb5363" } diff --git a/backend/.sqlx/query-9723edfd6de38cf0bf21123a46d88fa7d0b650b5cfd80c099f785f5e411062e5.json b/backend/.sqlx/query-9723edfd6de38cf0bf21123a46d88fa7d0b650b5cfd80c099f785f5e411062e5.json new file mode 100644 index 0000000000..4704ddd84e --- /dev/null +++ b/backend/.sqlx/query-9723edfd6de38cf0bf21123a46d88fa7d0b650b5cfd80c099f785f5e411062e5.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM draft_move WHERE workspace_id = $1 AND typ::text = ANY($2::text[])\n AND (old_path = $3 OR (new_path = $3 AND $4::text IS NULL))", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "9723edfd6de38cf0bf21123a46d88fa7d0b650b5cfd80c099f785f5e411062e5" +} diff --git a/backend/.sqlx/query-9bd1995747f0073b3a866d1238e78f7e6dfa9185056f7731547ff05b8176b271.json b/backend/.sqlx/query-9bd1995747f0073b3a866d1238e78f7e6dfa9185056f7731547ff05b8176b271.json new file mode 100644 index 0000000000..9c4d587c01 --- /dev/null +++ b/backend/.sqlx/query-9bd1995747f0073b3a866d1238e78f7e6dfa9185056f7731547ff05b8176b271.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_settings SET add_admins_and_developers_to_forks = $1 WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "9bd1995747f0073b3a866d1238e78f7e6dfa9185056f7731547ff05b8176b271" +} diff --git a/backend/.sqlx/query-9d1b202844e6935e636d1570a92d46af4419dda9410ffc46f19bc38e56efe609.json b/backend/.sqlx/query-9d1b202844e6935e636d1570a92d46af4419dda9410ffc46f19bc38e56efe609.json new file mode 100644 index 0000000000..f882ce2986 --- /dev/null +++ b/backend/.sqlx/query-9d1b202844e6935e636d1570a92d46af4419dda9410ffc46f19bc38e56efe609.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM draft_move\n WHERE workspace_id = $1 AND typ::text = ANY($2::text[])\n AND new_path = $4 AND old_path <> $3 AND ($5::text IS NULL OR email = $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "TextArray", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "9d1b202844e6935e636d1570a92d46af4419dda9410ffc46f19bc38e56efe609" +} diff --git a/backend/.sqlx/query-a0f1c0df6bc2f1fbca50edee90e42c94445536e201b322eda6f7a90bdf38f36a.json b/backend/.sqlx/query-a0f1c0df6bc2f1fbca50edee90e42c94445536e201b322eda6f7a90bdf38f36a.json deleted file mode 100644 index 3ed4f5a316..0000000000 --- a/backend/.sqlx/query-a0f1c0df6bc2f1fbca50edee90e42c94445536e201b322eda6f7a90bdf38f36a.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version \n LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version\n WHERE flow_version.path = $1 AND flow_version.workspace_id = $2 \n ORDER BY flow_version.created_at DESC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 2, - "name": "deployment_msg", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - true - ] - }, - "hash": "a0f1c0df6bc2f1fbca50edee90e42c94445536e201b322eda6f7a90bdf38f36a" -} diff --git a/backend/.sqlx/query-a1bcf143135ecb9f32709a56a353d3246dc238920c3e155caa9cf767246fa6fb.json b/backend/.sqlx/query-a1bcf143135ecb9f32709a56a353d3246dc238920c3e155caa9cf767246fa6fb.json new file mode 100644 index 0000000000..18ee22083e --- /dev/null +++ b/backend/.sqlx/query-a1bcf143135ecb9f32709a56a353d3246dc238920c3e155caa9cf767246fa6fb.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_settings SET auto_invite = (COALESCE(auto_invite, '{}'::jsonb) - 'domain') || $1::jsonb WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Jsonb", + "Text" + ] + }, + "nullable": [] + }, + "hash": "a1bcf143135ecb9f32709a56a353d3246dc238920c3e155caa9cf767246fa6fb" +} diff --git a/backend/.sqlx/query-a1ed94ef0a6d4f7bc89ac4a2341bc06dc95d60ec92469523935f5a18a4d3f907.json b/backend/.sqlx/query-a1ed94ef0a6d4f7bc89ac4a2341bc06dc95d60ec92469523935f5a18a4d3f907.json new file mode 100644 index 0000000000..c536425f1d --- /dev/null +++ b/backend/.sqlx/query-a1ed94ef0a6d4f7bc89ac4a2341bc06dc95d60ec92469523935f5a18a4d3f907.json @@ -0,0 +1,85 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n -- The guard's own predicate, ordered own row, then legacy, then another\n -- user's other-kind row: each needs a different remedy, and a bare\n -- LIMIT 1 would name an arbitrary one.\n (SELECT typ::text FROM draft WHERE workspace_id = $1 AND path = $3\n AND typ::text = ANY($6::text[])\n AND (typ <> $2 OR email = $4 OR email IS NULL)\n ORDER BY CASE WHEN email = $4 THEN 0 WHEN email IS NULL THEN 1 ELSE 2 END\n LIMIT 1) as \"at_target\",\n EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $3\n AND typ::text = ANY($6::text[]) AND email = $4) as \"at_target_own!\",\n EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $3\n AND typ::text = ANY($6::text[]) AND email IS NULL) as \"at_target_legacy!\",\n EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $5\n AND typ = $2 AND email = $4\n AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) > 0\n ) as \"poisoned!\",\n EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $5\n AND typ = $2 AND email IS NULL) as \"legacy!\" ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "at_target", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "at_target_own!", + "type_info": "Bool" + }, + { + "ordinal": 2, + "name": "at_target_legacy!", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "poisoned!", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "legacy!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "draft_kind", + "kind": { + "Enum": [ + "script", + "flow", + "app", + "raw_app", + "resource", + "variable", + "trigger_schedule", + "trigger_webhook", + "trigger_default_email", + "trigger_email", + "trigger_http", + "trigger_websocket", + "trigger_postgres", + "trigger_kafka", + "trigger_nats", + "trigger_mqtt", + "trigger_sqs", + "trigger_gcp", + "trigger_azure", + "trigger_poll", + "trigger_cli", + "trigger_nextcloud", + "trigger_google", + "trigger_github", + "data_pipeline", + "trigger_amqp" + ] + } + } + }, + "Text", + "Text", + "Text", + "TextArray" + ] + }, + "nullable": [ + null, + null, + null, + null, + null + ] + }, + "hash": "a1ed94ef0a6d4f7bc89ac4a2341bc06dc95d60ec92469523935f5a18a4d3f907" +} diff --git a/backend/.sqlx/query-a725d5b99b65a1637406179b058a1ff36620099c6b87fd82a59eae1e35303c07.json b/backend/.sqlx/query-a725d5b99b65a1637406179b058a1ff36620099c6b87fd82a59eae1e35303c07.json new file mode 100644 index 0000000000..2acc85fcd4 --- /dev/null +++ b/backend/.sqlx/query-a725d5b99b65a1637406179b058a1ff36620099c6b87fd82a59eae1e35303c07.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE draft_move SET workspace_id = $1 WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "a725d5b99b65a1637406179b058a1ff36620099c6b87fd82a59eae1e35303c07" +} diff --git a/backend/.sqlx/query-b98844926ff127c528ed3e7bc63bf1ebed0192be267983e8fbd18f79997e6142.json b/backend/.sqlx/query-b98844926ff127c528ed3e7bc63bf1ebed0192be267983e8fbd18f79997e6142.json deleted file mode 100644 index 94cf77ebc5..0000000000 --- a/backend/.sqlx/query-b98844926ff127c528ed3e7bc63bf1ebed0192be267983e8fbd18f79997e6142.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO usr (workspace_id, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via)\n SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via\n FROM usr WHERE workspace_id = $2\n ON CONFLICT DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Text" - ] - }, - "nullable": [] - }, - "hash": "b98844926ff127c528ed3e7bc63bf1ebed0192be267983e8fbd18f79997e6142" -} diff --git a/backend/.sqlx/query-bd8ac169ad15346f28f561233fe340efd453512c1464a413af2393ab8bfea9ee.json b/backend/.sqlx/query-bd8ac169ad15346f28f561233fe340efd453512c1464a413af2393ab8bfea9ee.json new file mode 100644 index 0000000000..8a59831e8a --- /dev/null +++ b/backend/.sqlx/query-bd8ac169ad15346f28f561233fe340efd453512c1464a413af2393ab8bfea9ee.json @@ -0,0 +1,47 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg,\n av.created_by as created_by, av.created_at as created_at\n FROM app a JOIN app_version av\n ON av.id = a.versions[array_upper(a.versions, 1)] AND av.app_id = a.id\n LEFT JOIN deployment_metadata dm ON av.id = dm.app_version\n WHERE a.workspace_id = $1 AND a.path = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "app_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "deployment_msg", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created_by", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "bd8ac169ad15346f28f561233fe340efd453512c1464a413af2393ab8bfea9ee" +} diff --git a/backend/.sqlx/query-e428f0cdb9077bffe2b7271d9b5f30ed81e8c26a9f1e65f423078947153ff9dd.json b/backend/.sqlx/query-e428f0cdb9077bffe2b7271d9b5f30ed81e8c26a9f1e65f423078947153ff9dd.json new file mode 100644 index 0000000000..57b5e2abd1 --- /dev/null +++ b/backend/.sqlx/query-e428f0cdb9077bffe2b7271d9b5f30ed81e8c26a9f1e65f423078947153ff9dd.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE draft_move SET email = $1 WHERE email = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "e428f0cdb9077bffe2b7271d9b5f30ed81e8c26a9f1e65f423078947153ff9dd" +} diff --git a/backend/.sqlx/query-e6140682ebc65083f48714ab3299fdb239ef507e23003db03aff7d35b02c540b.json b/backend/.sqlx/query-e6140682ebc65083f48714ab3299fdb239ef507e23003db03aff7d35b02c540b.json new file mode 100644 index 0000000000..f218653d46 --- /dev/null +++ b/backend/.sqlx/query-e6140682ebc65083f48714ab3299fdb239ef507e23003db03aff7d35b02c540b.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO draft (workspace_id, path, typ, value, created_at, email, base)\n SELECT $2, path, typ,\n to_json(\n CASE WHEN typ IN ('script', 'flow') THEN clean - 'on_behalf_of' ELSE clean END\n - CASE WHEN typ = 'flow' THEN 'version_id'\n WHEN typ IN ('app', 'raw_app') THEN 'parent_version'\n ELSE '' END\n ),\n created_at, email,\n CASE WHEN typ = 'script' THEN base END\n FROM (\n SELECT d.path, d.typ, d.created_at, d.email, d.base,\n replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean\n FROM draft d\n WHERE d.workspace_id = $1 AND (d.email = $3 OR d.email IS NULL)\n ) s", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "e6140682ebc65083f48714ab3299fdb239ef507e23003db03aff7d35b02c540b" +} diff --git a/backend/.sqlx/query-e63da92247045f2aaaa62b9d6ee17510fd05d0b7c5311b61e7257ccd07b16da2.json b/backend/.sqlx/query-e63da92247045f2aaaa62b9d6ee17510fd05d0b7c5311b61e7257ccd07b16da2.json new file mode 100644 index 0000000000..926add8159 --- /dev/null +++ b/backend/.sqlx/query-e63da92247045f2aaaa62b9d6ee17510fd05d0b7c5311b61e7257ccd07b16da2.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT m.new_path FROM draft_move m\n WHERE m.workspace_id = $1 AND m.typ = $2 AND m.old_path = $3\n AND (m.email IS NULL OR m.email = $4)\n AND NOT EXISTS (\n SELECT 1 FROM draft d\n WHERE d.workspace_id = $1 AND d.typ = $2 AND d.path = $3\n AND d.email IS NOT DISTINCT FROM $4\n )\n ORDER BY m.email IS NULL\n LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "new_path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "draft_kind", + "kind": { + "Enum": [ + "script", + "flow", + "app", + "raw_app", + "resource", + "variable", + "trigger_schedule", + "trigger_webhook", + "trigger_default_email", + "trigger_email", + "trigger_http", + "trigger_websocket", + "trigger_postgres", + "trigger_kafka", + "trigger_nats", + "trigger_mqtt", + "trigger_sqs", + "trigger_gcp", + "trigger_azure", + "trigger_poll", + "trigger_cli", + "trigger_nextcloud", + "trigger_google", + "trigger_github", + "data_pipeline", + "trigger_amqp" + ] + } + } + }, + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e63da92247045f2aaaa62b9d6ee17510fd05d0b7c5311b61e7257ccd07b16da2" +} diff --git a/backend/.sqlx/query-ede15bff96152f209aff756830cbc76b5afa1af6ed324376989117b1054c3447.json b/backend/.sqlx/query-e6e31fdf705896c81f9a0f45d47c1b93db0406aaf278bccba07be00e9f937e2a.json similarity index 78% rename from backend/.sqlx/query-ede15bff96152f209aff756830cbc76b5afa1af6ed324376989117b1054c3447.json rename to backend/.sqlx/query-e6e31fdf705896c81f9a0f45d47c1b93db0406aaf278bccba07be00e9f937e2a.json index 1c247ad5b5..7462224850 100644 --- a/backend/.sqlx/query-ede15bff96152f209aff756830cbc76b5afa1af6ed324376989117b1054c3447.json +++ b/backend/.sqlx/query-e6e31fdf705896c81f9a0f45d47c1b93db0406aaf278bccba07be00e9f937e2a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n workspace_id,\n slack_team_id,\n slack_name,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n mute_critical_alerts,\n guest_access_enabled,\n deploy_ui,\n large_file_storage,\n datatable\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ", + "query": "\n SELECT\n workspace_id,\n slack_team_id,\n slack_name,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n mute_critical_alerts,\n guest_access_enabled,\n add_admins_and_developers_to_forks,\n deploy_ui,\n large_file_storage,\n datatable\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ", "describe": { "columns": [ { @@ -45,16 +45,21 @@ }, { "ordinal": 8, + "name": "add_admins_and_developers_to_forks", + "type_info": "Bool" + }, + { + "ordinal": 9, "name": "deploy_ui", "type_info": "Jsonb" }, { - "ordinal": 9, + "ordinal": 10, "name": "large_file_storage", "type_info": "Jsonb" }, { - "ordinal": 10, + "ordinal": 11, "name": "datatable", "type_info": "Jsonb" } @@ -73,10 +78,11 @@ true, true, false, + false, true, true, true ] }, - "hash": "ede15bff96152f209aff756830cbc76b5afa1af6ed324376989117b1054c3447" + "hash": "e6e31fdf705896c81f9a0f45d47c1b93db0406aaf278bccba07be00e9f937e2a" } diff --git a/backend/.sqlx/query-eefa0588a6a927fd9b3f65e1df652fb2b4cf7983049d2c490940df360c7e2b30.json b/backend/.sqlx/query-eefa0588a6a927fd9b3f65e1df652fb2b4cf7983049d2c490940df360c7e2b30.json new file mode 100644 index 0000000000..ec641bf32d --- /dev/null +++ b/backend/.sqlx/query-eefa0588a6a927fd9b3f65e1df652fb2b4cf7983049d2c490940df360c7e2b30.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "eefa0588a6a927fd9b3f65e1df652fb2b4cf7983049d2c490940df360c7e2b30" +} diff --git a/backend/.sqlx/query-ef98b0fab16d3b19997fd639b74a895cce1e8ae010307431e6787d20c7b25780.json b/backend/.sqlx/query-ef98b0fab16d3b19997fd639b74a895cce1e8ae010307431e6787d20c7b25780.json new file mode 100644 index 0000000000..3b89a91024 --- /dev/null +++ b/backend/.sqlx/query-ef98b0fab16d3b19997fd639b74a895cce1e8ae010307431e6787d20c7b25780.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT flow_version.id, flow_version.created_at, flow_version.created_by, deployment_metadata.deployment_msg FROM flow_version \n LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version\n WHERE flow_version.path = $1 AND flow_version.workspace_id = $2 \n ORDER BY flow_version.created_at DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "created_by", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "deployment_msg", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "ef98b0fab16d3b19997fd639b74a895cce1e8ae010307431e6787d20c7b25780" +} diff --git a/backend/.sqlx/query-fc2081d9077c743bc3978ae1905db558549b2356fc6ac8f0557aa40e1345ff46.json b/backend/.sqlx/query-fc2081d9077c743bc3978ae1905db558549b2356fc6ac8f0557aa40e1345ff46.json new file mode 100644 index 0000000000..27cb37b876 --- /dev/null +++ b/backend/.sqlx/query-fc2081d9077c743bc3978ae1905db558549b2356fc6ac8f0557aa40e1345ff46.json @@ -0,0 +1,31 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.email IS NULL as \"legacy!\", COALESCE(u.username, p.username) as username\n FROM draft d\n LEFT JOIN usr u ON u.workspace_id = d.workspace_id AND u.email = d.email\n LEFT JOIN password p ON p.email = d.email AND p.super_admin = true\n WHERE d.workspace_id = $1 AND d.path = $2 AND d.typ::text = ANY($3::text[])\n -- A row a move already brought here from the very path being renamed is this\n -- rename's own draft, not an item in its way: the record says which owners\n -- it moved (all of them for an item move, one for a draft-only move). Any\n -- other row is a second item and still collides.\n AND NOT EXISTS (\n SELECT 1 FROM draft_move m\n WHERE m.workspace_id = $1 AND m.typ::text = ANY($3::text[])\n AND m.old_path = $4 AND m.new_path = $2\n AND (m.email IS NULL OR m.email IS NOT DISTINCT FROM d.email)\n )\n ORDER BY 2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "legacy!", + "type_info": "Bool" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "TextArray", + "Text" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "fc2081d9077c743bc3978ae1905db558549b2356fc6ac8f0557aa40e1345ff46" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 658274e26b..d653e6cd5e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2311,9 +2311,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" dependencies = [ "find-msvc-tools", "jobserver", @@ -5055,9 +5055,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" [[package]] name = "fixedbitset" @@ -5497,9 +5497,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +checksum = "54ade96dc9003043bce7c035c85a9df5a858bfb2039c5a2e6fdf00f324f6c551" dependencies = [ "cc", "cfg-if", @@ -14793,7 +14793,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-nats", @@ -14881,7 +14881,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.814.0" +version = "1.815.0" dependencies = [ "async-stream", "async-trait", @@ -14915,7 +14915,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14928,7 +14928,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "argon2", @@ -15068,7 +15068,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15091,7 +15091,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15108,7 +15108,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15134,7 +15134,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.814.0" +version = "1.815.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -15144,7 +15144,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15161,7 +15161,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -15183,7 +15183,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15206,7 +15206,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15222,7 +15222,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15244,7 +15244,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15266,7 +15266,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15280,7 +15280,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-nats", @@ -15315,7 +15315,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15340,7 +15340,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15368,7 +15368,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15390,7 +15390,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15410,7 +15410,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15448,7 +15448,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15477,7 +15477,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.814.0" +version = "1.815.0" dependencies = [ "lazy_static", "serde", @@ -15489,7 +15489,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.814.0" +version = "1.815.0" dependencies = [ "argon2", "axum 0.8.9", @@ -15513,7 +15513,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15527,7 +15527,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.814.0" +version = "1.815.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15562,7 +15562,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.814.0" +version = "1.815.0" dependencies = [ "chrono", "lazy_static", @@ -15576,7 +15576,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15595,7 +15595,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.814.0" +version = "1.815.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -15703,7 +15703,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.814.0" +version = "1.815.0" dependencies = [ "chrono", "futures", @@ -15723,7 +15723,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.814.0" +version = "1.815.0" dependencies = [ "regex", "serde", @@ -15740,7 +15740,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -15767,7 +15767,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "futures", @@ -15784,7 +15784,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.814.0" +version = "1.815.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15800,7 +15800,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -15821,7 +15821,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -15852,7 +15852,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "arc-swap", @@ -15877,7 +15877,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-stream", @@ -15912,7 +15912,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "futures", @@ -15930,7 +15930,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.814.0" +version = "1.815.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -15939,7 +15939,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -15951,7 +15951,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde_json", @@ -15963,7 +15963,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "gosyn", @@ -15975,7 +15975,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -15987,7 +15987,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde_json", @@ -15999,7 +15999,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "nu-parser", @@ -16010,7 +16010,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16021,7 +16021,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -16033,7 +16033,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "rustpython-ast", @@ -16044,7 +16044,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-recursion", @@ -16066,7 +16066,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde_json", @@ -16078,7 +16078,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -16092,7 +16092,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -16109,7 +16109,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -16122,7 +16122,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde", @@ -16134,7 +16134,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -16152,7 +16152,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -16168,7 +16168,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "rustpython-ast", @@ -16184,7 +16184,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -16198,7 +16198,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-recursion", @@ -16237,7 +16237,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "const_format", @@ -16277,7 +16277,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.814.0" +version = "1.815.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -16288,7 +16288,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-recursion", @@ -16323,7 +16323,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16347,7 +16347,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16380,7 +16380,7 @@ dependencies = [ [[package]] name = "windmill-trigger-amqp" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16407,7 +16407,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16440,7 +16440,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16460,7 +16460,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16494,7 +16494,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16530,7 +16530,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16553,7 +16553,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16577,7 +16577,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-nats", @@ -16601,7 +16601,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16636,7 +16636,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16664,7 +16664,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-trait", @@ -16689,7 +16689,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -16708,7 +16708,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-once-cell", @@ -16826,7 +16826,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.814.0" +version = "1.815.0" dependencies = [ "bytes", "futures", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 4d10383023..1ce51d9f51 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.814.0" +version = "1.815.0" authors.workspace = true edition.workspace = true @@ -88,7 +88,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.814.0" +version = "1.815.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index ea30a789b5..d121d59203 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -23d12f73e44a24bb91fa54d79dfc4ae1436e0227 +32d1cec18daca49868d6a70a31c0bd3053f7980d diff --git a/backend/migrations/20260914133558_draft_base_version.down.sql b/backend/migrations/20260914133558_draft_base_version.down.sql new file mode 100644 index 0000000000..07fc93ba30 --- /dev/null +++ b/backend/migrations/20260914133558_draft_base_version.down.sql @@ -0,0 +1 @@ +ALTER TABLE draft DROP COLUMN base; diff --git a/backend/migrations/20260914133558_draft_base_version.up.sql b/backend/migrations/20260914133558_draft_base_version.up.sql new file mode 100644 index 0000000000..835816d70c --- /dev/null +++ b/backend/migrations/20260914133558_draft_base_version.up.sql @@ -0,0 +1,14 @@ +-- The version a draft forked from, as one opaque text id whatever the kind: a +-- script hash (hex), a flow_version.id, an app_version.id. NULL for a draft that +-- was never forked from a deploy and for kinds that keep no lineage. +ALTER TABLE draft ADD COLUMN base TEXT; + +-- A U+0000 inside a `json` value makes `->>` raise 22P05; such rows keep NULL and +-- get their base on their next save. +UPDATE draft SET base = CASE typ::text + WHEN 'script' THEN value ->> 'parent_hash' + WHEN 'flow' THEN value ->> 'version_id' + ELSE value ->> 'parent_version' + END +WHERE typ::text IN ('script', 'flow', 'app', 'raw_app') + AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) = 0; diff --git a/backend/migrations/20260914154128_draft_move.down.sql b/backend/migrations/20260914154128_draft_move.down.sql new file mode 100644 index 0000000000..6fff84e4bb --- /dev/null +++ b/backend/migrations/20260914154128_draft_move.down.sql @@ -0,0 +1 @@ +DROP TABLE draft_move; diff --git a/backend/migrations/20260914154128_draft_move.up.sql b/backend/migrations/20260914154128_draft_move.up.sql new file mode 100644 index 0000000000..e68e36c5ff --- /dev/null +++ b/backend/migrations/20260914154128_draft_move.up.sql @@ -0,0 +1,17 @@ +-- Where an item's drafts went when it moved, so a draft save still addressed to the +-- old path (an editor left open across the move, a chat, the CLI) lands on the moved +-- draft instead of starting a new item there. `email` NULL records a deployed item's +-- move and applies to every user; set, it records that user's draft-only move. +CREATE TABLE draft_move ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, + typ DRAFT_KIND NOT NULL, + old_path VARCHAR(255) NOT NULL, + new_path VARCHAR(255) NOT NULL, + email VARCHAR(255) +); + +CREATE INDEX draft_move_old_path_idx ON draft_move (workspace_id, typ, old_path); + +GRANT ALL ON draft_move TO windmill_user; +GRANT ALL ON draft_move TO windmill_admin; diff --git a/backend/migrations/20260918092041_add_admins_and_developers_to_forks.down.sql b/backend/migrations/20260918092041_add_admins_and_developers_to_forks.down.sql new file mode 100644 index 0000000000..4e8ac47c69 --- /dev/null +++ b/backend/migrations/20260918092041_add_admins_and_developers_to_forks.down.sql @@ -0,0 +1 @@ +ALTER TABLE workspace_settings DROP COLUMN add_admins_and_developers_to_forks; diff --git a/backend/migrations/20260918092041_add_admins_and_developers_to_forks.up.sql b/backend/migrations/20260918092041_add_admins_and_developers_to_forks.up.sql new file mode 100644 index 0000000000..bb3502d362 --- /dev/null +++ b/backend/migrations/20260918092041_add_admins_and_developers_to_forks.up.sql @@ -0,0 +1 @@ +ALTER TABLE workspace_settings ADD COLUMN add_admins_and_developers_to_forks BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 335b3f79cf..fea3a11063 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.814.0" +version = "1.815.0" dependencies = [ "aho-corasick", "anyhow", @@ -6274,7 +6274,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.814.0" +version = "1.815.0" dependencies = [ "proc-macro2", "quote", @@ -6286,7 +6286,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.814.0" +version = "1.815.0" dependencies = [ "convert_case", "serde", @@ -6295,7 +6295,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -6307,7 +6307,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde_json", @@ -6319,7 +6319,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "gosyn", @@ -6331,7 +6331,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -6343,7 +6343,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde_json", @@ -6355,7 +6355,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "nu-parser", @@ -6366,7 +6366,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6377,7 +6377,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6389,7 +6389,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6400,7 +6400,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "async-recursion", @@ -6422,7 +6422,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde_json", @@ -6434,7 +6434,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -6448,7 +6448,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "convert_case", @@ -6465,7 +6465,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -6478,7 +6478,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde", @@ -6490,7 +6490,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -6508,7 +6508,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6524,7 +6524,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6540,7 +6540,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6572,7 +6572,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "lazy_static", @@ -6586,7 +6586,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.814.0" +version = "1.815.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 1262be6a3b..2f700b7b01 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" members = ["."] [workspace.package] -version = "1.814.0" +version = "1.815.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/src/main.rs b/backend/src/main.rs index 316ed099f2..9e7a93cfcd 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -52,7 +52,8 @@ use windmill_common::{ INSTANCE_EVENTS_WEBHOOK_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING, MAVEN_SETTINGS_XML_SETTING, - MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING, + MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, + NO_DEFAULT_MAVEN_SETTING, NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING, @@ -126,7 +127,8 @@ use windmill_worker::{ use crate::monitor::{ initial_load, load_concurrency_key_max_queued, load_disable_password_login, - load_fork_workspace_tag_append_fork_suffix, load_keep_job_dir, load_metrics_debug_enabled, + load_fork_workspace_tag_append_fork_suffix, load_keep_job_dir, + load_mcp_disable_token_query_param, load_metrics_debug_enabled, load_preview_tags_override, load_require_preexisting_user, load_retention_period_overrides, load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, load_workspace_fairness_duration_secs, load_workspace_fairness_enabled, @@ -2164,6 +2166,9 @@ async fn process_notify_event( DISABLE_PASSWORD_LOGIN_SETTING => { load_disable_password_login(db).await; } + MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING => { + load_mcp_disable_token_query_param(db).await; + } EXPOSE_METRICS_SETTING => { tracing::info!("Metrics setting changed, restarting"); spawn_graceful_killpill(tx, db, 30, "metrics setting change", server_mode) diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index e250302209..750bc40560 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -45,7 +45,10 @@ use windmill_common::otel_oss::{ use windmill_common::{ agent_workers::DECODED_AGENT_TOKEN, apps::APP_WORKSPACED_ROUTE, - auth::{create_token_for_owner, ephemeral_script_token_label, job_token_expiry_secs}, + auth::{ + create_token_for_owner, ephemeral_script_token_label, job_token_expiry_secs, + TOKEN_EXPIRY_WARNING_DAYS, + }, ee_oss::CriticalErrorChannel, email_oss::send_email_if_possible, error, @@ -62,6 +65,7 @@ use windmill_common::{ FORK_WORKSPACE_TAG_APPEND_FORK_SUFFIX_SETTING, HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, + MCP_DISABLE_TOKEN_QUERY_PARAM, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, NSJAIL_TMPFS_SIZE_MB_SETTING, NSJAIL_TMP_BACKING_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING, OTEL_TRACES_RETENTION_SECS_SETTING, OTEL_TRACING_PROXY_SETTING, @@ -285,6 +289,15 @@ pub async fn initial_load( ); if let Some(db) = conn.as_sql() { + // Outside the `server_mode` block below: a `MODE=mcp` process serves the MCP routes + // with `server_mode` false and would otherwise never read this at all. That mode + // joins no monitor loop, so there — as for every global setting, `base_url` + // included — this pass is the only read, and a change lands on restart. + pass.setting( + MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, + false, + |v| async move { apply_mcp_disable_token_query_param(v) }, + ); pass.setting(DEFAULT_TAGS_PER_WORKSPACE_SETTING, false, |v| async move { apply_tag_per_workspace_enabled(v) }); @@ -1614,6 +1627,23 @@ pub fn apply_disable_password_login(value: Option) { }; } +pub async fn load_mcp_disable_token_query_param(db: &DB) { + match load_value_from_global_settings(db, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING).await { + Ok(v) => apply_mcp_disable_token_query_param(v), + Err(e) => tracing::error!("Error loading mcp_disable_token_query_param setting: {e:#}"), + }; +} + +pub fn apply_mcp_disable_token_query_param(value: Option) { + match value { + Some(serde_json::Value::Bool(t)) => { + MCP_DISABLE_TOKEN_QUERY_PARAM.store(t, Ordering::Relaxed) + } + None => MCP_DISABLE_TOKEN_QUERY_PARAM.store(false, Ordering::Relaxed), + _ => (), + }; +} + struct LogFile { file_path: String, hostname: String, @@ -2198,7 +2228,7 @@ async fn cleanup_scheduled_job_deletions(db: &Pool) { } pub async fn check_expiring_tokens(db: &DB) { - // Find tokens expiring within 7 days that still have a pending notification row. + // Find tokens expiring within the warning window that still have a pending notification row. // The notification table stores token_hash (not plaintext) so the join works // even after the hash migration makes token.token nullable. let expiring_tokens_r = sqlx::query_as!( @@ -2207,8 +2237,9 @@ pub async fn check_expiring_tokens(db: &DB) { USING token t WHERE n.token_hash = t.token_hash AND n.expiration > now() - AND n.expiration <= now() + interval '7 days' + AND n.expiration <= now() + make_interval(days => $1) RETURNING t.token_prefix, t.label, t.email, t.workspace_id", + TOKEN_EXPIRY_WARNING_DAYS, ) .fetch_all(db) .await; diff --git a/backend/summarized_schema.txt b/backend/summarized_schema.txt index e27cb2893c..47f9faca68 100644 --- a/backend/summarized_schema.txt +++ b/backend/summarized_schema.txt @@ -234,7 +234,7 @@ workspace_protection_rule: workspace_id(char), name(char), rules(int), bypass_gr FK: (workspace_id) -> workspace(id) workspace_runnable_dependencies: flow_path(char), runnable_path(char), script_hash(bigint), runnable_is_flow(bool), workspace_id(char), app_path(char), id(bigint), runnable_is_agent(bool) FK: (app_path, workspace_id) -> app(path, workspace_id) | (flow_path, workspace_id) -> flow(path, workspace_id) -workspace_settings: workspace_id(char), slack_team_id(char), slack_name(char), slack_command_script(char), slack_email(char), customer_id(char), plan(char), webhook(text), ai_config(jsonb), large_file_storage(jsonb), git_sync(jsonb), default_app(char), default_scripts(jsonb), deploy_ui(jsonb), mute_critical_alerts(bool), color(char), operator_settings(jsonb), teams_command_script(text), teams_team_id(text), teams_team_name(text), git_app_installations(jsonb), ducklake(jsonb), slack_oauth_client_id(char), slack_oauth_client_secret(char), datatable(jsonb), teams_team_guid(text), auto_invite(jsonb), error_handler(jsonb), success_handler(jsonb), public_app_execution_limit_per_minute(int), dbt_warehouses(jsonb), guest_access_enabled(bool), guest_jwt_public_key(text), guest_jwt_jwks_url(text), ai_sessions_backup_generation(int) +workspace_settings: workspace_id(char), slack_team_id(char), slack_name(char), slack_command_script(char), slack_email(char), customer_id(char), plan(char), webhook(text), ai_config(jsonb), large_file_storage(jsonb), git_sync(jsonb), default_app(char), default_scripts(jsonb), deploy_ui(jsonb), mute_critical_alerts(bool), color(char), operator_settings(jsonb), teams_command_script(text), teams_team_id(text), teams_team_name(text), git_app_installations(jsonb), ducklake(jsonb), slack_oauth_client_id(char), slack_oauth_client_secret(char), datatable(jsonb), teams_team_guid(text), auto_invite(jsonb), error_handler(jsonb), success_handler(jsonb), public_app_execution_limit_per_minute(int), dbt_warehouses(jsonb), guest_access_enabled(bool), guest_jwt_public_key(text), guest_jwt_jwks_url(text), ai_sessions_backup_generation(int), add_admins_and_developers_to_forks(bool) FK: (workspace_id) -> workspace(id) zombie_job_counter: job_id(uuid), counter(int) FK: (job_id) -> v2_job(id) diff --git a/backend/tests/app_version_order.rs b/backend/tests/app_version_order.rs new file mode 100644 index 0000000000..e3557ad55c --- /dev/null +++ b/backend/tests/app_version_order.rs @@ -0,0 +1,193 @@ +//! The deployed order of an app's versions is the order they were appended to +//! `app.versions`, not the order of their `created_at`. +//! +//! `app_version.created_at` defaults to `now()`, which in Postgres is the +//! transaction's start time, while the append happens under the app row's lock. +//! Two deploys that overlap therefore land in one order and carry timestamps in +//! the other. The head the editor guards against, and the sequence the diff +//! picker numbers, both have to follow the array. +//! +//! Users from the `base` fixture: test-user (admin, token SECRET_TOKEN). + +use serde_json::json; +use sqlx::{Pool, Postgres}; +use windmill_test_utils::*; + +const TOKEN: &str = "SECRET_TOKEN"; + +#[sqlx::test(fixtures("base"))] +async fn test_app_head_follows_the_append_order_not_the_timestamps( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let ws = format!( + "http://localhost:{}/api/w/test-workspace", + server.addr.port() + ); + let client = reqwest::Client::new(); + + let res = client + .post(format!("{ws}/apps/create")) + .header("Authorization", format!("Bearer {TOKEN}")) + .json(&json!({ + "path": "u/test-user/order_app", + "summary": "ordered", + "value": {}, + "policy": { "execution_mode": "publisher", "triggerables": {} } + })) + .send() + .await?; + assert!(res.status().is_success(), "{}", res.text().await?); + + let first: i64 = sqlx::query_scalar( + "SELECT versions[array_upper(versions, 1)] FROM app + WHERE workspace_id = 'test-workspace' AND path = 'u/test-user/order_app'", + ) + .fetch_one(&db) + .await?; + + // The overlapping deploy: appended after `first`, so it is the version that + // landed, but stamped before it, so a timestamp sort puts it underneath. + let second: i64 = sqlx::query_scalar( + "INSERT INTO app_version (app_id, value, created_by, created_at, raw_app) + SELECT app_id, value, 'racer', created_at - interval '1 hour', raw_app + FROM app_version WHERE id = $1 + RETURNING id", + ) + .bind(first) + .fetch_one(&db) + .await?; + sqlx::query( + "UPDATE app SET versions = array_append(versions, $1::bigint) + WHERE workspace_id = 'test-workspace' AND path = 'u/test-user/order_app'", + ) + .bind(second) + .execute(&db) + .await?; + + let head: serde_json::Value = client + .get(format!( + "{ws}/apps/get_latest_version/u/test-user/order_app" + )) + .header("Authorization", format!("Bearer {TOKEN}")) + .send() + .await? + .json() + .await?; + assert_eq!( + head["version"], second, + "the head is the version appended last, not the newest timestamp: {head}" + ); + + let history: Vec = client + .get(format!("{ws}/apps/history/p/u/test-user/order_app")) + .header("Authorization", format!("Bearer {TOKEN}")) + .send() + .await? + .json() + .await?; + let listed: Vec = history + .iter() + .map(|v| v["version"].as_i64().unwrap()) + .collect(); + assert_eq!( + listed, + vec![second, first], + "the history lists in deployed order, so it leads with the head" + ); + + // Enough versions that a page cannot hold them, so "asked for nothing" and "asked for + // a page" are visibly different answers. + let mut appended = vec![second, first]; + for _ in 0..24 { + let extra: i64 = sqlx::query_scalar( + "INSERT INTO app_version (app_id, value, created_by, created_at, raw_app) + SELECT app_id, value, 'bulk', created_at, raw_app FROM app_version WHERE id = $1 + RETURNING id", + ) + .bind(first) + .fetch_one(&db) + .await?; + sqlx::query( + "UPDATE app SET versions = array_append(versions, $1::bigint) + WHERE workspace_id = 'test-workspace' AND path = 'u/test-user/order_app'", + ) + .bind(extra) + .execute(&db) + .await?; + appended.insert(0, extra); + } + + let versions_at = |query: &str| { + let url = format!("{ws}/apps/history/p/u/test-user/order_app{query}"); + let client = client.clone(); + async move { + let rows: Vec = client + .get(url) + .header("Authorization", format!("Bearer {TOKEN}")) + .send() + .await? + .json() + .await?; + Ok::<_, anyhow::Error>( + rows.iter() + .map(|v| v["version"].as_i64().unwrap()) + .collect::>(), + ) + } + }; + + // The deployment-history panel and the CLI read this endpoint without paging, so + // asking for no page has to keep answering with the whole history. + assert_eq!( + versions_at("").await?, + appended, + "an unpaginated request still answers whole" + ); + assert_eq!( + versions_at("?per_page=10").await?, + appended[..10], + "a page holds what was asked for, newest first" + ); + assert_eq!( + versions_at("?per_page=10&page=2").await?, + appended[10..20], + "the next page carries on where the first left off, skipping nothing" + ); + // A page past the end runs off it rather than overflowing into one. (The clamp on an + // asked-for size is pinned where it lives, in `paginate_optional`'s own test.) + assert!( + versions_at("?per_page=10&page=99999999") + .await? + .is_empty(), + "a page past the end is empty" + ); + // Naming only the page still asks for a page, rather than an offset into a listing + // sized to hold everything — which would answer nothing at all. + assert_eq!( + versions_at("?page=1").await?, + appended, + "a first page with no size named holds the history" + ); + + // A version that never entered the deployed sequence — one a restore or a fork copy + // could leave behind — still has to be reachable, after the ones that did. + let stray: i64 = sqlx::query_scalar( + "INSERT INTO app_version (app_id, value, created_by, created_at, raw_app) + SELECT app_id, value, 'stray', created_at, raw_app FROM app_version WHERE id = $1 + RETURNING id", + ) + .bind(first) + .fetch_one(&db) + .await?; + let mut with_stray = appended.clone(); + with_stray.push(stray); + assert_eq!( + versions_at("").await?, + with_stray, + "a version outside app.versions is listed, and after the deployed sequence" + ); + + Ok(()) +} diff --git a/backend/tests/dependency_map.rs b/backend/tests/dependency_map.rs index 4520cae848..f4b74525fb 100644 --- a/backend/tests/dependency_map.rs +++ b/backend/tests/dependency_map.rs @@ -521,10 +521,8 @@ def main(): .await .unwrap(); - assert_eq!( - res.text().await.unwrap(), - "app f/rel/root_app updated (npath: \"f/rel/root_app_renamed\")" - ); + let deployed: serde_json::Value = res.json().await.unwrap(); + assert_eq!(deployed["path"], "f/rel/root_app_renamed", "{deployed}"); let mut completed = listen_for_completed_jobs(&db).await; in_test_worker(&db, completed.next(), port).await; diff --git a/backend/tests/drafts_move_mirror.rs b/backend/tests/drafts_move_mirror.rs new file mode 100644 index 0000000000..d619381c46 --- /dev/null +++ b/backend/tests/drafts_move_mirror.rs @@ -0,0 +1,74 @@ +//! Moving a draft must carry both of its path keys. +//! +//! A draft value holds a typed path and a mirror the editors keep beside it while +//! it differs from the row's path (`path`/`draft_path`; which is which depends on +//! the kind). The loaders prefer the mirror, so a move that rewrote only the typed +//! key left the mirror naming the old location: reopening the item restored the +//! old path, and the next autosave wrote it back — undoing the move silently. +//! +//! This pins `move_draft`, including that a draft with no mirror never gains one. +//! A deploy's carry follows the keys only where they still name the old path +//! (`drafts_save_follows_move` pins that). + +use serde_json::Value; +use sqlx::{Pool, Postgres}; + +use windmill_test_utils::*; + +async fn move_to(port: u16, from: &str, to: &str) -> anyhow::Result { + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/script/{from}" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&serde_json::json!({ "new_path": to })) + .send() + .await?; + // Asserted here so a rejected move fails as itself, rather than as a JSON + // parse error in the read-back below. + let status = resp.status(); + let body = resp.text().await?; + assert!(status.is_success(), "move {from} -> {to} failed: {body}"); + Ok(body) +} + +/// The stored draft value at `path`, read back through the API so this test needs +/// no `sqlx::query!` (which would want an offline cache entry of its own). +async fn value_at(port: u16, path: &str) -> anyhow::Result { + let body: Value = reqwest::Client::new() + .get(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/get_own/script/{path}" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .send() + .await? + .json() + .await?; + Ok(body + .get("value") + .cloned() + .unwrap_or_else(|| panic!("no draft at {path}: {body}"))) +} + +#[sqlx::test(fixtures("base", "drafts_move_mirror"))] +async fn test_move_carries_both_path_keys(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + move_to(port, "u/test-user/draft_mirror", "u/test-user/renamed").await?; + move_to(port, "u/test-user/draft_plain", "u/test-user/plain2").await?; + + // The mirror follows: left at `u/test-user/friendly` it would win at load and + // walk the item back there. + let moved = value_at(port, "u/test-user/renamed").await?; + assert_eq!(moved["path"], "u/test-user/renamed"); + assert_eq!(moved["draft_path"], "u/test-user/renamed"); + + // A draft that never had a mirror must not be given one. + let plain = value_at(port, "u/test-user/plain2").await?; + assert_eq!(plain["path"], "u/test-user/plain2"); + assert_eq!(plain.get("draft_path"), None, "mirror injected: {plain}"); + + Ok(()) +} diff --git a/backend/tests/drafts_move_taken.rs b/backend/tests/drafts_move_taken.rs new file mode 100644 index 0000000000..40318ca2ef --- /dev/null +++ b/backend/tests/drafts_move_taken.rs @@ -0,0 +1,250 @@ +//! A rename onto a path that already holds a draft is refused. +//! +//! Nothing deployed can sit at a rename's destination (the deploy conflicts on +//! that), but a draft can: a never-deployed item, or a draft left on an archived +//! script. Moving onto it would merge two items or strand a row, so the rename +//! itself fails, in its own transaction, and the source stays deployed. The +//! destination draft here is the deployer's own, which is the same collision. + +use serde_json::{json, Value}; +use sqlx::{Pool, Postgres}; + +use windmill_test_utils::*; + +/// Hex form of script hash 7010, the way the API takes a parent hash. +const HEAD_HASH: &str = "0000000000001b62"; + +#[sqlx::test(fixtures("base", "drafts_move_taken"))] +async fn test_rename_onto_a_draft_is_refused(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let client = reqwest::Client::new(); + + let resp = client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/scripts/create" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "path": "u/test-user/mvtaken_b", + "parent_hash": HEAD_HASH, + "summary": "A", + "description": "", + "content": "export function main() { return 1 }", + "language": "deno", + "schema": {} + })) + .send() + .await?; + let status = resp.status(); + let body = resp.text().await?; + assert_eq!(status, 400, "rename onto a draft was not refused: {body}"); + assert!( + body.contains("already has a draft"), + "unexpected refusal: {body}" + ); + + // The whole deploy rolled back: the source is still the live head, and the + // draft at the destination is untouched. + let head: Value = client + .get(format!( + "http://localhost:{port}/api/w/test-workspace/scripts/get/p/u/test-user/mvtaken_a" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .send() + .await? + .json() + .await?; + assert_eq!(head["hash"], HEAD_HASH, "source was replaced: {head}"); + assert_eq!(head["archived"], false, "source was archived: {head}"); + + let draft: Value = client + .get(format!("http://localhost:{port}/api/w/test-workspace/drafts/get_own/script/u/test-user/mvtaken_b")) + .header("Authorization", "Bearer SECRET_TOKEN") + .send() + .await? + .json() + .await?; + assert_eq!( + draft["value"]["summary"], "B", + "destination draft changed: {draft}" + ); + + Ok(()) +} + +/// A legacy (ownerless) draft occupies its path too: a deploy there deletes it together +/// with the caller's own row, so a move that parks a second draft beside it would discard +/// edits the caller never saw. Only an admin can clear it, so the refusal says so. +#[sqlx::test(fixtures("base", "drafts_move_taken"))] +async fn test_draft_move_refuses_a_legacy_destination(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/script/u/test-user/mvtaken_b" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": "u/test-user/mvtaken_legacy" })) + .send() + .await?; + let status = resp.status(); + let body = resp.text().await?; + assert_eq!(status, 400, "move onto a legacy draft was allowed: {body}"); + assert!( + body.contains("legacy workspace draft") && body.contains("workspace admin"), + "the refusal did not point at the one remedy: {body}" + ); + + // Both rows stayed where they were: the caller's own, and the legacy one the list + // synthesizes under the caller's name. + let list: Vec = reqwest::Client::new() + .get(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/list" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .send() + .await? + .json() + .await?; + let mut at = list + .iter() + .filter_map(|d| Some((d["kind"].as_str()?, d["path"].as_str()?))) + .filter(|(_, p)| p.starts_with("u/test-user/mvtaken_b") || p.ends_with("mvtaken_legacy")) + .collect::>(); + at.sort(); + assert_eq!( + at, + vec![ + ("script", "u/test-user/mvtaken_b"), + ("script", "u/test-user/mvtaken_legacy") + ], + "{list:?}" + ); + Ok(()) +} + +/// A classic app and a raw app deploy into the same table, so a draft-only move onto +/// the other kind's draft must be refused: deploying either path afterwards deletes +/// the caller's drafts of both kinds, taking the loser's item with it. +#[sqlx::test(fixtures("base", "drafts_move_taken"))] +async fn test_draft_move_refuses_the_other_app_kind(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/raw_app/u/test-user/mvtaken_raw" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": "u/test-user/mvtaken_app" })) + .send() + .await?; + let status = resp.status(); + let body = resp.text().await?; + assert_eq!( + status, 400, + "move onto a classic app draft was allowed: {body}" + ); + assert!( + body.contains("already have a draft at 'u/test-user/mvtaken_app' (app)"), + "the refusal did not name the occupying kind: {body}" + ); + + // Both drafts are untouched. + let list: Vec = reqwest::Client::new() + .get(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/list" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .send() + .await? + .json() + .await?; + let mut at = list + .iter() + .filter(|d| matches!(d["kind"].as_str(), Some("app") | Some("raw_app"))) + .filter_map(|d| Some((d["kind"].as_str()?, d["path"].as_str()?))) + .collect::>(); + at.sort(); + assert_eq!( + at, + vec![ + ("app", "u/test-user/mvtaken_app"), + ("raw_app", "u/test-user/mvtaken_raw") + ], + "{list:?}" + ); + Ok(()) +} + +/// Teammates' drafts of one item share its path by design, so another user's row is no +/// obstacle — except across the app pair, where the two kinds are different items on one +/// deployed path: deploying either strands the other, and deleting the app takes both. +#[sqlx::test(fixtures("base", "drafts_move_taken"))] +async fn test_draft_move_refuses_another_users_other_app_kind( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let move_to = |kind: &'static str, from: &'static str, to: &'static str| async move { + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/{kind}/{from}" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": to })) + .send() + .await?; + Ok::<_, anyhow::Error>((resp.status(), resp.text().await?)) + }; + + let (status, body) = move_to( + "app", + "u/test-user/mvtaken_app", + "u/test-user/mvtaken_theirs", + ) + .await?; + assert_eq!( + status, 400, + "a classic app was moved onto another user's raw app: {body}" + ); + assert!( + body.contains("holds another user's raw app draft"), + "the refusal did not name the occupant: {body}" + ); + + // And the other direction, where the occupant reads as the classic kind. + let (status, body) = move_to( + "raw_app", + "u/test-user/mvtaken_raw", + "u/test-user/mvtaken_app_theirs", + ) + .await?; + assert_eq!( + status, 400, + "a raw app was moved onto another user's classic app: {body}" + ); + assert!( + body.contains("holds another user's app draft"), + "the refusal did not name the occupant: {body}" + ); + + // The same-kind case is the ordinary one: two users' drafts of one raw app. + let (status, body) = move_to( + "raw_app", + "u/test-user/mvtaken_raw", + "u/test-user/mvtaken_theirs", + ) + .await?; + assert!( + status.is_success(), + "a raw app was refused beside another user's raw-app draft: {body}" + ); + Ok(()) +} diff --git a/backend/tests/drafts_nul.rs b/backend/tests/drafts_nul.rs index d7314b8fb6..2a77fe772a 100644 --- a/backend/tests/drafts_nul.rs +++ b/backend/tests/drafts_nul.rs @@ -78,3 +78,46 @@ async fn test_draft_write_strips_nul(db: Pool) -> anyhow::Result<()> { Ok(()) } + +/// The lineage the server derives for `draft.base` comes from the same value: read before +/// the sanitizer, a NUL in it costs the draft its base, which then reads as up to date +/// whatever the head is. +#[sqlx::test(fixtures("drafts_nul"))] +async fn test_draft_base_is_derived_from_sanitized_value(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/dnul-ws"); + + let resp = authed(client().post(format!( + "{base}/drafts/update/script/u/dnul-admin/poisoned_base" + ))) + .json(&json!({ + "value": { + "summary": "s", + "path": "u/dnul-admin/poisoned_base", + "content": "x", + "parent_hash": "0000000000001b62\u{0}" + } + })) + .send() + .await?; + assert_eq!( + resp.status(), + 200, + "save refused: {}", + resp.text().await.unwrap_or_default() + ); + + let stored: Option = sqlx::query_scalar( + "SELECT base FROM draft WHERE workspace_id = 'dnul-ws' AND path = 'u/dnul-admin/poisoned_base'", + ) + .fetch_one(&db) + .await?; + assert_eq!( + stored.as_deref(), + Some("0000000000001b62"), + "the NUL cost the draft its base: {stored:?}" + ); + Ok(()) +} diff --git a/backend/tests/drafts_save_follows_move.rs b/backend/tests/drafts_save_follows_move.rs new file mode 100644 index 0000000000..afbec56c75 --- /dev/null +++ b/backend/tests/drafts_save_follows_move.rs @@ -0,0 +1,587 @@ +//! A draft save addressed to a path its item moved away from lands on the moved draft. +//! +//! A move carries every draft on the item to the new path and records where they +//! went. An editor left open across it still saves to the old path; the server puts +//! the save on the moved draft, keeps the path keys the move gave it, and names the +//! new path so the editor can follow. Without the record the save would plant a +//! phantom draft-only item at the old location. + +use serde_json::{json, Value}; +use sqlx::{Pool, Postgres}; + +use windmill_test_utils::*; + +/// Hex form of script hash 7030, the way the API takes a parent hash. +const HEAD_HASH: &str = "0000000000001b76"; + +async fn own_draft_value(port: u16, path: &str) -> anyhow::Result { + let draft: Value = reqwest::Client::new() + .get(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/get_own/script/{path}" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .send() + .await? + .json() + .await?; + Ok(draft["value"].clone()) +} + +async fn own_draft_paths(port: u16) -> anyhow::Result> { + let list: Vec = reqwest::Client::new() + .get(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/list" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .send() + .await? + .json() + .await?; + Ok(list + .iter() + .filter(|d| d["kind"] == "script") + .filter_map(|d| d["path"].as_str().map(String::from)) + .filter(|p| p.starts_with("u/test-user/follow_")) + .collect()) +} + +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_save_follows_a_rename(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let client = reqwest::Client::new(); + + // Rename from Home: a redeploy of the deployed content at the new path that + // keeps the deployer's own draft, so the draft is carried rather than consumed. + let resp = client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/scripts/create" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "path": "u/test-user/follow_b", + "parent_hash": HEAD_HASH, + "summary": "A", + "description": "", + "content": "export function main() { return 1 }", + "language": "deno", + "schema": {}, + "skip_draft_deletion": true + })) + .send() + .await?; + assert_eq!(resp.status(), 201, "rename failed: {}", resp.text().await?); + assert_eq!(own_draft_paths(port).await?, vec!["u/test-user/follow_b"]); + // A script draft's `path` is where deploying it lands, so it moves with the row. + assert_eq!( + own_draft_value(port, "u/test-user/follow_b").await?["path"], + "u/test-user/follow_b" + ); + + // The editor is still on the old path and writes that path back into the value. + let saved: Value = client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/update/script/u/test-user/follow_a" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "value": { + "path": "u/test-user/follow_a", + "parent_hash": HEAD_HASH, + "summary": "A", + "content": "edited after the move" + } + })) + .send() + .await? + .json() + .await?; + assert_eq!(saved["status"], "saved", "save refused: {saved}"); + assert_eq!( + saved["path"], "u/test-user/follow_b", + "save did not follow the row: {saved}" + ); + + // The write landed on the carried row; nothing reappeared at the old path. + assert_eq!(own_draft_paths(port).await?, vec!["u/test-user/follow_b"]); + let draft = own_draft_value(port, "u/test-user/follow_b").await?; + assert_eq!(draft["content"], "edited after the move", "{draft}"); + assert_eq!(draft["path"], "u/test-user/follow_b", "{draft}"); + + Ok(()) +} + +/// A draft-only move rewrites both path keys. The owner's open editor still carries +/// the typed path it had, which names neither the old nor the new path; the moved +/// draft's own keys have to win, or the save walks the item back. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_save_follows_a_draft_only_move(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let client = reqwest::Client::new(); + + let resp = client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/script/u/test-user/draft_store" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": "u/test-user/moved" })) + .send() + .await?; + assert!( + resp.status().is_success(), + "move failed: {}", + resp.text().await? + ); + + let saved: Value = client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/update/script/u/test-user/draft_store" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "value": { + "path": "u/test-user/friendly", + "draft_path": "u/test-user/friendly", + "summary": "D", + "content": "edited after the move" + } + })) + .send() + .await? + .json() + .await?; + assert_eq!(saved["path"], "u/test-user/moved", "{saved}"); + + let draft = own_draft_value(port, "u/test-user/moved").await?; + assert_eq!(draft["content"], "edited after the move", "{draft}"); + assert_eq!(draft["path"], "u/test-user/moved", "{draft}"); + assert_eq!(draft["draft_path"], "u/test-user/moved", "{draft}"); + Ok(()) +} + +/// Rename `from` to `to` the way Home does: redeploy the deployed content at the new +/// path, keeping the deployer's own draft so it is carried rather than consumed. +/// Returns the new head's hash. +async fn rename(port: u16, from_hash: &str, to: &str) -> anyhow::Result { + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/scripts/create" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "path": to, + "parent_hash": from_hash, + "summary": "A", + "description": "", + "content": "export function main() { return 1 }", + "language": "deno", + "schema": {}, + "skip_draft_deletion": true + })) + .send() + .await?; + let status = resp.status(); + let hash = resp.text().await?; + assert_eq!(status, 201, "rename to {to} failed: {hash}"); + Ok(hash) +} + +/// Save the draft as an editor still bound to `url_path` would. Returns the path the +/// save landed at. +async fn save_at(port: u16, url_path: &str, content: &str) -> anyhow::Result { + let saved: Value = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/update/script/{url_path}" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "value": { "path": url_path, "summary": "A", "content": content, "language": "deno" } + })) + .send() + .await? + .json() + .await?; + assert_eq!(saved["status"], "saved", "save refused: {saved}"); + Ok(saved["path"].as_str().unwrap_or_default().to_string()) +} + +/// A record is kept to one hop, and a move back to the path it left ends it: both are +/// three statements whose order decides the answer. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_move_records_stay_one_hop(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let b = rename(port, HEAD_HASH, "u/test-user/follow_b").await?; + let _c = rename(port, &b, "u/test-user/follow_c").await?; + assert_eq!( + save_at(port, "u/test-user/follow_a", "after two moves").await?, + "u/test-user/follow_c", + "a save at the first path did not reach the last" + ); + assert_eq!(own_draft_paths(port).await?, vec!["u/test-user/follow_c"]); + Ok(()) +} + +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_move_back_ends_the_record(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let b = rename(port, HEAD_HASH, "u/test-user/follow_b").await?; + let _a = rename(port, &b, "u/test-user/follow_a").await?; + assert_eq!( + save_at(port, "u/test-user/follow_a", "after moving back").await?, + "u/test-user/follow_a", + "a save was routed off the path the item moved back to" + ); + assert_eq!(own_draft_paths(port).await?, vec!["u/test-user/follow_a"]); + Ok(()) +} + +/// A rename carries a teammate's row too: both its path keys follow, and the version +/// it forked from does not move. A restamp there would clear their out-of-date prompt +/// and let them deploy over the mover's version believing they were current. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_a_teammates_draft_follows_with_its_base(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + rename(port, HEAD_HASH, "u/test-user/follow_b").await?; + + // Read from the pool: the teammate's row is another user's, and this asserts on + // `base`, which no endpoint exposes for someone else's draft. + let row: (String, String, Option) = sqlx::query_as( + "SELECT value::jsonb ->> 'path', value::jsonb ->> 'draft_path', base + FROM draft WHERE workspace_id = 'test-workspace' AND typ = 'script' + AND email = 'test2@windmill.dev'", + ) + .fetch_one(&db) + .await?; + assert_eq!(row.0, "u/test-user/follow_b", "typed path did not follow"); + assert_eq!(row.1, "u/test-user/follow_b", "mirror did not follow"); + assert_eq!( + row.2.as_deref(), + Some(HEAD_HASH), + "the teammate's base was restamped by someone else's rename" + ); + Ok(()) +} + +/// An item move and then the owner's own move of what is left: the two records have +/// different scopes, so the owner's move has to extend the chain in its own scope or +/// a save addressed to the first path stops at the abandoned middle one. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_an_owner_move_extends_an_item_move(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + rename(port, HEAD_HASH, "u/test-user/follow_b").await?; + // Archiving the script at the new path leaves the carried draft as a draft-only + // item, which its owner can move through `/drafts/move`. + sqlx::query("UPDATE script SET archived = true WHERE path = 'u/test-user/follow_b'") + .execute(&db) + .await?; + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/script/u/test-user/follow_b" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": "u/test-user/follow_c" })) + .send() + .await?; + assert!( + resp.status().is_success(), + "move failed: {}", + resp.text().await? + ); + + assert_eq!( + save_at(port, "u/test-user/follow_a", "after both moves").await?, + "u/test-user/follow_c", + "a save at the first path stopped at the path the owner's move left" + ); + assert_eq!(own_draft_paths(port).await?, vec!["u/test-user/follow_c"]); + Ok(()) +} + +/// Redeploying at a path an owner's move routed away from ends that route: the live item +/// owns its path again, and its saves must not follow the draft that left. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_redeploy_at_a_routed_path_ends_the_route(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + // `move_draft` ignores archived rows, so an archived script's draft can be moved away. + sqlx::query("UPDATE script SET archived = true WHERE path = 'u/test-user/follow_a'") + .execute(&db) + .await?; + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/script/u/test-user/follow_a" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": "u/test-user/follow_b" })) + .send() + .await?; + assert!( + resp.status().is_success(), + "move failed: {}", + resp.text().await? + ); + + // Unarchiving redeploys at the same path, with the archived version as parent. + rename(port, HEAD_HASH, "u/test-user/follow_a").await?; + + assert_eq!( + save_at(port, "u/test-user/follow_a", "for the live script").await?, + "u/test-user/follow_a", + "a save for the redeployed script followed the draft that moved away" + ); + Ok(()) +} + +/// A draft written before the NUL sanitizer still has to follow a move: its path keys are +/// what a deploy of it would land on, so the carry rewrites them, sanitizing the value it +/// could not otherwise parse. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_a_poisoned_draft_follows_a_rename(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + // The teammate's row, rewritten the way a pre-sanitizer client left one: a real NUL + // escape in the content, both path keys naming the path the item is about to leave. + sqlx::query( + r#"UPDATE draft SET value = '{"path": "u/test-user/follow_a", "draft_path": "u/test-user/follow_a", + "parent_hash": "0000000000001b76", "summary": "A", "content": "a\u0000b"}' + WHERE email = 'test2@windmill.dev'"#, + ) + .execute(&db) + .await?; + + rename(port, HEAD_HASH, "u/test-user/follow_b").await?; + + let row: (String, String, String) = sqlx::query_as( + "SELECT value::jsonb ->> 'path', value::jsonb ->> 'draft_path', value::jsonb ->> 'content' + FROM draft WHERE email = 'test2@windmill.dev'", + ) + .fetch_one(&db) + .await?; + assert_eq!(row.0, "u/test-user/follow_b", "typed path did not follow"); + assert_eq!(row.1, "u/test-user/follow_b", "mirror did not follow"); + assert_eq!(row.2, "ab", "the NUL survived the rewrite"); + Ok(()) +} + +/// A discard whose answer is lost is retried, and the retry finds nothing to delete. It +/// still has to name where the item went: the editor asking is on the path it left, and a +/// reload there lands on nothing. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_a_retried_discard_still_names_the_destination( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let client = reqwest::Client::new(); + + rename(port, HEAD_HASH, "u/test-user/follow_b").await?; + + let discard = |token: &'static str| { + let client = client.clone(); + async move { + client + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/update/script/u/test-user/follow_a" + )) + .header("Authorization", format!("Bearer {token}")) + .json(&json!({ "value": null })) + .send() + .await? + .json::() + .await + } + }; + + let first = discard("SECRET_TOKEN").await?; + assert_eq!(first["status"], "saved", "{first}"); + assert_eq!(first["path"], "u/test-user/follow_b", "{first}"); + + let retry = discard("SECRET_TOKEN").await?; + assert_eq!( + retry["status"], "saved", + "the retry was not idempotent: {retry}" + ); + assert_eq!(retry["path"], "u/test-user/follow_b", "{retry}"); + + // A third user has no draft on this item, so their discard deletes nothing and the + // destination is a move record and nothing else: it is answered only to a caller who + // can read that path. + let other = discard("SECRET_TOKEN_3").await?; + assert_eq!(other["status"], "saved", "{other}"); + assert_eq!( + other["path"], + Value::Null, + "a user who cannot read the destination was told where the item went: {other}" + ); + Ok(()) +} + +/// The legacy workspace-level row is carried by a rename like any other draft, and the +/// record that routes saves to it covers every caller — so discarding it from a page that +/// still names the old path has to reach it where it went. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_a_legacy_discard_follows_a_rename(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + sqlx::query( + r#"INSERT INTO draft (workspace_id, path, typ, value, email) + VALUES ('test-workspace', 'u/test-user/follow_a', 'script', + '{"path": "u/test-user/follow_a", "summary": "legacy", "content": "x"}', NULL)"#, + ) + .execute(&db) + .await?; + + rename(port, HEAD_HASH, "u/test-user/follow_b").await?; + + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/update/script/u/test-user/follow_a" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "value": null, "legacy": true })) + .send() + .await?; + assert!( + resp.status().is_success(), + "discard failed: {}", + resp.text().await? + ); + + let left: i64 = sqlx::query_scalar( + "SELECT count(*) FROM draft WHERE workspace_id = 'test-workspace' AND email IS NULL", + ) + .fetch_one(&db) + .await?; + assert_eq!( + left, 0, + "the legacy draft survived a discard aimed at its old path" + ); + Ok(()) +} + +/// Deploying a draft that a move carried off an archived script: its parent is still the +/// version at the old path, so the deploy renames from there and carries what is left over +/// — onto the very draft being deployed. That row is this deploy's own, not an item in its +/// way, or the deploy is refused and every retry refuses again. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_a_moved_draft_deploys_at_its_new_path(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + sqlx::query("UPDATE script SET archived = true WHERE path = 'u/test-user/follow_a'") + .execute(&db) + .await?; + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/script/u/test-user/follow_a" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": "u/test-user/follow_b" })) + .send() + .await?; + assert!( + resp.status().is_success(), + "move failed: {}", + resp.text().await? + ); + + // Deploy it where it now lives, still parented on the archived version it forked from. + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/scripts/create" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "path": "u/test-user/follow_b", + "parent_hash": HEAD_HASH, + "summary": "A", + "description": "", + "content": "export function main() { return 2 }", + "language": "deno", + "schema": {} + })) + .send() + .await?; + let status = resp.status(); + let body = resp.text().await?; + assert_eq!(status, 201, "the moved draft could not be deployed: {body}"); + Ok(()) +} + +/// A route is only as good as the item it points at: when an unrelated item claims the +/// destination, a save still addressed to the old path must stay where it is rather than +/// land on that item's draft. +#[sqlx::test(fixtures("base", "drafts_save_follows_move"))] +async fn test_a_reused_destination_ends_the_route(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + sqlx::query("UPDATE script SET archived = true WHERE path = 'u/test-user/follow_a'") + .execute(&db) + .await?; + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/drafts/move/script/u/test-user/follow_a" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "new_path": "u/test-user/follow_b" })) + .send() + .await?; + assert!( + resp.status().is_success(), + "move failed: {}", + resp.text().await? + ); + // Someone else's item takes the destination, and the moved draft goes with the + // deploy that consumes it. + sqlx::query("DELETE FROM draft WHERE workspace_id = 'test-workspace' AND path = 'u/test-user/follow_b'") + .execute(&db) + .await?; + let resp = reqwest::Client::new() + .post(format!( + "http://localhost:{port}/api/w/test-workspace/scripts/create" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ + "path": "u/test-user/follow_b", + "summary": "unrelated", + "description": "", + "content": "export function main() { return 3 }", + "language": "deno", + "schema": {} + })) + .send() + .await?; + assert_eq!(resp.status(), 201, "create failed: {}", resp.text().await?); + + assert_eq!( + save_at(port, "u/test-user/follow_a", "after the destination was reused").await?, + "u/test-user/follow_a", + "a save was routed onto the item that now owns the destination" + ); + Ok(()) +} diff --git a/backend/tests/fixtures/drafts_move_mirror.sql b/backend/tests/fixtures/drafts_move_mirror.sql new file mode 100644 index 0000000000..e96d985eae --- /dev/null +++ b/backend/tests/fixtures/drafts_move_mirror.sql @@ -0,0 +1,16 @@ +-- Fixture for the two-path-key move test. +-- +-- Two draft-only scripts owned by test-user, both parked at a generated storage +-- key the way a session-created draft is. One carries the `draft_path` mirror the +-- session editor writes while the typed path differs from that key; the other +-- carries no mirror at all, which is what a plain draft looks like. + +INSERT INTO draft (workspace_id, path, typ, value, email) VALUES + ('test-workspace', 'u/test-user/draft_mirror', 'script', + '{"path": "u/test-user/friendly", "draft_path": "u/test-user/friendly", + "content": "x", "language": "bun", "summary": "S"}', + 'test@windmill.dev'), + ('test-workspace', 'u/test-user/draft_plain', 'script', + '{"path": "u/test-user/draft_plain", + "content": "x", "language": "bun", "summary": "S"}', + 'test@windmill.dev'); diff --git a/backend/tests/fixtures/drafts_move_taken.sql b/backend/tests/fixtures/drafts_move_taken.sql new file mode 100644 index 0000000000..5fee11b942 --- /dev/null +++ b/backend/tests/fixtures/drafts_move_taken.sql @@ -0,0 +1,37 @@ +-- Fixture for refusing a rename onto a path a draft already occupies. +-- +-- A deployed script at `u/test-user/mvtaken_a` (hash 7010 = 0x1b62), and a +-- never-deployed draft of test-user's own at `u/test-user/mvtaken_b`, the path +-- the rename will target. Nothing deployed lives at the target, so only the +-- draft can refuse the move. + +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, + schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 7010, 'u/test-user/mvtaken_a', + 'export function main() { return 1 }', + 'deno', 'script', 'test-user', '{}', 'A', '', '', '{}'); + +INSERT INTO draft (workspace_id, path, typ, value, email) +VALUES ('test-workspace', 'u/test-user/mvtaken_b', 'script', + '{"path": "u/test-user/mvtaken_b", "summary": "B", "content": ""}', + 'test@windmill.dev'); + +-- An ownerless (pre per-user drafts) row at a third path: a deploy there deletes it +-- together with the caller's, so it occupies the path just as an own draft does. +INSERT INTO draft (workspace_id, path, typ, value, email) +VALUES ('test-workspace', 'u/test-user/mvtaken_legacy', 'script', + '{"path": "u/test-user/mvtaken_legacy", "summary": "legacy", "content": ""}', + NULL); + +-- A draft-only classic app and a draft-only raw app of the same owner. They share +-- the `app` table, so one occupies the other's path. The third is another user's raw +-- app: a different item at its own path, not a second copy of anyone's. +INSERT INTO draft (workspace_id, path, typ, value, email) VALUES + ('test-workspace', 'u/test-user/mvtaken_app', 'app', + '{"summary": "classic", "value": {}}', 'test@windmill.dev'), + ('test-workspace', 'u/test-user/mvtaken_raw', 'raw_app', + '{"summary": "raw", "files": {}}', 'test@windmill.dev'), + ('test-workspace', 'u/test-user/mvtaken_theirs', 'raw_app', + '{"summary": "theirs", "files": {}}', 'test2@windmill.dev'), + ('test-workspace', 'u/test-user/mvtaken_app_theirs', 'app', + '{"summary": "theirs", "value": {}}', 'test2@windmill.dev'); diff --git a/backend/tests/fixtures/drafts_save_follows_move.sql b/backend/tests/fixtures/drafts_save_follows_move.sql new file mode 100644 index 0000000000..16a41feb1a --- /dev/null +++ b/backend/tests/fixtures/drafts_save_follows_move.sql @@ -0,0 +1,29 @@ +-- Fixture for saving a draft to the path its item moved away from. +-- +-- A deployed script at `u/test-user/follow_a` (hash 7030 = 0x1b76) with the +-- deployer's own draft on it. The test renames the script and then saves the +-- draft from an editor still bound to the old path. + +INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, + schema, summary, description, lock, extra_perms) +VALUES ('test-workspace', 7030, 'u/test-user/follow_a', + 'export function main() { return 1 }', + 'deno', 'script', 'test-user', '{}', 'A', '', '', '{}'); + +INSERT INTO draft (workspace_id, path, typ, value, email, base) +VALUES ('test-workspace', 'u/test-user/follow_a', 'script', + '{"path": "u/test-user/follow_a", "parent_hash": "0000000000001b76", "summary": "A", "content": "draft"}', + 'test@windmill.dev', '0000000000001b76'); + +-- A draft-only script parked at a generated storage key, its typed path elsewhere. +INSERT INTO draft (workspace_id, path, typ, value, email) +VALUES ('test-workspace', 'u/test-user/draft_store', 'script', + '{"path": "u/test-user/friendly", "draft_path": "u/test-user/friendly", "summary": "D", "content": "draft"}', + 'test@windmill.dev'); + +-- A teammate's draft on the same deployed script, forked from the same head. The +-- rename must carry it too, without touching the version it forked from. +INSERT INTO draft (workspace_id, path, typ, value, email, base) +VALUES ('test-workspace', 'u/test-user/follow_a', 'script', + '{"path": "u/test-user/follow_a", "draft_path": "u/test-user/follow_a", "parent_hash": "0000000000001b76", "summary": "A", "content": "teammate draft"}', + 'test2@windmill.dev', '0000000000001b76'); diff --git a/backend/tests/flow_engine_parity.rs b/backend/tests/flow_engine_parity.rs index 78e45e404b..b6a943a23b 100644 --- a/backend/tests/flow_engine_parity.rs +++ b/backend/tests/flow_engine_parity.rs @@ -3312,3 +3312,60 @@ export function main(i: number) { Ok(()) } + +// A `$flow_expr[...]` step tag is resolved from the flow's state before the step is pushed, and +// one that cannot be resolved fails the step instead of queueing it on a tag no worker serves. +#[cfg(feature = "deno_core")] +#[sqlx::test(fixtures("base"))] +async fn test_flow_expr_step_tag(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + + let step = |id: &str, tag: Option<&str>| { + flow_module( + id, + FlowModuleValue::RawScript { + input_transforms: Default::default(), + language: ScriptLang::Deno, + content: "export function main() { return { lang: 'bun' } }".to_string(), + path: None, + lock: None, + tag: tag.map(str::to_string), + concurrency_settings: Default::default(), + is_trigger: None, + assets: None, + }, + ) + }; + let flow = FlowValue { + modules: vec![ + step("a", None), + step("b", Some("$flow_expr[results.a.lang]")), + step("c", Some("nobody-serves-$flow_expr[a.lang]")), + ], + same_worker: false, + ..Default::default() + }; + + let job = RunJob::from(JobPayload::RawFlow { value: flow, path: None, restarted_from: None }) + .run_until_complete(&db, false, server.addr.port()) + .await; + + let b_tag = sqlx::query_scalar::<_, String>( + "SELECT tag FROM v2_job WHERE parent_job = $1 AND flow_step_id = 'b'", + ) + .bind(job.id) + .fetch_one(&db) + .await?; + assert_eq!(b_tag, "bun"); + + assert!(!job.success); + let result = job.json_result().unwrap(); + let message = result["error"]["message"].as_str().unwrap_or_default(); + assert!( + message.contains("Could not resolve the step tag `nobody-serves-$flow_expr[a.lang]`"), + "got {result:?}" + ); + + Ok(()) +} diff --git a/backend/tests/instance_config.rs b/backend/tests/instance_config.rs index 882207ebfe..e2841032a4 100644 --- a/backend/tests/instance_config.rs +++ b/backend/tests/instance_config.rs @@ -1530,6 +1530,31 @@ async fn declarative_sync_rejects_an_unusable_instance_banner(db: Pool ); } +#[sqlx::test(fixtures("base"))] +async fn declarative_sync_rejects_a_malformed_max_token_expiration(db: Pool) { + clear_settings_and_configs(&db).await; + let before = count_global_settings(&db).await; + + let mut desired = BTreeMap::new(); + desired.insert( + "max_token_expiration_days".to_string(), + serde_json::json!("7.0"), + ); + + let err = windmill_common::instance_config::sync_global_settings_declarative( + &db, + &BTreeMap::new(), + &desired, + ) + .await + .expect_err("a ceiling the token routes cannot read must fail the sync"); + assert!( + err.to_string().contains("max_token_expiration_days"), + "the error should name the offending setting, got: {err}" + ); + assert_eq!(count_global_settings(&db).await, before); +} + #[sqlx::test(fixtures("base"))] async fn declarative_sync_rejects_an_unusable_default_allowed_origins(db: Pool) { // The declarative writers (the sync-config CLI, the operator's ConfigMap diff --git a/backend/tests/job_token_log_masking.rs b/backend/tests/job_token_log_masking.rs new file mode 100644 index 0000000000..5d7de27218 --- /dev/null +++ b/backend/tests/job_token_log_masking.rs @@ -0,0 +1,108 @@ +/* + * The job's own token (`$WM_TOKEN`) stays valid well past the job it was minted + * for, and job logs are persisted to `job_logs` and optionally to object storage, + * so a script that echoes the token would otherwise park a live credential in + * durable storage. `run_worker` registers the token with `sensitive_log_masks` + * for the job it pulled; this pins that the persisted log carries the masked form. + */ + +use sqlx::{Pool, Postgres}; +use windmill_common::{ + jobs::{JobPayload, RawCode}, + scripts::ScriptLang, +}; +use windmill_test_utils::*; + +/// Prefix of a serialized job token: `jwt_` plus the base64 of a JWT header. +/// The masked form keeps only `jwt` + the last three characters, so it never matches. +const RAW_TOKEN_PREFIX: &str = "jwt_ey"; + +#[sqlx::test(fixtures("base"))] +async fn test_job_token_masked_in_persisted_logs(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let job = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content: "echo \"running with --token $WM_TOKEN\"".to_string(), + path: None, + lock: None, + language: ScriptLang::Bash, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, + tag: None, + })) + .run_until_complete(&db, false, port) + .await; + assert!(job.success, "job should have succeeded"); + + let logs = + sqlx::query_scalar::<_, Option>("SELECT logs FROM job_logs WHERE job_id = $1") + .bind(job.id) + .fetch_one(&db) + .await? + .unwrap_or_default(); + + assert!( + !logs.contains(RAW_TOKEN_PREFIX), + "an unmasked job token reached the persisted logs: {logs}" + ); + assert!( + logs.contains("secret value was masked"), + "expected the masking notice in logs: {logs}" + ); + Ok(()) +} + +/// nativets runs V8 in-process and persists `console.log` output through its own +/// channel, so it is masked by a different mechanism than the bash case above and +/// needs its own guard. +#[cfg(feature = "deno_core")] +#[sqlx::test(fixtures("base"))] +async fn test_job_token_masked_in_nativets_logs(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let job = RunJob::from(JobPayload::Code(RawCode { + hash: None, + content: "export async function main() {\n console.log('running with --token ' + process.env.WM_TOKEN);\n return 'ok';\n}".to_string(), + path: None, + lock: None, + language: ScriptLang::Nativets, + cache_ttl: None, + cache_ignore_s3_path: None, + dedicated_worker: None, + concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() + .into(), + debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, + tag: None, + })) + .run_until_complete(&db, false, port) + .await; + assert!(job.success, "job should have succeeded"); + + let logs = + sqlx::query_scalar::<_, Option>("SELECT logs FROM job_logs WHERE job_id = $1") + .bind(job.id) + .fetch_one(&db) + .await? + .unwrap_or_default(); + + assert!( + !logs.contains(RAW_TOKEN_PREFIX), + "an unmasked job token reached the persisted logs: {logs}" + ); + assert!( + logs.contains("secret value was masked"), + "expected the masking notice in logs: {logs}" + ); + Ok(()) +} diff --git a/backend/windmill-api-auth/src/lib.rs b/backend/windmill-api-auth/src/lib.rs index 0e26c2a8b8..bb1586db68 100644 --- a/backend/windmill-api-auth/src/lib.rs +++ b/backend/windmill-api-auth/src/lib.rs @@ -1249,6 +1249,10 @@ pub async fn register_token_expiry_notification( if !windmill_common::auth::is_user_token(label) { return; } + let warning_days = windmill_common::auth::TOKEN_EXPIRY_WARNING_DAYS; + if expiration <= chrono::Utc::now() + chrono::Duration::days(warning_days.into()) { + return; + } if let Err(e) = sqlx::query!( "INSERT INTO token_expiry_notification (token_hash, expiration) VALUES ($1, $2) ON CONFLICT DO NOTHING", token_hash, diff --git a/backend/windmill-api-flows/src/flows.rs b/backend/windmill-api-flows/src/flows.rs index 5ac0e0588c..c7df69879e 100644 --- a/backend/windmill-api-flows/src/flows.rs +++ b/backend/windmill-api-flows/src/flows.rs @@ -51,7 +51,10 @@ use windmill_common::{ jobs::JobPayload, schedule::Schedule, triggers::MovedNativeTrigger, - utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath}, + utils::{ + http_get_from_hub, not_found_if_none, paginate, paginate_optional, Pagination, + RunnableKind, StripPath, + }, }; use windmill_dep_map::scoped_dependency_map::ScopedDependencyMap; use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; @@ -747,6 +750,14 @@ async fn create_flow( .execute(&mut *tx) .await?; } + windmill_common::user_drafts::clear_draft_moves_from( + &mut tx, + &w_id, + &[UserDraftItemKind::Flow], + &nf.path, + None, + ) + .await?; audit_log( &mut *tx, @@ -915,25 +926,36 @@ pub struct FlowVersion { pub created_at: chrono::DateTime, #[serde(skip_serializing_if = "Option::is_none")] pub deployment_msg: Option, + /// Who deployed this version — the diff's version picker names them so a reader + /// can tell their own deploys from a teammate's. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_by: Option, } async fn get_flow_history( authed: ApiAuthed, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, + Query(pagination): Query, ) -> JsonResult> { let path = path.to_path(); check_scopes(&authed, || format!("flows:read:{}", path))?; + // Unasked-for, this listing stays whole: the history panels, the restart picker and + // the CLI all read it without paging. The diff picker asks for a page. + let (per_page, offset) = paginate_optional(pagination); let mut tx = user_db.begin(&authed).await?; let flows = sqlx::query_as!( FlowVersion, - "SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version + "SELECT flow_version.id, flow_version.created_at, flow_version.created_by, deployment_metadata.deployment_msg FROM flow_version LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version - WHERE flow_version.path = $1 AND flow_version.workspace_id = $2 - ORDER BY flow_version.created_at DESC", + WHERE flow_version.path = $1 AND flow_version.workspace_id = $2 + ORDER BY flow_version.created_at DESC + LIMIT $3 OFFSET $4", path, - w_id + w_id, + per_page, + offset, ) .fetch_all(&mut *tx) .await?; @@ -953,7 +975,7 @@ async fn get_latest_version( let version = sqlx::query_as!( FlowVersion, - "SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version + "SELECT flow_version.id, flow_version.created_at, flow_version.created_by, deployment_metadata.deployment_msg FROM flow_version LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version WHERE flow_version.path = $1 AND flow_version.workspace_id = $2 ORDER BY flow_version.created_at DESC", @@ -1438,6 +1460,20 @@ async fn update_flow( .await?; } + if is_new_path { + // Everything left at the old path is a draft this deploy didn't consume + // — teammates' rows, and the deployer's own when the caller asked us to + // keep it. Carry them rather than strand them. + windmill_common::user_drafts::move_drafts_for_path( + &mut tx, + &w_id, + &[UserDraftItemKind::Flow], + flow_path, + &nf.path, + ) + .await?; + } + audit_log( &mut *tx, &authed, diff --git a/backend/windmill-api-integration-tests/tests/fork_clone_on_behalf_of.rs b/backend/windmill-api-integration-tests/tests/fork_clone_on_behalf_of.rs index dae02f3e5a..bc6c10f1ee 100644 --- a/backend/windmill-api-integration-tests/tests/fork_clone_on_behalf_of.rs +++ b/backend/windmill-api-integration-tests/tests/fork_clone_on_behalf_of.rs @@ -311,3 +311,130 @@ async fn test_fork_keeps_only_resolvable_on_behalf_of(db: Pool) -> any Ok(()) } + +/// Apps, schedules, triggers and their drafts cannot drop an identity the way scripts and flows +/// do, so one naming nobody in the fork goes to its creator while one that still resolves stays. +/// Forked as an admin, whose app policies the clone otherwise keeps. +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_fork_repoints_unresolvable_identities(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let base_url = format!("http://localhost:{}/api", server.addr.port()); + + let stranger = json!({ + "on_behalf_of": "u/test-user-2", + "on_behalf_of_email": "test2@windmill.dev", + "execution_mode": "publisher", + }); + sqlx::query( + "INSERT INTO app (workspace_id, path, summary, policy, versions) + VALUES ('test-workspace', 'u/test-user/stranger', '', $1, '{}'), + ('test-workspace', 'u/test-user/group', '', $2, '{}')", + ) + .bind(&stranger) + .bind(json!({ + "on_behalf_of": "g/all", + "on_behalf_of_email": "group-all@windmill.dev", + "execution_mode": "publisher", + })) + .execute(&db) + .await?; + // The clone re-aggregates `versions` from `app_version`, and the column is NOT NULL. + sqlx::query( + "WITH v AS ( + INSERT INTO app_version (app_id, value, created_by) + SELECT id, '{}'::json, 'test-user' FROM app WHERE workspace_id = 'test-workspace' + RETURNING id, app_id + ) + UPDATE app SET versions = ARRAY[v.id] FROM v WHERE app.id = v.app_id", + ) + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO draft (workspace_id, path, typ, value, created_at, email) + VALUES ('test-workspace', 'u/test-user/stranger', 'raw_app', $1::json, NOW(), 'test@windmill.dev'), + ('test-workspace', 'u/test-user/stranger', 'trigger_websocket', $2::json, NOW(), 'test@windmill.dev'), + ('test-workspace', 'u/test-user/nul', 'raw_app', $3::json, NOW(), 'test@windmill.dev')", + ) + .bind(json!({ "policy": stranger })) + .bind(json!({ "permissioned_as": "u/test-user-2" })) + // Saved before drafts were stripped of NULs: any jsonb parse of it raises, so it must be + // skipped rather than abort the fork. Built from parts because a NUL escape can't sit in source. + .bind(format!( + r#"{{"policy":{{"on_behalf_of":"u/test-user-2"}},"files":{{"f":"a{}u0000"}}}}"#, + "\\" + )) + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO schedule (workspace_id, path, edited_by, schedule, script_path, email, permissioned_as, enabled) + VALUES ('test-workspace', 'u/test-user/stranger', 'test-user', '0 0 * * * *', 'u/test-user/s', 'test2@windmill.dev', 'u/test-user-2', false)", + ) + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, edited_by, permissioned_as, mode) + VALUES ('test-workspace', 'u/test-user/stranger', 'ws://localhost', 'u/test-user/s', false, 'test-user', 'u/test-user-2', 'disabled')", + ) + .execute(&db) + .await?; + + let resp = reqwest::Client::new() + .post(format!( + "{base_url}/w/test-workspace/workspaces/create_fork" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "id": "wm-fork-repoint", "name": "Fork", "color": "#0000ff" })) + .send() + .await?; + assert!( + resp.status().is_success(), + "creating the fork: {}", + resp.text().await? + ); + + let text = |sql: &'static str| sqlx::query_scalar::<_, String>(sql).fetch_one(&db); + assert_eq!( + text("SELECT (policy->>'on_behalf_of') || ' ' || (policy->>'on_behalf_of_email') FROM app WHERE workspace_id = 'wm-fork-repoint' AND path = 'u/test-user/stranger'").await?, + "u/test-user test@windmill.dev" + ); + assert_eq!( + text("SELECT policy->>'on_behalf_of' FROM app WHERE workspace_id = 'wm-fork-repoint' AND path = 'u/test-user/group'").await?, + "g/all" + ); + assert_eq!( + text("SELECT value->'policy'->>'on_behalf_of' FROM draft WHERE workspace_id = 'wm-fork-repoint' AND path = 'u/test-user/stranger' AND typ = 'raw_app'").await?, + "u/test-user" + ); + // `clone_drafts` strips a NUL escape as it copies, so the row reaches the fork + // parseable and the repoint below reaches it like any other draft's. The rule this + // guards is that the fork completes and no identity naming nobody survives it; the + // skip only ever existed because `to_jsonb` raises on a value still holding one. + assert_eq!( + text("SELECT CASE WHEN strpos(value::text, 'u/test-user-2') > 0 THEN 'kept' ELSE 'rewritten' END FROM draft WHERE workspace_id = 'wm-fork-repoint' AND path = 'u/test-user/nul'").await?, + "rewritten" + ); + // And it arrives without the poison that made it a special case. + assert_eq!( + text("SELECT CASE WHEN position(chr(92) || 'u0000' in value::text) > 0 THEN 'poisoned' ELSE 'clean' END FROM draft WHERE workspace_id = 'wm-fork-repoint' AND path = 'u/test-user/nul'").await?, + "clean" + ); + assert_eq!( + text("SELECT value->>'permissioned_as' FROM draft WHERE workspace_id = 'wm-fork-repoint' AND typ = 'trigger_websocket'").await?, + "u/test-user" + ); + assert_eq!( + text("SELECT permissioned_as || ' ' || email FROM schedule WHERE workspace_id = 'wm-fork-repoint'").await?, + "u/test-user test@windmill.dev" + ); + assert_eq!( + text( + "SELECT permissioned_as FROM websocket_trigger WHERE workspace_id = 'wm-fork-repoint'" + ) + .await?, + "u/test-user" + ); + + Ok(()) +} diff --git a/backend/windmill-api-integration-tests/tests/fork_members.rs b/backend/windmill-api-integration-tests/tests/fork_members.rs new file mode 100644 index 0000000000..5f4baced1f --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/fork_members.rs @@ -0,0 +1,75 @@ +use serde_json::json; +use sqlx::{Pool, Postgres}; + +use windmill_test_utils::*; + +/// With `add_admins_and_developers_to_forks` on, a fork starts with the parent's admins and +/// developers at their parent role, even when a developer forks it; operators are left out. The +/// copies are manual members: a parent membership that came from an instance group must not carry +/// that provenance into a fork that does not configure the group. +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_fork_adds_parent_admins_and_developers(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let base_url = format!( + "http://localhost:{}/api/w/test-workspace/workspaces", + server.addr.port() + ); + let client = reqwest::Client::new(); + + sqlx::query( + "UPDATE usr SET operator = true WHERE workspace_id = 'test-workspace' AND username = 'test-user-3'", + ) + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO usr (workspace_id, email, username, is_admin, added_via) + VALUES ('test-workspace', 'test4@windmill.dev', 'test-user-4', false, + '{\"source\": \"instance_group\", \"group\": \"devs\"}')", + ) + .execute(&db) + .await?; + + let resp = client + .post(format!( + "{base_url}/edit_add_admins_and_developers_to_forks" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "add_admins_and_developers_to_forks": true })) + .send() + .await?; + assert!( + resp.status().is_success(), + "enabling the setting: {}", + resp.text().await? + ); + + let resp = client + .post(format!("{base_url}/create_fork")) + .header("Authorization", "Bearer SECRET_TOKEN_2") + .json(&json!({ "id": "wm-fork-team", "name": "Team fork" })) + .send() + .await?; + assert!( + resp.status().is_success(), + "creating the fork: {}", + resp.text().await? + ); + + let members: Vec<(String, bool, bool)> = sqlx::query_as( + "SELECT username, is_admin, added_via IS NULL FROM usr + WHERE workspace_id = 'wm-fork-team' ORDER BY username", + ) + .fetch_all(&db) + .await?; + assert_eq!( + members, + vec![ + ("test-user".to_string(), true, true), + ("test-user-2".to_string(), false, true), + ("test-user-4".to_string(), false, true), + ] + ); + + Ok(()) +} diff --git a/backend/windmill-api-integration-tests/tests/login_link.rs b/backend/windmill-api-integration-tests/tests/login_link.rs index 5c3a231a48..3906232075 100644 --- a/backend/windmill-api-integration-tests/tests/login_link.rs +++ b/backend/windmill-api-integration-tests/tests/login_link.rs @@ -115,6 +115,60 @@ async fn login_link_is_single_use_and_same_origin(db: Pool) -> anyhow: Ok(()) } +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn confirmed_login_link_is_spent_by_the_click_not_the_page( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api"); + + let resp = client() + .post(format!("{base}/users/login_links")) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({"email": "test2@windmill.dev", "confirm": true})) + .send() + .await?; + assert_eq!(resp.status(), 201); + let link = resp.json::().await?; + // The URL handed out is the frontend page, not the API path that signs in on a GET. + let token = link["url"] + .as_str() + .unwrap() + .split_once("/user/login_link?token=") + .expect("confirmation page url") + .1 + .to_string(); + + let confirm = || { + client() + .post(format!("{base}/auth/login_link/{token}")) + .send() + }; + let resp = confirm().await?; + assert_eq!(resp.status(), 200); + assert!(resp + .headers() + .get_all("set-cookie") + .iter() + .any(|c| c.to_str().unwrap().starts_with("token="))); + assert_eq!( + resp.json::().await?["location"], + "/user/workspaces" + ); + + let resp = confirm().await?; + assert_eq!(resp.status(), 200); + assert!(resp.headers().get("set-cookie").is_none()); + assert_eq!( + resp.json::().await?["location"], + "/user/login_link_expired?reason=used" + ); + + Ok(()) +} + #[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn login_link_mint_can_require_a_login_type(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; diff --git a/backend/windmill-api-integration-tests/tests/max_token_expiration.rs b/backend/windmill-api-integration-tests/tests/max_token_expiration.rs new file mode 100644 index 0000000000..b748ce49a1 --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/max_token_expiration.rs @@ -0,0 +1,239 @@ +//! `max_token_expiration_days`: the instance-wide ceiling on how far ahead a token a caller +//! picks the lifetime of may expire, and the service-account exemption. + +use serde_json::json; +use sqlx::types::chrono::{DateTime, Utc}; +use sqlx::{Pool, Postgres}; +use windmill_test_utils::*; + +const DAY: u64 = 24 * 60 * 60; + +fn client() -> reqwest::Client { + reqwest::Client::new() +} + +fn from_now(secs: u64) -> DateTime { + Utc::now() + std::time::Duration::from_secs(secs) +} + +async fn set_max(db: &Pool, value: serde_json::Value) { + sqlx::query( + "INSERT INTO global_settings (name, value) VALUES ('max_token_expiration_days', $1) + ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value", + ) + .bind(value) + .execute(db) + .await + .unwrap(); +} + +/// Mints as `test2@windmill.dev`, a plain member of `test-workspace`. +async fn create_token(port: u16, body: serde_json::Value) -> reqwest::Response { + client() + .post(format!("http://localhost:{port}/api/users/tokens/create")) + .header("Authorization", "Bearer SECRET_TOKEN_2") + .json(&body) + .send() + .await + .unwrap() +} + +async fn stored_expiration(db: &Pool, label: &str) -> Option> { + sqlx::query_scalar::<_, Option>>("SELECT expiration FROM token WHERE label = $1") + .bind(label) + .fetch_one(db) + .await + .unwrap() +} + +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_max_token_expiration_days_shortens_user_tokens( + db: Pool, +) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let resp = create_token(port, json!({ "label": "unset" })).await; + assert_eq!(resp.status(), 201); + assert_eq!( + stored_expiration(&db, "unset").await, + None, + "with no setting a token may still have no expiration" + ); + + // Refused at write time: the token routes can only read a value they cannot parse as no + // ceiling at all. + let resp = client() + .post(format!( + "http://localhost:{port}/api/settings/global/max_token_expiration_days" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "value": 7.5 })) + .send() + .await?; + assert_eq!(resp.status(), 400); + + set_max(&db, json!(7)).await; + + // The token form reads the ceiling as whoever is creating the token, usually not a + // superadmin, so it can offer only expirations the server would keep. + let resp = client() + .get(format!( + "http://localhost:{port}/api/settings/global/max_token_expiration_days" + )) + .header("Authorization", "Bearer SECRET_TOKEN_2") + .send() + .await?; + assert_eq!(resp.status(), 200); + assert_eq!(resp.text().await?, "7"); + + let resp = create_token(port, json!({ "label": "none asked" })).await; + assert_eq!(resp.status(), 201); + let expiration = stored_expiration(&db, "none asked") + .await + .expect("a token asking for no expiration gets the ceiling"); + assert!( + expiration > from_now(6 * DAY) && expiration <= from_now(7 * DAY), + "expected the 7 day ceiling, got {expiration}" + ); + + let resp = create_token( + port, + json!({ "label": "past the ceiling", "expiration": from_now(30 * DAY) }), + ) + .await; + assert_eq!(resp.status(), 201); + let expiration = stored_expiration(&db, "past the ceiling").await.unwrap(); + assert!( + expiration > from_now(6 * DAY) && expiration <= from_now(7 * DAY), + "expected an expiration past the ceiling to be shortened to it, got {expiration}" + ); + + let resp = create_token( + port, + json!({ "label": "within", "expiration": from_now(3 * DAY) }), + ) + .await; + assert_eq!(resp.status(), 201); + let expiration = stored_expiration(&db, "within").await.unwrap(); + assert!( + expiration <= from_now(3 * DAY), + "an expiration within the ceiling must be kept, got {expiration}" + ); + + // The settings UI stores an integer, but the YAML instance config and config sync can write + // the same whole number as a string or as `5.0`. Reading either as "unset" would silently + // drop the ceiling, while the token form (`parseMaxTokenExpirationDays`) still showed it. + for (stored, label) in [ + (json!("5"), "string setting"), + (json!(5.0), "float setting"), + ] { + set_max(&db, stored).await; + let resp = create_token(port, json!({ "label": label })).await; + assert_eq!(resp.status(), 201); + let expiration = stored_expiration(&db, label).await; + assert!( + expiration.is_some_and(|e| e > from_now(4 * DAY) && e <= from_now(5 * DAY)), + "{label}: expected the 5 day ceiling, got {expiration:?}" + ); + } + + // A superadmin impersonating a user picks the lifetime too, so the ceiling applies there; + // left out, it would be the one way to mint a token that never expires. + let resp = client() + .post(format!( + "http://localhost:{port}/api/users/tokens/impersonate" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "label": "impersonated", "impersonate_email": "test3@windmill.dev" })) + .send() + .await?; + assert_eq!(resp.status(), 201); + assert!( + stored_expiration(&db, "impersonated").await.is_some(), + "an impersonation token asking for no expiration gets the ceiling" + ); + + Ok(()) +} + +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_service_accounts_are_exempt(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + set_max(&db, json!(7)).await; + // The same email is a service account in one workspace and an ordinary user in another. + sqlx::query( + "UPDATE usr SET is_service_account = true + WHERE email = 'test2@windmill.dev' AND workspace_id = 'test-workspace'", + ) + .execute(&db) + .await?; + sqlx::query("INSERT INTO workspace (id, name, owner) VALUES ('other', 'other', 'test-user')") + .execute(&db) + .await?; + sqlx::query("INSERT INTO workspace_settings (workspace_id) VALUES ('other')") + .execute(&db) + .await?; + sqlx::query( + "INSERT INTO usr (workspace_id, email, username, is_admin, role) + VALUES ('other', 'test2@windmill.dev', 'test-user-2', false, 'User')", + ) + .execute(&db) + .await?; + + // The token form decides the exemption from this list, so it must carry each membership's flag. + let memberships: serde_json::Value = client() + .get(format!("http://localhost:{port}/api/workspaces/users")) + .header("Authorization", "Bearer SECRET_TOKEN_2") + .send() + .await? + .json() + .await?; + for (workspace, is_service_account) in [("test-workspace", true), ("other", false)] { + let membership = memberships["workspaces"] + .as_array() + .and_then(|ws| ws.iter().find(|w| w["id"] == workspace)) + .unwrap_or_else(|| panic!("{workspace} missing from {memberships}")); + assert_eq!(membership["is_service_account"], json!(is_service_account)); + } + + for (label, workspace_id, exempt) in [ + ("own workspace", Some("test-workspace"), true), + ("other workspace", Some("other"), false), + // A workspace-less token has no workspace to match, so a service account anywhere counts. + ("global", None, true), + ] { + let resp = create_token( + port, + json!({ "label": label, "workspace_id": workspace_id }), + ) + .await; + assert_eq!(resp.status(), 201); + assert_eq!( + stored_expiration(&db, label).await.is_none(), + exempt, + "{label}: expected exempt = {exempt}" + ); + } + + // Impersonation checks the impersonated account, not the superadmin minting the token. + let resp = client() + .post(format!( + "http://localhost:{port}/api/users/tokens/impersonate" + )) + .header("Authorization", "Bearer SECRET_TOKEN") + .json(&json!({ "label": "impersonated service account", "impersonate_email": "test2@windmill.dev" })) + .send() + .await?; + assert_eq!(resp.status(), 201); + assert_eq!( + stored_expiration(&db, "impersonated service account").await, + None + ); + + Ok(()) +} diff --git a/backend/windmill-api-integration-tests/tests/mcp_token_query_param.rs b/backend/windmill-api-integration-tests/tests/mcp_token_query_param.rs new file mode 100644 index 0000000000..d0d6e070f6 --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/mcp_token_query_param.rs @@ -0,0 +1,102 @@ +//! The `mcp_disable_token_query_param` switch closes the URL-borne credential path. +//! +//! The rejection is a middleware layered between the `WWW-Authenticate` decorator and +//! everything that reads a token, on both the workspaced and the gateway mount. Each half of +//! that sandwich is pinned: the `WWW-Authenticate` header on the refusal catches the layer +//! being moved outward (a client would lose the pointer that starts OAuth discovery), and +//! refusing a token that was never valid catches it being moved inward past authentication +//! (the URL-borne token would be hashed and looked up before anything refused it). +#![cfg(feature = "mcp")] + +use std::sync::atomic::Ordering; + +use serde_json::json; +use sqlx::{Pool, Postgres}; + +use windmill_common::global_settings::MCP_DISABLE_TOKEN_QUERY_PARAM; +use windmill_test_utils::*; + +/// Workspace-less with an `mcp:` scope, which is what the gateway mount requires; the +/// workspaced mount takes its workspace from the path, so one token reaches both. +async fn insert_mcp_token(db: &Pool) -> anyhow::Result<()> { + sqlx::query( + "INSERT INTO token (token_hash, token_prefix, token, email, label, super_admin, scopes) + VALUES (encode(sha256('MCP_TOKEN'::bytea), 'hex'), 'MCP_TOK', 'MCP_TOKEN', 'test@windmill.dev', 'mcp token', true, ARRAY['mcp:all'])", + ) + .execute(db) + .await?; + Ok(()) +} + +/// A token that is not in `token` at all. Authentication would refuse it on its own, so a +/// refusal carrying the middleware's own wording is evidence nothing looked it up first. +const BOGUS_TOKEN: &str = "NOT_A_REAL_TOKEN"; + +async fn tools_list(url: &str) -> anyhow::Result { + Ok(reqwest::Client::new() + .post(url) + .header("Accept", "application/json, text/event-stream") + .json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} })) + .send() + .await?) +} + +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_mcp_token_query_param_switch(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + set_jwt_secret().await; + insert_mcp_token(&db).await?; + let server = ApiServer::start_mcp(db.clone()).await?; + let port = server.addr.port(); + let workspaced = + format!("http://localhost:{port}/api/mcp/w/test-workspace/mcp?token=MCP_TOKEN"); + let gateway = format!("http://localhost:{port}/api/mcp/gateway?token=MCP_TOKEN"); + + assert_eq!( + tools_list(&workspaced).await?.status(), + 200, + "a URL-borne token is the documented default and must keep working while the switch is off" + ); + assert_eq!(tools_list(&gateway).await?.status(), 200); + + MCP_DISABLE_TOKEN_QUERY_PARAM.store(true, Ordering::Relaxed); + + for url in [&workspaced, &gateway] { + let resp = tools_list(url).await?; + assert_eq!( + resp.status(), + 401, + "{url} still admitted a token in the URL" + ); + // What sends the client into the OAuth flow rather than leaving it stuck on a 401. + assert!( + resp.headers().contains_key("www-authenticate"), + "{url} rejected without pointing at the authorization server" + ); + } + + // Refused before authentication, not after: an invalid token gets the middleware's own + // message rather than the generic 401 that looking it up would produce. + let resp = tools_list(&format!( + "http://localhost:{port}/api/mcp/w/test-workspace/mcp?token={BOGUS_TOKEN}" + )) + .await?; + assert_eq!(resp.status(), 401); + assert!( + resp.text().await?.contains("does not accept a token in the MCP URL"), + "an invalid URL token was answered by authentication, so the token was read before \ + the switch refused it" + ); + + // The header stays open: it is the channel the OAuth flow itself hands tokens over on. + let resp = reqwest::Client::new() + .post(format!("http://localhost:{port}/api/mcp/gateway")) + .header("Accept", "application/json, text/event-stream") + .header("Authorization", "Bearer MCP_TOKEN") + .json(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} })) + .send() + .await?; + assert_eq!(resp.status(), 200); + + Ok(()) +} diff --git a/backend/windmill-api-integration-tests/tests/token_expiry_warning.rs b/backend/windmill-api-integration-tests/tests/token_expiry_warning.rs new file mode 100644 index 0000000000..667e064cca --- /dev/null +++ b/backend/windmill-api-integration-tests/tests/token_expiry_warning.rs @@ -0,0 +1,49 @@ +//! Which user tokens get an "expiring soon" warning queued when they are created. + +use serde_json::json; +use sqlx::types::chrono::Utc; +use sqlx::{Pool, Postgres}; +use windmill_test_utils::*; + +const DAY: u64 = 24 * 60 * 60; + +async fn warning_queued(db: &Pool, label: &str) -> bool { + sqlx::query_scalar::<_, bool>( + "SELECT EXISTS(SELECT 1 FROM token_expiry_notification n + JOIN token t ON t.token_hash = n.token_hash WHERE t.label = $1)", + ) + .bind(label) + .fetch_one(db) + .await + .unwrap() +} + +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_short_lived_tokens_get_no_expiry_warning(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + for (label, lifetime) in [("short", DAY), ("long", 30 * DAY)] { + let resp = reqwest::Client::new() + .post(format!("http://localhost:{port}/api/users/tokens/create")) + .header("Authorization", "Bearer SECRET_TOKEN_2") + .json(&json!({ + "label": label, + "expiration": Utc::now() + std::time::Duration::from_secs(lifetime), + })) + .send() + .await?; + assert_eq!(resp.status(), 201); + } + + assert!( + !warning_queued(&db, "short").await, + "a token whose whole lifetime fits in the warning window must not be warned about" + ); + assert!( + warning_queued(&db, "long").await, + "a longer-lived token still gets its warning" + ); + Ok(()) +} diff --git a/backend/windmill-api-integration-tests/tests/workspace_encryption_key_git_sync.rs b/backend/windmill-api-integration-tests/tests/workspace_encryption_key_git_sync.rs index 8edfd39d61..a22ad974c3 100644 --- a/backend/windmill-api-integration-tests/tests/workspace_encryption_key_git_sync.rs +++ b/backend/windmill-api-integration-tests/tests/workspace_encryption_key_git_sync.rs @@ -275,6 +275,67 @@ async fn test_encryption_key_rotation_dispatches_batched_git_sync( Ok(()) } +/// Stored repository tokens and webhook secrets are encrypted under the +/// workspace key but never synced, so a rotation has to carry them over even +/// when the caller skips re-encrypting variables. +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_encryption_key_rotation_reencrypts_git_sync_secrets( + db: Pool, +) -> anyhow::Result<()> { + use windmill_common::variables::{build_crypt, crypt_from_key_with_suffix, decrypt, encrypt}; + initialize_tracing().await; + + create_folder(&db, "28103").await?; + create_git_repo_resource(&db).await?; + let sync_script_path = "f/28103/test_sync_script_git_secrets"; + create_sync_script(&db, sync_script_path).await?; + setup_git_sync_config(&db, sync_script_path).await?; + + let mc = build_crypt(&db, "test-workspace").await?; + sqlx::query( + r#" + UPDATE workspace_settings SET + git_credentials = jsonb_build_array(jsonb_build_object( + 'token', $1::text, 'repo_identity', 'https://gitlab.example.com/grp/proj')), + git_sync = jsonb_set(git_sync, '{repositories,0,auto_pull}', jsonb_build_object( + 'enabled', true, 'mode', 'webhook', 'webhook_id', 1, 'webhook_secret', $2::text)) + WHERE workspace_id = 'test-workspace' + "#, + ) + .bind(encrypt(&mc, "stored-token")) + .bind(encrypt(&mc, "hook-secret")) + .execute(&db) + .await?; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/test-workspace/workspaces"); + + let new_key = "c".repeat(64); + let resp = authed(client().post(format!("{base}/encryption_key"))) + .json(&json!({"new_key": new_key, "skip_reencrypt": true})) + .send() + .await?; + assert_eq!( + resp.status(), + 200, + "set_encryption_key failed: {}", + resp.text().await? + ); + + let (token, secret): (String, String) = sqlx::query_as( + "SELECT git_credentials->0->>'token', git_sync#>>'{repositories,0,auto_pull,webhook_secret}' + FROM workspace_settings WHERE workspace_id = 'test-workspace'", + ) + .fetch_one(&db) + .await?; + let new_mc = crypt_from_key_with_suffix(&new_key, ""); + assert_eq!(decrypt(&new_mc, token)?, "stored-token"); + assert_eq!(decrypt(&new_mc, secret)?, "hook-secret"); + + Ok(()) +} + /// Regression test for the non-debouncing fallback: a workspace whose sync /// script predates hub version 28103 must still receive git-sync jobs for the /// encryption_key entry and every re-encrypted secret. Before the fallback was diff --git a/backend/windmill-api-integration-tests/tests/workspaces.rs b/backend/windmill-api-integration-tests/tests/workspaces.rs index 25312cae27..ff603269e7 100644 --- a/backend/windmill-api-integration-tests/tests/workspaces.rs +++ b/backend/windmill-api-integration-tests/tests/workspaces.rs @@ -1166,3 +1166,47 @@ async fn test_create_service_account_drops_orphaned_group_memberships( Ok(()) } + +#[cfg(feature = "private")] +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_edit_auto_invite_preserves_instance_groups(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + sqlx::query( + r#"UPDATE workspace_settings + SET auto_invite = '{"instance_groups": ["eng"], "instance_groups_roles": {"eng": "developer"}}' + WHERE workspace_id = 'test-workspace'"#, + ) + .execute(&db) + .await?; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/test-workspace/workspaces"); + + // enable, then disable + for body in [ + json!({"operator": false, "invite_all": true, "auto_add": false}), + json!({}), + ] { + let resp = authed(client().post(format!("{base}/edit_auto_invite"))) + .json(&body) + .send() + .await?; + assert_eq!(resp.status(), 200, "{body}: {}", resp.text().await?); + + let auto_invite: serde_json::Value = sqlx::query_scalar( + "SELECT auto_invite FROM workspace_settings WHERE workspace_id = 'test-workspace'", + ) + .fetch_one(&db) + .await?; + assert_eq!(auto_invite["instance_groups"], json!(["eng"]), "{body}"); + assert_eq!( + auto_invite["instance_groups_roles"], + json!({"eng": "developer"}), + "{body}" + ); + } + + Ok(()) +} diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index a4667e4876..51f2619805 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -55,7 +55,7 @@ use windmill_common::{ min_version_supports_runnable_settings_v0, RunnableSettings, RunnableSettingsTrait, }, scripts::{hash_script, ScriptRunnableSettingsHandle, ScriptRunnableSettingsInline}, - utils::{paginate_without_limits, WarnAfterExt}, + utils::{paginate_optional, paginate_without_limits, WarnAfterExt}, worker::CLOUD_HOSTED, }; use windmill_object_store::upload_artifact_to_store; @@ -2312,6 +2312,20 @@ async fn create_script_internal<'c>( .await?; } + if p_path != &ns.path { + // Everything left at the old path is a draft this deploy didn't + // consume — teammates' rows, and the deployer's own when the caller + // asked us to keep it. Carry them rather than strand them. + windmill_common::user_drafts::move_drafts_for_path( + &mut tx, + &w_id, + &[UserDraftItemKind::Script], + p_path, + &ns.path, + ) + .await?; + } + sqlx::query!( "UPDATE capture_config SET path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS FALSE", ns.path, @@ -2389,19 +2403,32 @@ async fn create_script_internal<'c>( tx = push_scheduled_job(&db, tx, &schedule, None, None).await?; } } - } else if !skip_draft_deletion { - // See the matching branch above — only wipe the deployer's own - // draft (plus the legacy NULL-email row). - sqlx::query!( - "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script' \ - AND (email = $3 OR email IS NULL)", - ns.path, - &w_id, - &authed.email, - ) - .execute(&mut *tx) - .await?; + } else { + if !skip_draft_deletion { + // See the matching branch above — only wipe the deployer's own + // draft (plus the legacy NULL-email row). + sqlx::query!( + "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script' \ + AND (email = $3 OR email IS NULL)", + ns.path, + &w_id, + &authed.email, + ) + .execute(&mut *tx) + .await?; + } } + // Every deploy, not only a new script: an archived script's draft can be moved away + // (`move_draft` ignores archived rows), and unarchiving redeploys at the same path, + // where a route left behind would send the live script's saves to the moved draft. + windmill_common::user_drafts::clear_draft_moves_from( + &mut tx, + &w_id, + &[UserDraftItemKind::Script], + &ns.path, + p_path_opt.as_deref(), + ) + .await?; if p_hashes.is_some() && !p_hashes.unwrap().is_empty() { audit_log( &mut *tx, @@ -3061,17 +3088,24 @@ async fn get_script_history( authed: ApiAuthed, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, + Query(pagination): Query, ) -> JsonResult> { let path = path.to_path(); check_scopes(&authed, || format!("scripts:read:{}", path))?; + // Unasked-for, this listing stays whole: the deployment-history panels, the restart + // picker and the CLI all read it without paging. The diff picker asks for a page. + let (per_page, offset) = paginate_optional(pagination); let mut tx = user_db.begin(&authed).await?; let query_result = sqlx::query!( - "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at + "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at, s.created_by as created_by FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash WHERE s.workspace_id = $1 AND s.path = $2 - ORDER by s.created_at DESC", + ORDER by s.created_at DESC + LIMIT $3 OFFSET $4", w_id, path, + per_page, + offset, ) .fetch_all(&mut *tx) .await?; @@ -3083,6 +3117,7 @@ async fn get_script_history( script_hash: ScriptHash(row.hash), deployment_msg: row.deployment_msg, created_at: Some(row.created_at), + created_by: Some(row.created_by), }) .collect(); return Ok(Json(result)); @@ -3097,7 +3132,7 @@ async fn get_latest_version( check_scopes(&authed, || format!("scripts:read:{}", path))?; let mut tx = user_db.begin(&authed).await?; let row_o = sqlx::query!( - "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at + "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at, s.created_by as created_by FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash WHERE s.workspace_id = $1 AND s.path = $2 ORDER by s.created_at DESC LIMIT 1", @@ -3113,6 +3148,7 @@ async fn get_latest_version( script_hash: ScriptHash(row.hash), deployment_msg: row.deployment_msg, created_at: Some(row.created_at), + created_by: Some(row.created_by), }; return Ok(Json(Some(result))); } else { diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index f73eacd6b9..8f48f7e3f6 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -63,6 +63,7 @@ use windmill_common::{ GITHUB_APP_WEBHOOK_BASE_URL_SETTING, HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS_SETTING, HTTP_ROUTE_WORKSPACED_ROUTE_SETTING, HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING, INSTANCE_BANNER_SETTING, MAX_RETENTION_OVERRIDE_WORKSPACES, + MAX_TOKEN_EXPIRATION_DAYS_SETTING, MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING, RETENTION_PERIOD_SECS_OVERRIDES_SETTING, RUFF_CONFIG_SETTING, UNIQUE_ID_SETTING, WORKSPACE_FAIRNESS_DURATION_SECS_SETTING, WORKSPACE_FAIRNESS_ENABLED_SETTING, WORKSPACE_FAIRNESS_MAX_PERCENT_SETTING, WORKSPACE_FAIRNESS_MIN_TOTAL_SETTING, @@ -1209,6 +1210,12 @@ async fn run_setting_pre_write_hook( } } } + MAX_TOKEN_EXPIRATION_DAYS_SETTING => { + windmill_common::global_settings::parse_max_token_expiration_days(Some(value)) + .map_err(|e| { + error::Error::BadRequest(format!("{MAX_TOKEN_EXPIRATION_DAYS_SETTING}: {e}")) + })?; + } INSTANCE_BANNER_SETTING => { match value { // Clearing (delete row) is handled by the caller; allow it through. @@ -1378,6 +1385,12 @@ pub async fn get_global_setting( && key != HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS_SETTING && key != WS_BASE_URL_SETTING && key != INSTANCE_BANNER_SETTING + // The token form reads it to stop offering expirations the server would shorten. + && key != MAX_TOKEN_EXPIRATION_DAYS_SETTING + // Whoever is wiring up an MCP client reads it to know whether a URL-borne token + // would be refused, and they are usually not a superadmin. Not a secret: pointing + // any MCP client at the instance discovers the same answer. + && key != MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING { require_super_admin(&db, &authed).await?; } diff --git a/backend/windmill-api-users/src/users.rs b/backend/windmill-api-users/src/users.rs index 2bfac244f5..da81c7e228 100644 --- a/backend/windmill-api-users/src/users.rs +++ b/backend/windmill-api-users/src/users.rs @@ -47,7 +47,10 @@ use windmill_audit::audit_oss::audit_log; use windmill_audit::ActionKind; use windmill_common::audit::AuditAuthor; use windmill_common::auth::{hash_token, safe_token_prefix, TOKEN_PREFIX_LEN}; -use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING; +use windmill_common::global_settings::{ + load_value_from_global_settings, parse_max_token_expiration_days, + AUTOMATE_USERNAME_CREATION_SETTING, MAX_TOKEN_EXPIRATION_DAYS_SETTING, +}; use windmill_common::oauth2::InstanceEvent; use windmill_common::per_minute_counter::PerMinuteCounter; use windmill_common::users::truncate_token; @@ -168,7 +171,10 @@ pub fn make_unauthed_service() -> Router { .route("/logout", post(logout).get(logout)) .route("/is_first_time_setup", get(is_first_time_setup)) .route("/request_password_reset", post(request_password_reset)) - .route("/login_link/{token}", get(consume_login_link)) + .route( + "/login_link/{token}", + get(consume_login_link).post(confirm_login_link), + ) .route("/is_smtp_configured", get(is_smtp_configured)) .route( "/is_password_login_disabled", @@ -3092,11 +3098,68 @@ pub async fn create_guest_session_token<'c>( // create_token_internal is re-exported from windmill-api-auth above +/// Applies the instance-wide ceiling on how long a token a caller picks the lifetime of may +/// live (`create_token`, and `impersonate` for superadmins), returning the expiration to store: +/// the requested one while it fits, the ceiling otherwise, and the ceiling as well when none was +/// requested. Only the stored expiration is capped: tokens already stored when the setting is +/// turned on or lowered keep theirs, since the auth lookup never reads the setting. +/// +/// It shortens rather than refuses because most callers do not comply on their own. The CLI +/// authorization page, `wmill user create-token` and the editor's language-server token each +/// pick a lifetime, often none at all, without reading the setting (and CLIs already installed +/// never will), so refusing would break logging in and the editor instead of the long-lived +/// tokens the setting is aimed at. +/// +/// Read from `global_settings` on each call rather than cached: token creation is rare +/// enough that the round trip costs nothing, and the ceiling is then never served stale. +/// +/// A token owned by a service account is exempt: in the workspace the token names, or in any +/// workspace for a workspace-less token, which has none to match. Service accounts are the +/// identity automation that needs a long-lived credential runs as. The cost is that any +/// workspace admin can create and impersonate one to hold an uncapped token, so the ceiling +/// bounds personal tokens rather than what an admin can obtain. +async fn cap_token_expiration( + db: &DB, + owner_email: &str, + workspace_id: Option<&str>, + requested: Option>, +) -> Result>> { + let value = load_value_from_global_settings(db, MAX_TOKEN_EXPIRATION_DAYS_SETTING).await?; + let max_days = match parse_max_token_expiration_days(value.as_ref()) { + Ok(Some(max_days)) => max_days, + Ok(None) => return Ok(requested), + // Both write paths reject this, so only a row written around them gets here. + Err(e) => { + tracing::warn!("ignoring {MAX_TOKEN_EXPIRATION_DAYS_SETTING}: {e}"); + return Ok(requested); + } + }; + let max = chrono::Utc::now() + chrono::Duration::days(max_days); + + let is_service_account = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM usr WHERE email = $1 AND is_service_account IS true + AND ($2::varchar IS NULL OR workspace_id = $2))", + owner_email, + workspace_id, + ) + .fetch_one(db) + .await? + .unwrap_or(false); + if is_service_account { + return Ok(requested); + } + + Ok(Some(match requested { + Some(expiration) if expiration < max => expiration, + _ => max, + })) +} + async fn create_token( Extension(db): Extension, authed: ApiAuthed, OptJobAuthed { job_id, .. }: OptJobAuthed, - Json(token_config): Json, + Json(mut token_config): Json, ) -> Result<(StatusCode, String)> { forbid_elevated_job_token(&db, &authed.email, job_id).await?; check_token_create_rate_limit(&authed.username)?; @@ -3118,6 +3181,14 @@ async fn create_token( windmill_api_auth::ensure_scopes_within_caller(&authed, token_config.scopes.as_deref())?; + token_config.expiration = cap_token_expiration( + &db, + &authed.email, + token_config.workspace_id.as_deref(), + token_config.expiration, + ) + .await?; + let mut tx = db.begin().await?; let token = create_token_internal(&mut *tx, &db, &authed, token_config).await?; @@ -3174,6 +3245,7 @@ async fn impersonate( .fetch_optional(&db) .await? .unwrap_or(false); + let expiration = cap_token_expiration(&db, &impersonated, None, new_token.expiration).await?; let mut tx = db.begin().await?; sqlx::query!( @@ -3185,7 +3257,7 @@ async fn impersonate( plaintext as Option<&str>, impersonated, new_token.label, - new_token.expiration, + expiration, is_super_admin ) .execute(&mut *tx) @@ -3195,7 +3267,7 @@ async fn impersonate( &mut *tx, &t_hash, new_token.label.as_deref(), - new_token.expiration, + expiration, ) .await; @@ -3215,9 +3287,12 @@ async fn impersonate( } const LOGIN_LINK_DEFAULT_TTL_S: u32 = 600; -const LOGIN_LINK_MAX_TTL_S: u32 = 900; +// Long enough for a link sent by email to still work when it is read. `require_login_type` is +// only checked at mint, so a much longer cap would need re-checking it when the link is opened. +const LOGIN_LINK_MAX_TTL_S: u32 = 7200; const LOGIN_LINK_DEFAULT_RD: &str = "/user/workspaces"; const LOGIN_LINK_EXPIRED_PAGE: &str = "/user/login_link_expired"; +const LOGIN_LINK_CONFIRM_PAGE: &str = "/user/login_link"; #[derive(Deserialize)] pub struct NewLoginLink { @@ -3228,6 +3303,9 @@ pub struct NewLoginLink { /// account it created can require `pending_oauth`, so the link stops working once the /// owner has set a password or signed in with a provider. pub require_login_type: Option, + /// Hand out a page that signs in only when its button is clicked. Mail scanners open links + /// on delivery, and opening the plain link spends it, so a link sent by email sets this. + pub confirm: Option, } #[derive(Serialize)] @@ -3378,11 +3456,12 @@ async fn create_login_link( .await?; tx.commit().await?; - let url = format!( - "{}/api/auth/login_link/{}", - (**BASE_URL.load()).clone(), - token - ); + let base_url = (**BASE_URL.load()).clone(); + let url = if nl.confirm.unwrap_or(false) { + format!("{base_url}{LOGIN_LINK_CONFIRM_PAGE}?token={token}") + } else { + format!("{base_url}/api/auth/login_link/{token}") + }; Ok((StatusCode::CREATED, Json(LoginLink { url, expires_at }))) } @@ -3628,19 +3707,45 @@ async fn consume_login_link( Path(token): Path, Query(query): Query, ) -> Result { - let bounce = |reason: &str| { - Ok(login_link_redirect(format!( - "{LOGIN_LINK_EXPIRED_PAGE}?reason={reason}" - ))) - }; + let location = redeem_login_link(&headers, cookies, &db, &token, query.rd).await?; + Ok(login_link_redirect(location)) +} + +#[derive(Serialize)] +struct LoginLinkLocation { + location: String, +} + +/// The confirmation page's click. It answers with where to go rather than redirecting, and the +/// page navigates there itself. +async fn confirm_login_link( + headers: axum::http::HeaderMap, + cookies: Cookies, + Extension(db): Extension, + Path(token): Path, +) -> JsonResult { + let location = redeem_login_link(&headers, cookies, &db, &token, None).await?; + Ok(Json(LoginLinkLocation { location })) +} + +/// Spends the link and sets the session cookie, returning the post-login destination; or +/// returns the explanation page, with no session, when the link cannot be used. +async fn redeem_login_link( + headers: &axum::http::HeaderMap, + cookies: Cookies, + db: &DB, + token: &str, + requested_rd: Option, +) -> Result { + let bounce = |reason: &str| Ok(format!("{LOGIN_LINK_EXPIRED_PAGE}?reason={reason}")); if token.len() != 32 { return bounce("invalid"); } - let t_hash = hash_token(&token); + let t_hash = hash_token(token); // The account is unknown until the row is read, so only the global and per-IP tiers // apply here; a 32-char random token leaves nothing for the per-account tier to guard. windmill_common::login_rate_limit::check_and_increment_login_attempt( - &headers, + headers, &t_hash[..TOKEN_PREFIX_LEN], )?; @@ -3707,11 +3812,10 @@ async fn consume_login_link( .await?; tx.commit().await?; - let rd = link + Ok(link .rd - .or_else(|| same_origin_rd(query.rd)) - .unwrap_or_else(|| LOGIN_LINK_DEFAULT_RD.to_string()); - Ok(login_link_redirect(rd)) + .or_else(|| same_origin_rd(requested_rd)) + .unwrap_or_else(|| LOGIN_LINK_DEFAULT_RD.to_string())) } #[derive(Deserialize)] @@ -3945,6 +4049,7 @@ async fn update_token_label( AND NOT starts_with(label, 'embed_app:') AND NOT starts_with(label, 'sdk_app:') AND NOT starts_with(label, 'impersonation:') + AND NOT starts_with(label, 'cli-login:') )) RETURNING token_prefix", req.label.as_deref(), diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index a93407f6f1..9a9ae59993 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -38,7 +38,7 @@ use windmill_common::global_settings::HTTP_ROUTE_WORKSPACED_ROUTE; use windmill_common::query_builders::{render_db_quoted_identifier, DbType}; use windmill_common::users::username_to_permissioned_as; use windmill_common::variables::{ - build_crypt, decrypt, encrypt, SECRET_SALT, WORKSPACE_CRYPT_CACHE, + crypt_from_key_with_suffix, decrypt, encrypt, WORKSPACE_CRYPT_CACHE, }; use windmill_common::worker::{to_raw_value, CLOUD_HOSTED}; use windmill_common::workspaces::GitRepositorySettings; @@ -156,6 +156,10 @@ pub fn workspaced_service() -> Router { .route("/edit_deploy_ui_config", post(edit_deploy_ui_config)) .route("/edit_default_app", post(edit_default_app)) .route("/edit_guest_access", post(edit_guest_access)) + .route( + "/edit_add_admins_and_developers_to_forks", + post(edit_add_admins_and_developers_to_forks), + ) .route("/edit_guest_jwt_key", post(edit_guest_jwt_key)) .route("/guest_usage", get(get_guest_usage)) .route("/default_app", get(get_default_app)) @@ -339,6 +343,7 @@ pub struct WorkspaceSettings { pub guest_jwt_public_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub guest_jwt_jwks_url: Option, + pub add_admins_and_developers_to_forks: bool, } /// Subset of `WorkspaceSettings` that is safe to return to any workspace @@ -364,6 +369,8 @@ pub struct WorkspacePublicSettings { /// Not sensitive, and the app editor needs it to say whether the guest rung is /// live -- an app can be set to `guest` while the workspace has guests off. pub guest_access_enabled: bool, + /// Read by the fork dialog, which tells the forker who else the fork will include. + pub add_admins_and_developers_to_forks: bool, #[serde(skip_serializing_if = "Option::is_none")] pub deploy_ui: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -556,6 +563,7 @@ struct UserWorkspace { /// screen off this. pub created_by: Option, pub disabled: bool, + pub is_service_account: bool, } #[derive(Deserialize)] @@ -1126,7 +1134,8 @@ async fn get_settings( error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, - guest_jwt_jwks_url + guest_jwt_jwks_url, + add_admins_and_developers_to_forks FROM workspace_settings WHERE @@ -1168,6 +1177,7 @@ async fn get_public_settings( teams_team_guid, mute_critical_alerts, guest_access_enabled, + add_admins_and_developers_to_forks, deploy_ui, large_file_storage, datatable @@ -5248,6 +5258,47 @@ async fn edit_guest_access( )) } +#[derive(Deserialize)] +struct EditAddAdminsAndDevelopersToForks { + add_admins_and_developers_to_forks: bool, +} + +async fn edit_add_admins_and_developers_to_forks( + authed: ApiAuthed, + Extension(db): Extension, + Path(w_id): Path, + Json(EditAddAdminsAndDevelopersToForks { add_admins_and_developers_to_forks }): Json< + EditAddAdminsAndDevelopersToForks, + >, +) -> Result { + require_admin(authed.is_admin, &authed.username)?; + + let mut tx = db.begin().await?; + sqlx::query!( + "UPDATE workspace_settings SET add_admins_and_developers_to_forks = $1 WHERE workspace_id = $2", + add_admins_and_developers_to_forks, + &w_id + ) + .execute(&mut *tx) + .await?; + + audit_log( + &mut *tx, + &authed, + "workspaces.edit_add_admins_and_developers_to_forks", + ActionKind::Update, + &w_id, + Some(&add_admins_and_developers_to_forks.to_string()), + None, + ) + .await?; + tx.commit().await?; + + Ok(format!( + "Adding admins and developers to new forks set to {add_admins_and_developers_to_forks} for workspace {w_id}" + )) +} + #[derive(Deserialize)] struct EditGuestJwtKey { /// A PEM public key (RS or ES family), or a JWKS URL, at most one. Both empty clears the @@ -5836,9 +5887,6 @@ async fn set_encryption_key( )); } - // Build the previous cipher before the transaction (reads from cache/pool) - let previous_encryption_key = build_crypt(&db, w_id.as_str()).await?; - let mut tx = db.begin().await?; // Under the row's lock, so two rotations racing serialize and each sees the key the @@ -5872,17 +5920,14 @@ async fn set_encryption_key( None }; + // From the keys read and written under the lock, never from `build_crypt`: its + // cache can still hold a key an earlier rotation replaced, and the git-sync + // secrets below are skipped rather than failed when they do not decrypt. + let previous_encryption_key = crypt_from_key_with_suffix(&previous_key, ""); + let new_encryption_key = crypt_from_key_with_suffix(&request.new_key, ""); + let mut reencrypted_secret_paths: Vec = Vec::new(); if !request.skip_reencrypt.unwrap_or(false) { - // Build the new cipher directly from the key string, since the transaction - // hasn't committed yet and build_crypt() would read the old key from the pool. - let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() { - format!("{}{}", request.new_key, salt) - } else { - request.new_key.clone() - }; - let new_encryption_key = magic_crypt::new_magic_crypt!(crypt_key, 256); - let mut truncated_new_key = request.new_key.clone(); truncated_new_key.truncate(8); tracing::warn!( @@ -5922,6 +5967,14 @@ async fn set_encryption_key( } } + reencrypt_git_sync_secrets( + &mut tx, + &w_id, + &previous_encryption_key, + &new_encryption_key, + ) + .await?; + tx.commit().await?; // Invalidate the cache only after the transaction has committed @@ -5959,6 +6012,64 @@ async fn set_encryption_key( return Ok(()); } +/// Move the git-sync secrets the server keeps under the workspace key (stored +/// repository tokens, webhook secrets) to the new key. They are never synced, so +/// unlike variables they are still under the old key when the caller skips +/// re-encryption. +async fn reencrypt_git_sync_secrets( + conn: &mut sqlx::PgConnection, + w_id: &str, + old: &magic_crypt::MagicCrypt256, + new: &magic_crypt::MagicCrypt256, +) -> Result<()> { + let Some((mut credentials, mut git_sync)) = + sqlx::query_as::<_, (serde_json::Value, Option)>( + "SELECT git_credentials, git_sync FROM workspace_settings WHERE workspace_id = $1 FOR UPDATE", + ) + .bind(w_id) + .fetch_optional(&mut *conn) + .await? + else { + return Ok(()); + }; + let reencrypt = |value: &mut serde_json::Value| { + let Some(ciphertext) = value.as_str() else { + return; + }; + match decrypt(old, ciphertext.to_string()) { + Ok(plain) => *value = serde_json::Value::String(encrypt(new, &plain)), + // Left by an earlier rotation and unrecoverable either way; failing here + // would block every later rotation of the workspace. + Err(e) => tracing::warn!( + "a git-sync secret of workspace {w_id} does not decrypt under its current key, leaving it as is: {e}" + ), + } + }; + for entry in credentials.as_array_mut().into_iter().flatten() { + if let Some(token) = entry.get_mut("token") { + reencrypt(token); + } + } + let repositories = git_sync + .as_mut() + .and_then(|g| g.get_mut("repositories")) + .and_then(|r| r.as_array_mut()); + for repo in repositories.into_iter().flatten() { + if let Some(secret) = repo.pointer_mut("/auto_pull/webhook_secret") { + reencrypt(secret); + } + } + sqlx::query( + "UPDATE workspace_settings SET git_credentials = $2, git_sync = $3 WHERE workspace_id = $1", + ) + .bind(w_id) + .bind(credentials) + .bind(git_sync) + .execute(&mut *conn) + .await?; + Ok(()) +} + #[derive(Serialize)] struct UsedTriggers { pub websocket_used: bool, @@ -6087,7 +6198,7 @@ async fn user_workspaces( workspace.is_dev_workspace, workspace.dev_workspace_label, workspace.owner AS \"created_by?\", CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings, - usr.disabled + usr.disabled, usr.is_service_account FROM workspace JOIN usr ON usr.workspace_id = workspace.id JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id @@ -6908,7 +7019,8 @@ async fn update_workspace_settings( ducklake = source_ws.ducklake, dbt_warehouses = source_ws.dbt_warehouses, datatable = source_ws.datatable, - git_app_installations = source_ws.git_app_installations + git_app_installations = source_ws.git_app_installations, + add_admins_and_developers_to_forks = source_ws.add_admins_and_developers_to_forks FROM workspace_settings source_ws WHERE source_ws.workspace_id = $1 AND workspace_settings.workspace_id = $2 @@ -7049,14 +7161,21 @@ async fn copy_workspace_members( tx: &mut Transaction<'_, Postgres>, source_workspace_id: &str, target_workspace_id: &str, + admins_and_developers_only: bool, ) -> Result<()> { + // Admins and developers join as manual members: the fork does not inherit the source's + // instance-group config, so a copied `instance_group` provenance would let the fork's + // reconciliation delete them and their data. sqlx::query!( "INSERT INTO usr (workspace_id, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via) - SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account, added_via + SELECT $1, username, email, is_admin, created_at, operator, disabled, role, is_service_account, + CASE WHEN $3 THEN NULL ELSE added_via END FROM usr WHERE workspace_id = $2 + AND (NOT $3 OR (NOT operator AND NOT disabled AND NOT is_service_account)) ON CONFLICT DO NOTHING", target_workspace_id, source_workspace_id, + admins_and_developers_only, ) .execute(&mut **tx) .await?; @@ -7473,6 +7592,127 @@ async fn clear_orphaned_compat_address( Ok(()) } +/// SQL boolean: the principal the `principal` expression yields resolves in the workspace bound as +/// `$1`. The same predicate `clone_scripts` and `clone_flows` inline, whose `query!` macros cannot +/// take a composed string, so keep the three in step. +fn principal_resolves_sql(principal: &str) -> String { + format!( + "CASE WHEN {principal} LIKE 'u/%' THEN EXISTS ( + SELECT 1 FROM usr u WHERE u.workspace_id = $1 + AND u.username = substring({principal} from 3) + UNION ALL + SELECT 1 FROM password pw WHERE pw.super_admin + AND (pw.username = substring({principal} from 3) + OR pw.email = substring({principal} from 3))) + WHEN {principal} LIKE 'g/%' THEN EXISTS ( + SELECT 1 FROM group_ g WHERE g.workspace_id = $1 + AND g.name = substring({principal} from 3)) + ELSE EXISTS ( + SELECT 1 FROM usr u WHERE u.workspace_id = $1 AND u.username = {principal} + UNION ALL + SELECT 1 FROM password pw WHERE pw.email = {principal} AND pw.super_admin) + END" + ) +} + +/// Re-point the identities a fork clones verbatim at its creator when they name nobody in the fork, +/// once its membership is final so copied members keep theirs. Unlike scripts and flows these +/// cannot drop the identity: an app deploy rejects a preserved one that does not resolve, and +/// publisher apps, schedules and triggers need one to run. +async fn repoint_unresolvable_cloned_identities( + tx: &mut Transaction<'_, Postgres>, + target_workspace_id: &str, + authed: &ApiAuthed, +) -> Result<()> { + let principal = username_to_permissioned_as(&authed.username); + + sqlx::query(&format!( + "UPDATE app SET policy = policy + || jsonb_build_object('on_behalf_of', $2::text, 'on_behalf_of_email', $3::text) + WHERE workspace_id = $1 AND policy->>'on_behalf_of' IS NOT NULL + AND NOT ({})", + principal_resolves_sql("(policy->>'on_behalf_of')") + )) + .bind(target_workspace_id) + .bind(&principal) + .bind(&authed.email) + .execute(&mut **tx) + .await?; + + // A draft holding a genuine NUL escape (the rule of `json_text_has_nul_escape`) is left as it + // is, since parsing it would abort the fork. The check must stay in a CASE: json `->>` raises on + // a NUL anywhere in the value, and Postgres reorders plain AND conditions. + let nul_escape = r"(^|[^\\])(\\\\)*\\u0000"; + sqlx::query(&format!( + "UPDATE draft SET value = to_json(jsonb_set(jsonb_set(to_jsonb(value), + ARRAY['policy', 'on_behalf_of'], to_jsonb($2::text)), + ARRAY['policy', 'on_behalf_of_email'], to_jsonb($3::text))) + WHERE workspace_id = $1 AND typ IN ('app', 'raw_app') + AND CASE WHEN value::text ~ $4 THEN false + ELSE value->'policy'->>'on_behalf_of' IS NOT NULL AND NOT ({}) END", + principal_resolves_sql("(value->'policy'->>'on_behalf_of')") + )) + .bind(target_workspace_id) + .bind(&principal) + .bind(&authed.email) + .bind(nul_escape) + .execute(&mut **tx) + .await?; + + sqlx::query(&format!( + "UPDATE draft SET value = to_json(jsonb_set(to_jsonb(value), + ARRAY['permissioned_as'], to_jsonb($2::text))) + WHERE workspace_id = $1 AND starts_with(typ::text, 'trigger_') + AND CASE WHEN value::text ~ $3 THEN false + ELSE value->>'permissioned_as' IS NOT NULL AND NOT ({}) END", + principal_resolves_sql("(value->>'permissioned_as')") + )) + .bind(target_workspace_id) + .bind(&principal) + .bind(nul_escape) + .execute(&mut **tx) + .await?; + + let column_resolves = principal_resolves_sql("permissioned_as"); + + // SAFETY: every table name is a literal from this list, never user input. + for table in [ + "http_trigger", + "websocket_trigger", + "kafka_trigger", + "nats_trigger", + "postgres_trigger", + "mqtt_trigger", + "amqp_trigger", + "sqs_trigger", + "gcp_trigger", + "azure_trigger", + "email_trigger", + ] { + sqlx::query(&format!( + "UPDATE {table} SET permissioned_as = $2 + WHERE workspace_id = $1 AND NOT ({column_resolves})" + )) + .bind(target_workspace_id) + .bind(&principal) + .execute(&mut **tx) + .await?; + } + + // `email` is still written for workers that predate `permissioned_as`. + sqlx::query(&format!( + "UPDATE schedule SET permissioned_as = $2, email = $3 + WHERE workspace_id = $1 AND NOT ({column_resolves})" + )) + .bind(target_workspace_id) + .bind(&principal) + .bind(&authed.email) + .execute(&mut **tx) + .await?; + + Ok(()) +} + /// Carries over the recorded principal under the rule spelled out on [`clone_scripts`]. async fn clone_flows( tx: &mut Transaction<'_, Postgres>, @@ -7918,14 +8158,32 @@ async fn clone_drafts( // filtered like `clone_scripts`: the address the draft still carries re-derives the // clone's own principal at deploy time, which is the more accurate answer of the two. sqlx::query!( - "INSERT INTO draft (workspace_id, path, typ, value, created_at, email) + // A script hash is content-addressed and copied as-is, so a script draft's base + // still names a version the clone has. `clone_flows` / `clone_apps` mint new ids, + // so those drafts arrive with no base (staleness falls back to the timestamps), + // lineage field included, or the next autosave would re-derive the source id. + // + // `clean` is `strip_json_nul`'s parity rule in SQL, so a pre-sanitizer U+0000 + // escape cannot abort the clone on `to_jsonb` or arrive with its principal + // unstripped: escaped backslashes park on chr(1) (lossless, a `json` value's text + // cannot hold a raw control byte) so only a real NUL is removed, and chr(92) + // spells the backslash so no escape sequence reaches this source file. + r#"INSERT INTO draft (workspace_id, path, typ, value, created_at, email, base) SELECT $2, path, typ, - CASE WHEN typ IN ('script', 'flow') - THEN to_json(to_jsonb(value) - 'on_behalf_of') - ELSE value END, - created_at, email - FROM draft - WHERE workspace_id = $1 AND (email = $3 OR email IS NULL)", + to_json( + CASE WHEN typ IN ('script', 'flow') THEN clean - 'on_behalf_of' ELSE clean END + - CASE WHEN typ = 'flow' THEN 'version_id' + WHEN typ IN ('app', 'raw_app') THEN 'parent_version' + ELSE '' END + ), + created_at, email, + CASE WHEN typ = 'script' THEN base END + FROM ( + SELECT d.path, d.typ, d.created_at, d.email, d.base, + replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean + FROM draft d + WHERE d.workspace_id = $1 AND (d.email = $3 OR d.email IS NULL) + ) s"#, source_workspace_id, target_workspace_id, authed_email, @@ -8744,8 +9002,19 @@ async fn create_workspace_fork( // intended. Dev creation is already admin-gated, so this is transitively admin-only too. Done before // the explicit creator insert below so the creator (a parent member) is copied with full metadata // (operator/role/is_service_account/added_via), not the bare row the insert alone would leave. + // Independently, the parent's admins can have every fork of it start with its admins and + // developers; the forker cannot opt out, since the point is that those admins can review it. if nw.copy_members && nw.is_dev_workspace { - copy_workspace_members(&mut tx, &parent_workspace_id, &forked_id).await?; + copy_workspace_members(&mut tx, &parent_workspace_id, &forked_id, false).await?; + } else if sqlx::query_scalar!( + "SELECT add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $1", + parent_workspace_id + ) + .fetch_optional(&mut *tx) + .await? + .unwrap_or(false) + { + copy_workspace_members(&mut tx, &parent_workspace_id, &forked_id, true).await?; } // Ensure the creator is a member of the fork even without copy_members (or if they aren't a parent @@ -8807,6 +9076,8 @@ async fn create_workspace_fork( // re-enables in the fork, with parent-conflict warnings on enable. clone_triggers_and_schedules(&mut tx, &parent_workspace_id, &forked_id).await?; + repoint_unresolvable_cloned_identities(&mut tx, &forked_id, &authed).await?; + // Update forked datatable settings to point to new databases for fdt in &nw.forked_datatables { apply_forked_datatable(&db, &mut tx, &authed, &parent_workspace_id, &forked_id, fdt) diff --git a/backend/windmill-api-workspaces/src/workspaces_extra.rs b/backend/windmill-api-workspaces/src/workspaces_extra.rs index c325d7730d..434636f15d 100644 --- a/backend/windmill-api-workspaces/src/workspaces_extra.rs +++ b/backend/windmill-api-workspaces/src/workspaces_extra.rs @@ -118,7 +118,7 @@ pub(crate) async fn change_workspace_id( // Duplicate workspace settings (keep copy in old workspace for reference) info!("Duplicating workspace_settings table"); sqlx::query!( - "INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url FROM workspace_settings WHERE workspace_id = $2", + "INSERT INTO workspace_settings (workspace_id, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks) SELECT $1, slack_team_id, slack_name, slack_command_script, slack_email, customer_id, plan, webhook, ai_config, large_file_storage, git_sync, default_app, default_scripts, deploy_ui, mute_critical_alerts, color, operator_settings, teams_command_script, teams_team_id, teams_team_name, git_app_installations, git_credentials, ducklake, dbt_warehouses, slack_oauth_client_id, slack_oauth_client_secret, datatable, teams_team_guid, auto_invite, error_handler, success_handler, public_app_execution_limit_per_minute, error_handler_fallback_to_instance_alerts, guest_access_enabled, guest_jwt_public_key, guest_jwt_jwks_url, add_admins_and_developers_to_forks FROM workspace_settings WHERE workspace_id = $2", &rw.new_id, &old_id ) @@ -387,6 +387,13 @@ pub(crate) async fn change_workspace_id( ) .execute(&mut *tx) .await?; + sqlx::query!( + "UPDATE draft_move SET workspace_id = $1 WHERE workspace_id = $2", + &rw.new_id, + &old_id + ) + .execute(&mut *tx) + .await?; info!("Updating favorite table"); sqlx::query!( diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index b9f34d084a..8fa4acd1ae 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.814.0 + version: 1.815.0 title: Windmill API contact: @@ -529,6 +529,30 @@ paths: responses: "302": description: redirected to the post-login destination, or to /user/login_link_expired when the link is used, expired or unknown + post: + security: [] + summary: consume a single-use login link from its confirmation page, set the session cookie and answer where to go + operationId: confirmLoginLink + tags: + - user + parameters: + - name: token + in: path + required: true + schema: + type: string + responses: + "200": + description: the post-login destination, or /user/login_link_expired when the link is used, expired or unknown + content: + application/json: + schema: + type: object + required: + - location + properties: + location: + type: string /auth/reset_password: post: @@ -4103,9 +4127,13 @@ paths: guest_access_enabled: type: boolean description: Whether this workspace admits guest sessions. An app's own `guest` execution mode is inert while this is false. + add_admins_and_developers_to_forks: + type: boolean + description: Whether every new fork of this workspace starts with its admins and developers as members, keeping their role. required: - workspace_id - guest_access_enabled + - add_admins_and_developers_to_forks /w/{workspace}/workspaces/get_settings: get: @@ -4196,6 +4224,9 @@ paths: guest_jwt_jwks_url: type: string description: JWKS URL a guest JWT (`jwt_guest_`) is verified against for this workspace. Mutually exclusive with `guest_jwt_public_key`. + add_admins_and_developers_to_forks: + type: boolean + description: Whether every new fork of this workspace starts with its admins and developers as members, keeping their role. /w/{workspace}/workspaces/get_deploy_to: get: @@ -6437,6 +6468,39 @@ paths: schema: type: string + /w/{workspace}/workspaces/edit_add_admins_and_developers_to_forks: + post: + summary: choose whether new forks of this workspace start with its admins and developers + description: >- + When on, every fork created from this workspace gets the workspace's admins and + developers as members, with the role they hold here; operators, disabled users and + service accounts are left out. The setting is copied into each fork, so forks of a + fork follow it too. Off by default. Workspace-admin gated. + operationId: editAddAdminsAndDevelopersToForks + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: Whether new forks start with this workspace's admins and developers + required: true + content: + application/json: + schema: + type: object + properties: + add_admins_and_developers_to_forks: + type: boolean + required: + - add_admins_and_developers_to_forks + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + /w/{workspace}/workspaces/edit_guest_jwt_key: post: summary: set the key guest JWTs are verified against for this workspace @@ -6901,7 +6965,7 @@ paths: type: string expires_in_s: type: integer - description: link lifetime in seconds, at most 900 (default 600) + description: link lifetime in seconds, at most 7200 (default 600) rd: type: string description: same-origin path the browser lands on after login (default /user/workspaces) @@ -6911,6 +6975,12 @@ paths: mint only while the account still has this login type (for example pending_oauth), so a link stops working once the owner has set a password or signed in with a provider + confirm: + type: boolean + description: >- + return a /user/login_link page that signs in only when its button is + clicked, instead of a link spent by opening it; set it for links sent by + email, which mail scanners open on delivery (default false) responses: "201": description: login link minted @@ -10470,8 +10540,49 @@ paths: current_timestamp: type: string format: date-time + path: + type: string + description: "`saved` only, upsert or delete: where the write landed. Differs from the URL path when the item had moved away from it; the editor follows it there. Absent when a delete found nothing to remove and the caller cannot read the path it moved to." required: [status, current_timestamp] + /w/{workspace}/drafts/move/{kind}/{path}: + post: + summary: move the current user's draft-only item to another path + description: Relocates the authed user's own draft row, along with both path keys inside its value (the typed path and the mirror the editors keep beside it). Only for draft-only items — a deployed item must be moved through its own deploy endpoint, which carries every draft with it. Restricted to script, flow, app and raw_app; any other kind is rejected with 400, because only these keep their deploy target where this endpoint rewrites it. + operationId: moveDraft + tags: + - draft + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: kind + in: path + required: true + description: script, flow, app or raw_app only. + schema: + type: string + enum: [script, flow, app, raw_app] + - $ref: "#/components/parameters/ScriptPath" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + new_path: + type: string + summary: + type: string + description: Also restate the draft's summary. + required: [new_path] + responses: + "200": + description: move result + content: + text/plain: + schema: + type: string + /w/{workspace}/drafts/migrate_legacy/{kind}/{path}: post: summary: resolve a legacy (workspace-level) draft (admin only) @@ -10982,6 +11093,8 @@ paths: parameters: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" responses: "200": description: script history @@ -12387,6 +12500,8 @@ paths: parameters: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" tags: - flow responses: @@ -14265,6 +14380,8 @@ paths: parameters: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" responses: "200": description: app history @@ -14532,9 +14649,9 @@ paths: "200": description: app updated content: - text/plain: + application/json: schema: - type: string + $ref: "#/components/schemas/AppDeployed" /w/{workspace}/apps/create_raw_source: post: @@ -14674,9 +14791,9 @@ paths: "200": description: app updated content: - text/plain: + application/json: schema: - type: string + $ref: "#/components/schemas/AppDeployed" /w/{workspace}/apps/update_raw/{path}: post: @@ -14730,9 +14847,9 @@ paths: "200": description: app updated content: - text/plain: + application/json: schema: - type: string + $ref: "#/components/schemas/AppDeployed" /w/{workspace}/apps/custom_path_exists/{custom_path}: get: @@ -27780,6 +27897,13 @@ components: draft_saved_at: type: string format: date-time + draft_base: + type: string + description: | + The deployed version the draft forked from, as text whatever the + kind (script hash, flow version id, app version id). Compare to the + deployed head to tell a draft that is behind. Absent when there is + no draft or it was never forked from a deploy. no_deployed: type: boolean draft: @@ -29675,6 +29799,11 @@ components: type: string deployment_msg: type: string + created_at: + type: string + format: date-time + created_by: + type: string required: - script_hash @@ -35169,6 +35298,9 @@ components: nullable: true disabled: type: boolean + is_service_account: + type: boolean + description: Whether this membership is a service account. required: - id - name @@ -35858,6 +35990,20 @@ components: - raw_app + AppDeployed: + type: object + description: "What a deploy of an existing app answers with. `version` is the one this call wrote, which is what an editor pins as the fork base of the draft it starts next: reading the head back afterwards cannot tell it from a deploy that landed beside it. A metadata-only update writes none and reports the head it kept." + properties: + path: + type: string + description: Where the app now lives, which differs from the request path on a rename. + version: + type: integer + format: int64 + required: + - path + - version + AppHistory: type: object properties: @@ -35865,6 +36011,11 @@ components: type: integer deployment_msg: type: string + created_at: + type: string + format: date-time + created_by: + type: string required: - version @@ -35934,6 +36085,8 @@ components: format: date-time deployment_msg: type: string + created_by: + type: string required: - id - created_at diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 918fce53bf..432af1bef0 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -69,8 +69,8 @@ use windmill_common::{ user_drafts::{overlay_or_draft_only, DraftUserRef, UserDraftItemKind, WithDraftOverlay}, users::username_to_permissioned_as, utils::{ - http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin, - strip_json_nul, Pagination, RunnableKind, StripPath, + http_get_from_hub, not_found_if_none, paginate, paginate_optional, + query_elems_from_hub, require_admin, strip_json_nul, Pagination, RunnableKind, StripPath, }, variables::{build_crypt, build_crypt_with_key_suffix, encrypt}, worker::{to_raw_value, CLOUD_HOSTED}, @@ -274,6 +274,12 @@ pub struct AppHistory { pub version: i64, #[serde(skip_serializing_if = "Option::is_none")] pub deployment_msg: Option, + /// Who deployed this version, and when — the diff's version picker names them so + /// a reader can tell their own deploys from a teammate's. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_by: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option>, } #[derive(Deserialize)] @@ -567,6 +573,17 @@ pub struct CreateApp { pub skip_draft_deletion: Option, } +/// What a deploy of an existing app answers with. `version` is the one this call wrote, +/// which is what an editor pins as the fork base of the draft it starts next: reading the +/// head back afterwards cannot tell it from a deploy that landed beside it. A +/// metadata-only update writes none and reports the head it kept. +#[derive(Serialize)] +pub struct AppDeployed { + /// Where the app now lives, which differs from the request path on a rename. + pub path: String, + pub version: i64, +} + #[derive(Serialize, Deserialize)] pub struct EditApp { pub path: Option, @@ -1220,18 +1237,42 @@ async fn get_app_history( authed: ApiAuthed, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, + Query(pagination): Query, ) -> JsonResult> { let path = path.to_path(); check_scopes(&authed, || format!("apps:read:{}", &path))?; + // Unasked-for, this listing stays whole: the deployment-history panel reads it + // without paging. The diff picker asks for a page. + let (per_page, offset) = paginate_optional(pagination); let mut tx = user_db.begin(&authed).await?; + // Newest first in deployed order, which is a version's position in `app.versions` and + // not its `created_at`: the latter is the deploying transaction's start time, so two + // that overlap can carry it in the opposite order from the one they landed in. A row + // outside the array never sat in that sequence, so it sorts after the ones that did. + // Paging happens before the metadata joins, so a page costs its own rows. let query_result = sqlx::query!( - "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg - FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version + "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg, + av.created_by as created_by, av.created_at as created_at + FROM app a + JOIN LATERAL ( + SELECT av2.id, COALESCE(v.ord, 0) AS ord + FROM app_version av2 + LEFT JOIN unnest(a.versions) WITH ORDINALITY AS v(id, ord) ON v.id = av2.id + WHERE av2.app_id = a.id + ORDER BY ord DESC, av2.id DESC + LIMIT $3 OFFSET $4 + ) page ON TRUE + JOIN app_version av ON av.id = page.id + LEFT JOIN deployment_metadata dm ON av.id = dm.app_version WHERE a.workspace_id = $1 AND a.path = $2 - ORDER BY created_at DESC", + ORDER BY page.ord DESC", w_id, path, - ).fetch_all(&mut *tx).await?; + per_page, + offset, + ) + .fetch_all(&mut *tx) + .await?; tx.commit().await?; let result: Vec = query_result @@ -1240,6 +1281,8 @@ async fn get_app_history( app_id: row.app_id, version: row.version_id, deployment_msg: row.deployment_msg, + created_by: Some(row.created_by), + created_at: Some(row.created_at), }) .collect(); return Ok(Json(result)); @@ -1253,14 +1296,22 @@ async fn get_latest_version( let path = path.to_path(); check_scopes(&authed, || format!("apps:read:{}", path))?; let mut tx = user_db.begin(&authed).await?; + // The head is the tail of `app.versions` — the version the runtime serves. Deploys + // append to it under the app row's lock, whereas `app_version.created_at` is the + // deploying transaction's start time, so two that overlap can carry it in either + // order and the newest timestamp is then not the one that landed last. let row = sqlx::query!( - "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg - FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version - WHERE a.workspace_id = $1 AND a.path = $2 - ORDER BY created_at DESC", + "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg, + av.created_by as created_by, av.created_at as created_at + FROM app a JOIN app_version av + ON av.id = a.versions[array_upper(a.versions, 1)] AND av.app_id = a.id + LEFT JOIN deployment_metadata dm ON av.id = dm.app_version + WHERE a.workspace_id = $1 AND a.path = $2", w_id, path, - ).fetch_optional(&mut *tx).await?; + ) + .fetch_optional(&mut *tx) + .await?; tx.commit().await?; if let Some(row) = row { @@ -1268,6 +1319,8 @@ async fn get_latest_version( app_id: row.app_id, version: row.version_id, deployment_msg: row.deployment_msg, + created_by: Some(row.created_by), + created_at: Some(row.created_at), }; return Ok(Json(Some(result))); @@ -2591,6 +2644,14 @@ async fn create_app_internal<'a>( .execute(&mut *tx) .await?; } + windmill_common::user_drafts::clear_draft_moves_from( + &mut tx, + &w_id, + &[UserDraftItemKind::App, UserDraftItemKind::RawApp], + &app.path, + None, + ) + .await?; let id = sqlx::query_scalar!( "INSERT INTO app (workspace_id, path, summary, policy, versions, custom_path, labels) @@ -2947,7 +3008,7 @@ async fn update_app( Extension(webhook): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, -) -> Result { +) -> JsonResult { if authed.is_operator { return Err(Error::NotAuthorized( "Operators cannot update apps for security reasons".to_string(), @@ -2986,7 +3047,7 @@ async fn update_app( }, ); - Ok(format!("app {} updated (npath: {:?})", opath, npath)) + Ok(Json(AppDeployed { path: npath, version: v_id })) } /// Deploy a raw app from its sources, compiling them on a worker. `update_raw` @@ -3000,7 +3061,7 @@ async fn update_app_raw_source( Extension(webhook): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, -) -> Result { +) -> JsonResult { if authed.is_operator { return Err(Error::NotAuthorized( "Operators cannot update apps for security reasons".to_string(), @@ -3098,7 +3159,7 @@ async fn update_app_raw_source( }, ); - Ok(format!("app {} updated (npath: {:?})", opath, npath)) + Ok(Json(AppDeployed { path: npath, version: v_id })) } /// Whether the caller may create an app at `path` — asked of the database rather @@ -3324,7 +3385,7 @@ async fn update_app_raw<'a>( Extension(webhook): Extension, Path((w_id, path)): Path<(String, StripPath)>, multipart: Multipart, -) -> Result { +) -> JsonResult { if authed.is_operator { return Err(Error::NotAuthorized( "Operators cannot update apps for security reasons".to_string(), @@ -3372,7 +3433,7 @@ async fn update_app_raw<'a>( }, ); - Ok(format!("app {} updated (npath: {:?})", opath, npath)) + Ok(Json(AppDeployed { path: npath, version: v_id })) } // async fn create_app_internal<'a>( // authed: ApiAuthed, @@ -3749,6 +3810,19 @@ async fn update_app_internal<'a>( .execute(&mut *tx) .await?; } + if npath != path { + // Everything left at the old path is a draft this deploy didn't consume + // — teammates' rows, and the deployer's own when the caller asked us to + // keep it. Carry them rather than strand them. + windmill_common::user_drafts::move_drafts_for_path( + &mut tx, + &w_id, + &[UserDraftItemKind::App, UserDraftItemKind::RawApp], + path, + &npath, + ) + .await?; + } audit_log( &mut *tx, &authed, diff --git a/backend/windmill-api/src/drafts.rs b/backend/windmill-api/src/drafts.rs index e5928f761e..8bcbb56384 100644 --- a/backend/windmill-api/src/drafts.rs +++ b/backend/windmill-api/src/drafts.rs @@ -19,7 +19,7 @@ use windmill_common::{ error::{Error, Result}, user_drafts::{DraftUserRef, UserDraftItemKind, ENCRYPTED_DRAFT_PREFIX}, users::resolve_username_to_email, - utils::strip_json_nul, + utils::{check_proper_path, strip_json_nul}, variables::{build_crypt, encrypt}, }; @@ -29,6 +29,7 @@ pub fn workspaced_service() -> Router { .route("/get/{kind}/{*path}", get(get_draft_for_user)) .route("/get_own/{kind}/{*path}", get(get_own_draft)) .route("/update/{kind}/{*path}", post(update_draft)) + .route("/move/{kind}/{*path}", post(move_draft)) .route("/migrate_legacy/{kind}/{*path}", post(migrate_legacy_draft)) } @@ -320,12 +321,71 @@ pub struct SaveDraftResponse { /// On `saved`: when the change was applied (client remembers it as the /// next `last_sync`). On `conflict`: the existing row's `created_at`. pub current_timestamp: chrono::DateTime, + /// `saved` only: where the write landed. Differs from the URL path when the item + /// had moved away from it; the editor follows it there. Absent when a delete found + /// nothing to remove and the caller cannot read the path it moved to. + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, } -/// Apply the current user's draft at (workspace, kind, path): non-null `value` -/// upserts, `null` (or omitted) deletes. Either way, when the existing row is -/// newer than `last_sync` (and `force` is false) the op is skipped and the -/// response is `status = conflict` + the server's current timestamp. +/// The version a draft forked from, as the editors write it into `draft.value`. +/// Each kind names it differently and only one is ever set. +#[derive(Deserialize)] +struct DraftBaseVersion { + /// Scripts: hex-encoded script hash. + #[serde(default)] + parent_hash: Option, + /// Flows: `flow_version.id`. + #[serde(default)] + version_id: Option, + /// Apps / raw apps: `app_version.id`. + #[serde(default)] + parent_version: Option, +} + +impl DraftBaseVersion { + /// The base as the `draft.base` column stores it: one opaque text id whatever + /// the kind, so a reader compares it to the head without knowing the kind's + /// own field name or type. + fn as_text(&self, kind: UserDraftItemKind) -> Option { + use UserDraftItemKind::*; + match kind { + Script => self.parent_hash.clone(), + Flow => self.version_id.map(|v| v.to_string()), + _ => self.parent_version.map(|v| v.to_string()), + } + } +} + +/// The version this draft forked from, or `None` when it has none — a kind +/// that keeps no lineage, a malformed payload, or a draft that was never forked +/// from a deploy. Pure: no queries. +fn draft_lineage(kind: UserDraftItemKind, value: &str) -> Option { + use UserDraftItemKind::*; + if !matches!(kind, Script | Flow | App | RawApp) { + return None; + } + let base = serde_json::from_str::(value).ok()?; + let has_base = match kind { + Script => base + .parent_hash + .as_deref() + .and_then(|h| windmill_common::scripts::to_i64(h).ok()) + .is_some(), + Flow => base.version_id.is_some(), + _ => base.parent_version.is_some(), + }; + has_base.then_some(base) +} + +/// Apply the current user's draft: non-null `value` upserts, `null` (or +/// omitted) deletes. Either way, when the existing row is newer than +/// `last_sync` (and `force` is false) the op is skipped and the response is +/// `status = conflict` + the server's current timestamp. +/// +/// A save addressed to a path its item moved away from lands where the move took +/// the drafts (`draft_move`), unless the caller still has a draft of their own at +/// that path. The response names where it landed. async fn update_draft( authed: ApiAuthed, Extension(db): Extension, @@ -334,7 +394,7 @@ async fn update_draft( Json(req): Json, ) -> Result> { let email = &authed.email; - let path = path.to_path(); + let url_path = path.to_path(); // Saving a draft requires write permission on the underlying path. Deleting // (discarding) one's OWN draft does not: the email-scoped row belongs to the // authed user, so they can always discard it even after losing write access @@ -343,11 +403,68 @@ async fn update_draft( // touch the caller's own row. Legacy (NULL-email) rows aren't owned by anyone // — they keep the write gate. let is_own_discard = req.value.is_none() && !req.legacy; + // `legacy` targets the workspace-level row and is delete-only: an upsert writes the + // caller's own row whatever it says. Every read of that rule goes through this. + let legacy_delete = req.value.is_none() && req.legacy; + + // Whose row this write is for: the caller's, or the workspace-level one on a legacy + // DELETE (`legacy` is delete-only, so an upsert is the caller's own row either way). + // It picks both the record that applies — an item's move (`email IS NULL`) covers the + // legacy row too, since the same rename carried it — and the draft whose presence + // means this path is still the write's own. + let owner: Option<&str> = (!legacy_delete).then_some(email.as_str()); + // The caller's own draft-only move outranks the move of the deployed item. Only the + // kinds whose value carries a deploy target are ever recorded as moved, so for the + // rest this would be a guaranteed-empty query on the autosave hot path. + let moved_to = match kind.typed_path_field() { + None => None, + Some(_) => { + sqlx::query_scalar!( + r#"SELECT m.new_path FROM draft_move m + WHERE m.workspace_id = $1 AND m.typ = $2 AND m.old_path = $3 + AND (m.email IS NULL OR m.email = $4) + AND NOT EXISTS ( + SELECT 1 FROM draft d + WHERE d.workspace_id = $1 AND d.typ = $2 AND d.path = $3 + AND d.email IS NOT DISTINCT FROM $4 + ) + ORDER BY m.email IS NULL + LIMIT 1"#, + &w_id, + kind as UserDraftItemKind, + url_path, + owner, + ) + .fetch_optional(&db) + .await? + } + }; + let path: &str = moved_to.as_deref().unwrap_or(url_path); + + // Everything past here writes, so the gate applies from here on. Answered + // without the path when the item moved: it may have gone somewhere the caller + // cannot see. if !is_own_discard { - require_can_write_path(&authed, &db, &user_db, &w_id, kind, path).await?; + match require_can_write_path(&authed, &db, &user_db, &w_id, kind, path).await { + // Naming the move is for whoever was editing the item: it tells them why a + // save they were already making stopped landing. Someone who cannot read the + // path they addressed gets the plain denial, or the wording itself would + // answer whether an item was moved away from a path they only guessed at. + Err(Error::NotAuthorized(e)) if moved_to.is_some() => { + return Err(Error::NotAuthorized( + match require_can_read_path(&authed, &user_db, &w_id, kind, url_path).await { + Ok(()) => { + "this draft's item was moved to a path you cannot write".to_string() + } + Err(_) => e, + }, + )); + } + other => other?, + } } - let applied_at = if let Some(value) = &req.value { + let applied = if let Some(value) = &req.value { // Secret variable values must never sit in `draft.value` in plaintext // (see `encrypt_secret_variable_value`). let serialized = if kind == UserDraftItemKind::Variable { @@ -359,19 +476,49 @@ async fn update_draft( // escape and later make any `->>`/`to_jsonb` extraction raise `22P05`. // Strip it here so a NUL never reaches the column. let serialized = strip_json_nul(&serialized); + // `base` is derived here from the value's per-kind field rather than sent + // by the client, so every writer (editors, chat, CLI) fills it the same way. + // Read from the sanitized text, which is what the value column gets: a NUL in the + // lineage field otherwise costs the draft its base (the hash no longer parses), + // leaving a draft that reads as up to date whatever the head is. + let base = draft_lineage(kind, serialized.as_ref()).and_then(|l| l.as_text(kind)); // Upsert. The conflict check rides on the DO UPDATE WHERE clause — // when the row is newer than `last_sync`, RETURNING yields nothing. // `created_at` defaults to `now()` but the migration overrides it ($8) // so a migrated draft keeps its original age instead of jumping to top. - sqlx::query_scalar!( - r#"INSERT INTO draft (workspace_id, email, path, typ, value, created_at) - VALUES ($1, $2, $3, $4, $5::text::json, COALESCE($8::timestamptz, now())) + // + // A moved save ($10) carries the path keys its editor had before the move. + // One naming the path it addressed ($11) follows to where it landed; a draft + // already there keeps the keys the move gave it. A pre-sanitizer NUL escape + // in that draft makes `to_jsonb` raise, so it takes the incoming keys. + sqlx::query!( + r#"INSERT INTO draft (workspace_id, email, path, typ, value, created_at, base) + VALUES ($1, $2, $3::text, $4, + CASE WHEN $10::bool + THEN to_json($5::text::jsonb || jsonb_strip_nulls(jsonb_build_object( + 'path', CASE WHEN $5::text::jsonb -> 'path' = to_jsonb($11::text) + THEN to_jsonb($3::text) END, + 'draft_path', CASE WHEN $5::text::jsonb -> 'draft_path' = to_jsonb($11::text) + THEN to_jsonb($3::text) END))) + ELSE $5::text::json + END, + COALESCE($8::timestamptz, now()), $9) ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL - DO UPDATE SET value = EXCLUDED.value, created_at = EXCLUDED.created_at + DO UPDATE SET value = CASE + WHEN NOT $10::bool + OR position(chr(92) || 'u0000' in replace(draft.value::text, chr(92) || chr(92), '')) > 0 + THEN EXCLUDED.value + ELSE to_json((to_jsonb(EXCLUDED.value) - 'path' - 'draft_path') + || jsonb_strip_nulls(jsonb_build_object( + 'path', to_jsonb(draft.value) -> 'path', + 'draft_path', to_jsonb(draft.value) -> 'draft_path'))) + END, + created_at = EXCLUDED.created_at, + base = EXCLUDED.base WHERE $7::bool = true OR $6::timestamptz IS NULL OR draft.created_at <= $6::timestamptz - RETURNING created_at"#, + RETURNING path, created_at"#, &w_id, email, path, @@ -380,9 +527,13 @@ async fn update_draft( req.last_sync, req.force, req.created_at, + base.as_deref(), + moved_to.is_some(), + url_path, ) .fetch_optional(&db) .await? + .map(|r| (r.created_at, Some(r.path))) } else { // Delete, same conflict rule in the WHERE clause. Returns NULL when // the row was too new (conflict) OR already absent (idempotent) — @@ -403,16 +554,20 @@ async fn update_draft( kind as UserDraftItemKind, req.last_sync, req.force, - req.legacy, + legacy_delete, ) .fetch_optional(&db) .await? + // Named for the same reason an upsert is: the editor that discarded is still on + // the path the item left, and reloading there would land on nothing. + .map(|ts| (ts, moved_to.clone())) }; - if let Some(ts) = applied_at { + if let Some((ts, path)) = applied { return Ok(Json(SaveDraftResponse { status: SaveDraftStatus::Saved, current_timestamp: ts, + path, })); } @@ -428,7 +583,7 @@ async fn update_draft( email, path, kind as UserDraftItemKind, - req.legacy, + legacy_delete, ) .fetch_optional(&db) .await?; @@ -437,20 +592,307 @@ async fn update_draft( Some(ts) => Ok(Json(SaveDraftResponse { status: SaveDraftStatus::Conflict, current_timestamp: ts, + path: None, })), // Delete + nothing-was-there ⇒ report success with server's NOW(). None => { let now = sqlx::query_scalar!(r#"SELECT now() as "now!""#) .fetch_one(&db) .await?; + // A retry of a routed discard whose answer was lost lands here: the row is + // gone but the editor is still on the path the item left, so it needs the + // destination as much as the first attempt did. Unlike the arm above there is + // no deleted row proving the caller ever held that draft, and an own discard + // is not gated, so this names a path to someone who may have none of it: the + // read gate, which is what keeps a path from being disclosed elsewhere in this + // module. Without it, discarding at a guessed path reads `draft_move`. + let disclosed = match moved_to { + Some(dest) => { + match require_can_read_path(&authed, &user_db, &w_id, kind, &dest).await { + Ok(()) => Some(dest), + Err(Error::NotFound(_)) + | Err(Error::NotAuthorized(_)) + | Err(Error::BadRequest(_)) => None, + Err(e) => return Err(e), + } + } + None => None, + }; Ok(Json(SaveDraftResponse { status: SaveDraftStatus::Saved, current_timestamp: now, + path: disclosed, })) } } } +#[derive(Deserialize)] +pub struct MoveDraftRequest { + pub new_path: String, + /// Also restate the draft's summary, so the same drawer that renames a + /// deployed item can retitle a draft-only one. + #[serde(default)] + pub summary: Option, +} + +/// Relocate the authed user's own DRAFT-ONLY item. Such an item is nothing but +/// its draft row, so moving it is a rewrite of that row's path plus both path +/// keys inside its value — there is no deployed row, schedule or trigger to +/// cascade to. +/// +/// The owner's own open editor follows: its next save, still addressed to the old +/// path, lands at the new one through the move record, and it is told where. +/// +/// Scoped to the caller's own row on purpose: two users can each have a draft +/// at the same never-deployed path, and those are two separate items. +/// +/// A DEPLOYED item must move through its own deploy endpoint instead, which +/// cascades everything that references the path and carries every draft along. +async fn move_draft( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path((w_id, kind, path)): Path<(String, UserDraftItemKind, windmill_common::utils::StripPath)>, + Json(req): Json, +) -> Result { + let path = path.to_path(); + let new_path = req.new_path.as_str(); + // Only the full-page editor kinds, which is exactly the set that has a typed + // path to rewrite. Reading the movable set off the same mapping the rewrite + // uses keeps them from drifting apart: a resource, a variable or a trigger + // keeps its deploy target in `value.path` with no editor to stage a rename, + // so moving one would leave the real target naming the old location and the + // next deploy would recreate it where it came from. + let (Some(typed_field), Some(mirror_field)) = + (kind.typed_path_field(), kind.mirror_path_field()) + else { + return Err(Error::BadRequest(format!( + "moving a {kind:?} draft is not supported — only scripts, flows and apps" + ))); + }; + // Validate before authorizing: `require_can_write_path` is not a format check + // (an admin returns immediately, and a user returns early inside their own + // namespace), so without this a malformed path is stored as-is, and an over-long + // or NUL-bearing one reaches Postgres as a raw server error. The source is a URL + // segment and arrives decoded, so it needs the same check as the destination. + check_proper_path(path)?; + check_proper_path(new_path)?; + // A summary-only edit is a legitimate use of this endpoint: the drawer edits + // both fields, and for a draft-only script the path it posts back is the row + // path unchanged (`list_scripts` only reports `draft_path` when it differs). + // Returning early on the path alone would drop the new summary silently. + if new_path == path && req.summary.is_none() { + return Ok("unchanged".to_string()); + } + require_can_write_path(&authed, &db, &user_db, &w_id, kind, path).await?; + if new_path != path { + require_can_write_path(&authed, &db, &user_db, &w_id, kind, new_path).await?; + } + + if let Some(table) = kind.deployed_table() { + // `table` is from the closed `deployed_table()` enum, never user input. + // Archived and soft-deleted rows keep sitting at their path — a script + // move archives its parent in place — so an existence check that counted + // them would refuse a move away from, or into, a path nothing occupies. + // `create_script_internal` resolves its own path clashes the same way. + let archived_filter = if table == "script" { + " AND NOT archived AND NOT deleted" + } else { + "" + }; + let query = format!( + "SELECT 1 FROM {table} WHERE path = $1 AND workspace_id = $2{archived_filter} LIMIT 1" + ); + let mut tx = user_db.clone().begin(&authed).await?; + let deployed_at_old = sqlx::query_scalar::<_, i32>(&query) + .bind(path) + .bind(&w_id) + .fetch_optional(&mut *tx) + .await?; + let deployed_at_new = sqlx::query_scalar::<_, i32>(&query) + .bind(new_path) + .bind(&w_id) + .fetch_optional(&mut *tx) + .await?; + tx.commit().await?; + if deployed_at_old.is_some() { + return Err(Error::BadRequest(format!( + "'{path}' is deployed — move it from its editor so schedules and triggers follow" + ))); + } + if deployed_at_new.is_some() { + return Err(Error::BadRequest(format!( + "'{new_path}' already has a deployed item — moving there would turn this into a draft on top of it" + ))); + } + } + + // A NUL in the summary reaches Postgres as a raw byte and fails the statement with + // an encoding error, so it is dropped here the way `strip_json_nul` drops one from a + // draft value. `json` cannot store it either. + let summary = req.summary.as_ref().map(|s| s.replace('\0', "")); + + // A classic app and a raw app share the `app` table, so a draft of either kind + // occupies the destination for both: deploying there deletes the caller's drafts + // of both kinds, taking the item that lost the collision with it. + let collision_typs: Vec<&str> = match kind { + UserDraftItemKind::App | UserDraftItemKind::RawApp => vec![ + UserDraftItemKind::App.as_str(), + UserDraftItemKind::RawApp.as_str(), + ], + _ => vec![kind.as_str()], + }; + + // One transaction with the move record, so a save addressed to the old path + // never sees the row gone without knowing where it went. + let mut tx = db.begin().await?; + let moved = sqlx::query_scalar!( + r#"UPDATE draft + SET path = $3, + -- Both path keys, not just the typed one: the editors mirror the + -- typed path into the other while it differs from the row's path, + -- and the loaders prefer the mirror — left naming the old location + -- it un-does this move on the next save. `create_missing = false` + -- on both, so a draft carrying only one keeps only one. + value = to_json( + jsonb_set( + jsonb_set( + CASE WHEN $7::text IS NULL THEN to_jsonb(value) + ELSE jsonb_set(to_jsonb(value), ARRAY['summary'], to_jsonb($7::text)) + END, + ARRAY[$5::text], to_jsonb($3::text), false + ), + ARRAY[$8::text], to_jsonb($3::text), false + ) + ) + WHERE workspace_id = $1 + AND path = $2 + AND typ = $4 + AND email = $6 + -- A pre-sanitizer NUL escape makes `to_jsonb` raise 22P05. Excluded + -- here so the statement can't 500; reported below instead. Unlike the + -- passive carry, rewriting the value IS this operation, so skipping it + -- silently would move the row and leave its typed path stale. + AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) = 0 + -- Skipped on a summary-only edit, where the "target" row is this + -- row and the guard would refuse the update against itself. + AND ($2 = $3 OR NOT EXISTS ( + SELECT 1 FROM draft o + WHERE o.workspace_id = $1 AND o.path = $3 AND o.typ::text = ANY($9::text[]) + -- Of this kind only the caller's own row and the legacy one collide: + -- teammates' drafts of one item share its path by design, but a deploy + -- there wipes those two together, so a second would discard edits the + -- caller never saw. The other app kind is a different item on the same + -- deployed path, so it collides whoever owns it. + AND (o.typ <> $4 OR o.email = $6 OR o.email IS NULL) + )) + RETURNING id"#, + &w_id, + path, + new_path, + kind as UserDraftItemKind, + typed_field, + &authed.email, + summary, + mirror_field, + &collision_typs as &[&str], + ) + .fetch_optional(&mut *tx) + .await?; + if moved.is_some() && new_path != path { + windmill_common::user_drafts::record_draft_move( + &mut tx, + &w_id, + &[kind], + path, + new_path, + Some(&authed.email), + ) + .await?; + } + tx.commit().await?; + + if moved.is_none() { + let row = sqlx::query!( + r#"SELECT + -- The guard's own predicate, ordered own row, then legacy, then another + -- user's other-kind row: each needs a different remedy, and a bare + -- LIMIT 1 would name an arbitrary one. + (SELECT typ::text FROM draft WHERE workspace_id = $1 AND path = $3 + AND typ::text = ANY($6::text[]) + AND (typ <> $2 OR email = $4 OR email IS NULL) + ORDER BY CASE WHEN email = $4 THEN 0 WHEN email IS NULL THEN 1 ELSE 2 END + LIMIT 1) as "at_target", + EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $3 + AND typ::text = ANY($6::text[]) AND email = $4) as "at_target_own!", + EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $3 + AND typ::text = ANY($6::text[]) AND email IS NULL) as "at_target_legacy!", + EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $5 + AND typ = $2 AND email = $4 + AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) > 0 + ) as "poisoned!", + EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $5 + AND typ = $2 AND email IS NULL) as "legacy!" "#, + &w_id, + kind as UserDraftItemKind, + new_path, + &authed.email, + path, + &collision_typs as &[&str], + ) + .fetch_one(&db) + .await?; + return Err(Error::BadRequest(if row.poisoned { + // This endpoint also serves a summary-only edit, so name the operation + // the caller actually asked for rather than always saying "moved". + let attempted = if new_path == path { "updated" } else { "moved" }; + format!( + "'{path}' contains a NUL character and predates the sanitizer, so it cannot be \ + {attempted}. Reopen it, re-save to rewrite it cleanly, then retry." + ) + } else if row.legacy { + // The home list synthesizes a draft-only row for the legacy draft with the + // caller's own name on it, so this is reachable from the row menu. Only an + // admin can claim or discard that row, and only from the drafts page. + format!( + "'{path}' is a legacy workspace draft with no owner, so it cannot be moved. \ + A workspace admin can claim or discard it on the Review & deploy drafts page." + ) + } else if let Some(occupant) = row.at_target.filter(|_| new_path != path) { + // Naming the kind matters for the app pair: a classic-app draft refusing a + // raw-app move is invisible in the raw-app list the caller is looking at. + let occupant = occupant.replace('_', " "); + if row.at_target_own { + format!("You already have a draft at '{new_path}' ({occupant})") + } else if row.at_target_legacy { + // An ownerless row the caller cannot clear themselves, so send them to + // the one place it can be resolved rather than to "discard your draft". + format!( + "A legacy workspace draft with no owner is already at '{new_path}' \ + ({occupant}). A workspace admin can claim or discard it on the Review & \ + deploy drafts page." + ) + } else { + // The other app kind, owned by someone else: one deployed path cannot hold + // both, so this is the other item's path, not a teammate's copy of this one. + format!( + "'{new_path}' holds another user's {occupant} draft, and an app and a raw \ + app cannot share a path. Pick another path, or ask them to move or \ + discard theirs." + ) + } + } else { + format!("You have no draft at '{path}'") + })); + } + + if new_path == path { + return Ok(format!("updated draft {path}")); + } + Ok(format!("moved draft {path} to {new_path}")) +} + #[derive(Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum MigrateLegacyDraftAction { @@ -504,12 +946,12 @@ async fn migrate_legacy_draft( r#"WITH legacy AS ( DELETE FROM draft WHERE workspace_id = $1 AND path = $2 AND typ = $3 AND email IS NULL - RETURNING value + RETURNING value, base ) - INSERT INTO draft (workspace_id, email, path, typ, value, created_at) - SELECT $1, $4, $2, $3, value, now() FROM legacy + INSERT INTO draft (workspace_id, email, path, typ, value, created_at, base) + SELECT $1, $4, $2, $3, value, now(), base FROM legacy ON CONFLICT (workspace_id, path, typ, email) WHERE email IS NOT NULL - DO UPDATE SET value = EXCLUDED.value, created_at = now() + DO UPDATE SET value = EXCLUDED.value, created_at = now(), base = EXCLUDED.base RETURNING 1 as "one!""#, &w_id, path, @@ -767,8 +1209,8 @@ async fn require_can_write_path( /// Resolves to `Ok(())` if `authed` can read at `path`. Three layers: /// 1. admin → always. -/// 2. Path-prefix match against own `u/{username}` or any folder in -/// `authed.folders` (the precomputed read set, with groups + direct +/// 2. Path-prefix match against own `u/{username}`, a group in `authed.groups`, or any +/// folder in `authed.folders` (the precomputed read set, with groups + direct /// grants already factored in). /// 3. RLS-aware `SELECT 1` against the backing table — covers item-level /// extra_perms grants that bypass folder/owner checks. @@ -793,6 +1235,10 @@ async fn require_can_read_path( if parts.len() >= 2 { match parts[0] { "u" if parts[1] == authed.username => return Ok(()), + // As `require_can_write_path` and the `see_member` RLS policy read it: a + // draft-only `g/` path has no row for the probe below to fall back on, so + // without this a member cannot see a draft their group owns. + "g" if authed.groups.iter().any(|g| g == parts[1]) => return Ok(()), "f" => { let folder = parts[1]; if authed.folders.iter().any(|(name, _, _)| name == folder) { diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index b70d80140a..0fd5ac31b5 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -560,7 +560,7 @@ pub async fn run_server( if server_mode || mcp_mode { use mcp::{ add_www_authenticate_header, add_www_authenticate_header_gateway, - extract_workspace_from_token, + extract_workspace_from_token, reject_token_query_param, }; let (mcp_router, mcp_cancellation_token) = setup_mcp_server( db.clone(), @@ -573,15 +573,17 @@ pub async fn run_server( let workspaced_mcp_router = mcp_router .clone() .route_layer(from_extractor::()) + .layer(axum::middleware::from_fn(reject_token_query_param)) .layer(axum::middleware::from_fn(add_www_authenticate_header)) .layer(axum::middleware::from_fn(extract_and_store_workspace_id)); // Gateway MCP router — resolves workspace from token let gateway_mcp_router = mcp_router .route_layer(from_extractor::()) + .layer(axum::middleware::from_fn(extract_workspace_from_token)) + .layer(axum::middleware::from_fn(reject_token_query_param)) .layer(axum::middleware::from_fn( add_www_authenticate_header_gateway, - )) - .layer(axum::middleware::from_fn(extract_workspace_from_token)); + )); ( workspaced_mcp_router, gateway_mcp_router, diff --git a/backend/windmill-api/src/mcp/core.rs b/backend/windmill-api/src/mcp/core.rs index 480e3c0841..86fbec1806 100644 --- a/backend/windmill-api/src/mcp/core.rs +++ b/backend/windmill-api/src/mcp/core.rs @@ -43,11 +43,14 @@ use axum::{ extract::{Extension, Path}, http::Request, middleware::Next, - response::Response, + response::{IntoResponse, Response}, routing::get, Json, Router, }; -use windmill_common::{auth::hash_token, db::GatewayWorkspaceId, error::JsonResult}; +use windmill_common::{ + auth::hash_token, db::GatewayWorkspaceId, error::JsonResult, + global_settings::MCP_DISABLE_TOKEN_QUERY_PARAM, +}; // McpAuth impl for ApiAuthed is in windmill-api-auth (same crate as the type) @@ -446,6 +449,29 @@ pub async fn add_www_authenticate_header( } } +/// Middleware refusing a credential carried in the MCP URL once the instance sets +/// `mcp_disable_token_query_param`. Sits outside everything that reads the token, so neither +/// the gateway lookup nor `ApiAuthed` ever sees it, and inside the `WWW-Authenticate` layer, +/// whose header is what sends the client into the OAuth flow instead. Refused rather than +/// ignored: the URL leaked the token whether or not the request used it. +pub async fn reject_token_query_param(request: Request, next: Next) -> Response { + let carries_token = MCP_DISABLE_TOKEN_QUERY_PARAM.load(std::sync::atomic::Ordering::Relaxed) + && request + .uri() + .query() + .is_some_and(|q| url::form_urlencoded::parse(q.as_bytes()).any(|(k, _)| k == "token")); + if carries_token { + return ( + axum::http::StatusCode::UNAUTHORIZED, + "This instance does not accept a token in the MCP URL. Remove the token query \ + parameter and let your client sign in through OAuth, or send the token in an \ + Authorization header.", + ) + .into_response(); + } + next.run(request).await +} + /// Extract the bearer token from either the `Authorization` header or the /// `?token=` query parameter (MCP clients commonly pass it in the URL). fn extract_gateway_token(request: &Request) -> Option { diff --git a/backend/windmill-api/src/mcp/mod.rs b/backend/windmill-api/src/mcp/mod.rs index 5f6bd5edb5..5545d59a9f 100644 --- a/backend/windmill-api/src/mcp/mod.rs +++ b/backend/windmill-api/src/mcp/mod.rs @@ -12,5 +12,5 @@ pub mod oauth_server; pub use core::{ add_www_authenticate_header, add_www_authenticate_header_gateway, extract_and_store_workspace_id, extract_workspace_from_token, list_tools_service, - setup_mcp_server, + reject_token_query_param, setup_mcp_server, }; diff --git a/backend/windmill-common/src/auth.rs b/backend/windmill-common/src/auth.rs index 3e8eb58a14..c12d62f206 100644 --- a/backend/windmill-common/src/auth.rs +++ b/backend/windmill-common/src/auth.rs @@ -20,7 +20,7 @@ use crate::{ /// Whether `label` denotes a user-created token rather than a system token /// (`session`, `guest_session`, `ephemeral*`, `debugger-token`, `mcp-oauth-*`, -/// `embed_app:*`, `sdk_app:*`, `impersonation:*`). System-token labels are load-bearing — +/// `embed_app:*`, `sdk_app:*`, `impersonation:*`, `cli-login:*`). System-token labels are load-bearing — /// session cleanup, super_admin propagation, expiry notifications and username overrides /// all key off them — so they must not be user-editable. `None` (no label) is treated as /// a user token. @@ -47,10 +47,17 @@ pub fn is_user_token(label: Option<&str>) -> bool { && !l.starts_with(APP_EMBED_TOKEN_LABEL_PREFIX) && !l.starts_with(RAW_APP_SDK_TOKEN_LABEL_PREFIX) && !l.starts_with("impersonation:") + && !l.starts_with(CLI_LOGIN_TOKEN_LABEL_PREFIX) } } } +/// How far ahead of a user token's expiration its owner is warned (`check_expiring_tokens` in +/// the monitor). A token whose whole lifetime fits in this window gets no warning at all: it +/// would arrive minutes after creation, about a lifetime its creator just picked. Its +/// "expired and deleted" notice still goes out. +pub const TOKEN_EXPIRY_WARNING_DAYS: i32 = 7; + /// Label prefix, followed by the app path, of the token an app viewer's sandboxed iframe /// runs with. Reserved in [`is_user_token`], whose SQL and frontend mirrors spell it out. pub const APP_EMBED_TOKEN_LABEL_PREFIX: &str = "embed_app:"; @@ -59,6 +66,13 @@ pub const APP_EMBED_TOKEN_LABEL_PREFIX: &str = "embed_app:"; /// frontend SDK. Reserved in [`is_user_token`], whose SQL and frontend mirrors spell it out. pub const RAW_APP_SDK_TOKEN_LABEL_PREFIX: &str = "sdk_app:"; +/// Label prefix, followed by the username, of the token the CLI authorization page mints for +/// `wmill` logins. Reserved in [`is_user_token`], whose SQL and frontend mirrors spell it out: +/// the CLI signs in again on its own once that token expires, so an expiry email for it asks +/// the user to do nothing. Not in [`is_server_minted_label`], since the page mints it through +/// `/users/tokens/create`. +pub const CLI_LOGIN_TOKEN_LABEL_PREFIX: &str = "cli-login:"; + /// Whether `label` belongs to a namespace only the server mints, and which therefore must be /// rejected by `create_token`. Narrower than [`is_user_token`], which also drives label /// editability and expiry notifications and can afford to reserve more: `Ephemeral lsp token`, @@ -475,14 +489,16 @@ async fn fetch_authed_from_permissioned_as_inner( // principal — a username freed and reassigned while its previous holder keeps a privileged // account — would mix one account's role with another's instance privileges. let member = match permissioned_as.split_once('/') { - Some(("u", name)) => sqlx::query!( - "SELECT is_admin, operator, email FROM usr where username = $1 AND \ + Some(("u", name)) => { + sqlx::query!( + "SELECT is_admin, operator, email FROM usr where username = $1 AND \ workspace_id = $2 AND disabled = false", - name, - &w_id - ) - .fetch_optional(&mut *conn) - .await?, + name, + &w_id + ) + .fetch_optional(&mut *conn) + .await? + } _ => None, }; let resolved_email; @@ -980,6 +996,7 @@ mod tests { assert!(!is_user_token(Some("embed_app:f/team/dashboard"))); assert!(!is_user_token(Some("sdk_app:u/admin/raw app"))); assert!(!is_user_token(Some("impersonation:admin@windmill.dev"))); + assert!(!is_user_token(Some("cli-login:admin"))); } #[test] diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index 7e69c4c866..34ec76d0bb 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -104,6 +104,49 @@ pub const HUB_API_SECRET_SETTING: &str = "hub_api_secret"; pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation"; pub const DISABLE_WORKSPACE_INVITE_EMAILS_SETTING: &str = "disable_workspace_invite_emails"; pub const DISABLE_PASSWORD_LOGIN_SETTING: &str = "disable_password_login"; +/// Refuse `?token=` on the MCP endpoints, leaving the `Authorization` header as the only way +/// in. A URL-borne credential ends up in browser history, proxy logs and referrers, so an +/// instance that cares sends MCP clients through the OAuth flow instead. +pub const MCP_DISABLE_TOKEN_QUERY_PARAM_SETTING: &str = "mcp_disable_token_query_param"; +/// Ceiling, in days, on how far ahead a token minted through `POST /users/tokens/create` or +/// `POST /users/tokens/impersonate` may expire; a request asking for more, or for no +/// expiration at all, is shortened to it rather than refused. On those routes only: server-side +/// mints (webhook tokens, app embed tokens, sessions) choose a lifetime the caller never picks +/// and go straight to `create_token_internal`. Read and validated by +/// [`parse_max_token_expiration_days`]. +pub const MAX_TOKEN_EXPIRATION_DAYS_SETTING: &str = "max_token_expiration_days"; +/// Largest `max_token_expiration_days` read as a ceiling, about 2,700 years. The token form +/// applies the same bound (`frontend/src/lib/tokenExpiration.ts`) so that it and the server +/// agree on whether a ceiling exists. +pub const MAX_TOKEN_EXPIRATION_DAYS_BOUND: i64 = 1_000_000; + +/// Reads a stored `max_token_expiration_days`: `Ok(None)` when unset or cleared (null or an +/// empty string), the ceiling for a whole number of days within +/// `1..=MAX_TOKEN_EXPIRATION_DAYS_BOUND` stored as an integer, an integral float or a string of +/// digits, and an error for anything else. +/// +/// The settings API and config sync both reject the error at write time: the token routes can +/// only read an unparseable value as no ceiling, so accepting a typo would silently turn the +/// policy off. `parseMaxTokenExpirationDays` in the frontend must accept exactly the same values. +pub fn parse_max_token_expiration_days( + value: Option<&serde_json::Value>, +) -> Result, String> { + let days = match value { + None | Some(serde_json::Value::Null) => return Ok(None), + Some(serde_json::Value::String(s)) if s.trim().is_empty() => return Ok(None), + Some(serde_json::Value::Number(n)) => n + .as_i64() + .or_else(|| n.as_f64().filter(|f| f.fract() == 0.0).map(|f| f as i64)), + Some(serde_json::Value::String(s)) => s.trim().parse::().ok(), + Some(_) => None, + }; + match days { + Some(days) if (1..=MAX_TOKEN_EXPIRATION_DAYS_BOUND).contains(&days) => Ok(Some(days)), + _ => Err(format!( + "must be a whole number of days from 1 to {MAX_TOKEN_EXPIRATION_DAYS_BOUND}, or empty for no limit" + )), + } +} pub const AUTO_LOGIN_PROVIDER_SETTING: &str = "auto_login_provider"; /// Name of the SAML attribute or OIDC userinfo claim carrying the user's IdP groups. Unset or /// empty leaves instance-group membership entirely to SCIM. @@ -373,6 +416,7 @@ use std::sync::atomic::AtomicBool; lazy_static::lazy_static! { pub static ref HTTP_ROUTE_WORKSPACED_ROUTE: AtomicBool = AtomicBool::new(false); pub static ref DISABLE_PASSWORD_LOGIN: AtomicBool = AtomicBool::new(false); + pub static ref MCP_DISABLE_TOKEN_QUERY_PARAM: AtomicBool = AtomicBool::new(false); /// Origins HTTP routes allow cross-origin when they configure none of their /// own. Empty means unset, which keeps the historical `*`. pub static ref HTTP_ROUTE_DEFAULT_ALLOWED_ORIGINS: arc_swap::ArcSwap> = @@ -816,6 +860,55 @@ pub fn workspace_integration_auth_endpoint(client_name: &str, base_url: &str) -> mod tests { use super::*; + // `frontend/src/lib/tokenExpiration.test.ts` holds the same table for the token form's + // parser; the two must stay in step. + #[test] + fn max_token_expiration_days_accepts_only_whole_days_within_the_bound() { + use serde_json::json; + for (stored, days) in [ + (json!(7), 7), + (json!(7.0), 7), + (json!("7"), 7), + (json!(" 30 "), 30), + (json!("+7"), 7), + ( + json!(MAX_TOKEN_EXPIRATION_DAYS_BOUND), + MAX_TOKEN_EXPIRATION_DAYS_BOUND, + ), + ] { + assert_eq!( + parse_max_token_expiration_days(Some(&stored)), + Ok(Some(days)), + "{stored}" + ); + } + for cleared in [json!(null), json!(""), json!(" ")] { + assert_eq!( + parse_max_token_expiration_days(Some(&cleared)), + Ok(None), + "{cleared}" + ); + } + assert_eq!(parse_max_token_expiration_days(None), Ok(None)); + for bad in [ + json!(7.5), + json!(0), + json!(-3), + json!("7.0"), + json!("1e1"), + json!("0x7"), + json!(MAX_TOKEN_EXPIRATION_DAYS_BOUND + 1), + json!("99999999999999999999"), + json!(true), + json!([7]), + ] { + assert!( + parse_max_token_expiration_days(Some(&bad)).is_err(), + "{bad} must be rejected" + ); + } + } + #[test] fn webhook_base_url_errors_never_echo_credentials() { // These strings reach sync-config output and operator logs, so no branch may diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index c7bac6b160..09d84b959f 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -1398,6 +1398,10 @@ pub async fn sync_global_settings_declarative( crate::global_settings::parse_allowed_origins_setting(desired.get(origins_key)) .map_err(|e| anyhow::anyhow!("{origins_key}: {e}"))?; + let max_expiration_key = crate::global_settings::MAX_TOKEN_EXPIRATION_DAYS_SETTING; + crate::global_settings::parse_max_token_expiration_days(desired.get(max_expiration_key)) + .map_err(|e| anyhow::anyhow!("{max_expiration_key}: {e}"))?; + let diff = diff_global_settings(current, desired, ApplyMode::Replace); let external_pg_key = crate::global_settings::EXTERNAL_INSTANCE_PG_SETTING; if diff.deletes.iter().any(|k| k == external_pg_key) { diff --git a/backend/windmill-common/src/sensitive_log_masks.rs b/backend/windmill-common/src/sensitive_log_masks.rs index b6f6262b77..c999297ec0 100644 --- a/backend/windmill-common/src/sensitive_log_masks.rs +++ b/backend/windmill-common/src/sensitive_log_masks.rs @@ -10,7 +10,7 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; -use std::sync::RwLock; +use std::sync::{Arc, RwLock}; use uuid::Uuid; /// Minimum length for a secret to be registered for masking. @@ -20,9 +20,27 @@ const MIN_SECRET_LENGTH: usize = 8; const MASKED_NOTICE: &str = "[windmill] secret value was masked for security reasons, use string transformations to display full value"; +/// The secrets registered for one job, plus the automaton compiled from them. +#[derive(Default)] +struct JobMasks { + secrets: HashSet, + /// Built on the first `snapshot` after a change and shared by every later + /// snapshot. Every job registers at least its own token, so without this + /// cache each log batch of each job would rebuild the automaton. + compiled: Option>, +} + +/// Aho-Corasick automaton for O(m) multi-pattern matching in a single pass, +/// regardless of the number of secrets registered, with the replacement +/// strings indexed to match the automaton's pattern order. +struct CompiledMasks { + ac: aho_corasick::AhoCorasick, + replacements: Vec, +} + lazy_static::lazy_static! { - /// Map of job_id -> set of secret values that should be masked in that job's logs. - static ref SENSITIVE_MASKS: RwLock>> = + /// Map of job_id -> secret values that should be masked in that job's logs. + static ref SENSITIVE_MASKS: RwLock> = RwLock::new(HashMap::new()); /// Set of currently running job IDs on this worker process. @@ -32,13 +50,8 @@ lazy_static::lazy_static! { } /// A lock-free snapshot of secrets for a job, taken once per log batch. -/// Uses Aho-Corasick for O(m) multi-pattern matching in a single pass, -/// regardless of the number of secrets registered. pub struct MaskSnapshot { - /// Aho-Corasick automaton for fast matching. - ac: aho_corasick::AhoCorasick, - /// Replacement strings, indexed to match the automaton's pattern order. - replacements: Vec, + compiled: Arc, /// Whether the security notice has already been appended for this snapshot. /// Tracked locally to avoid a global write lock on every masked line. notice_shown: std::cell::Cell, @@ -53,34 +66,104 @@ impl MaskSnapshot { } // Single-pass check + replace using the pre-built automaton - if !self.ac.is_match(text) { + if !self.compiled.ac.is_match(text) { return Cow::Borrowed(text); } - let mut result = self.ac.replace_all(text, &self.replacements); + let mut result = self + .compiled + .ac + .replace_all(text, &self.compiled.replacements); - // Append the notice only once per snapshot (i.e. per batch) + // Append the notice only once per snapshot (i.e. per batch), as its own line. + // Callers pass either a bare line (`handle_child`) or a chunk that already ends + // in a newline (nativets), and the sinks concatenate what they get verbatim: + // assuming either shape welds the notice onto a neighbouring line. if !self.notice_shown.get() { self.notice_shown.set(true); - result.push('\n'); - result.push_str(MASKED_NOTICE); + if result.ends_with('\n') { + result.push_str(MASKED_NOTICE); + result.push('\n'); + } else { + result.push('\n'); + result.push_str(MASKED_NOTICE); + } } Cow::Owned(result) } } +/// A masker for sinks that mask line by line rather than in batches, like nativets +/// masking each `console.log` chunk as V8 produces it. `snapshot` per line would +/// re-arm the security notice on every one; this keeps it to once per distinct set +/// of secrets while still picking up secrets registered mid-run. +/// +/// Masks by job id alone — the caller is the one that knows the text it passes +/// belongs to that job. +pub struct JobMasker { + job_id: Uuid, + snapshot: Option, +} + +impl JobMasker { + pub fn new(job_id: Uuid) -> Self { + JobMasker { job_id, snapshot: snapshot(&job_id) } + } + + /// Mask every secret registered for the job. Returns `Cow::Borrowed` when no match. + /// Falls back to the masks it last saw once the job is unregistered, so a sink + /// still draining past the end of a run does not start emitting secrets. + pub fn mask<'a>(&mut self, text: &'a str) -> Cow<'a, str> { + if let Some(fresh) = snapshot(&self.job_id) { + // Replacing an equivalent snapshot would re-arm the notice, so only take + // one built from a secret set we have not seen. + let unchanged = self + .snapshot + .as_ref() + .is_some_and(|cur| Arc::ptr_eq(&cur.compiled, &fresh.compiled)); + if !unchanged { + self.snapshot = Some(fresh); + } + } + match self.snapshot.as_ref() { + Some(snapshot) => snapshot.mask(text), + None => Cow::Borrowed(text), + } + } +} + /// Take a snapshot of the current secrets for a job. Returns `None` if no secrets /// are registered (the caller can then skip masking entirely for the whole batch). /// /// Call this once per log batch in `write_lines`, not per line. pub fn snapshot(job_id: &Uuid) -> Option { - let masks = SENSITIVE_MASKS.read().unwrap_or_else(|e| e.into_inner()); - let secrets = masks.get(job_id)?; - if secrets.is_empty() { - return None; + { + let masks = SENSITIVE_MASKS.read().unwrap_or_else(|e| e.into_inner()); + let job = masks.get(job_id)?; + if job.secrets.is_empty() { + return None; + } + if let Some(compiled) = job.compiled.as_ref() { + return Some(MaskSnapshot { + compiled: compiled.clone(), + notice_shown: std::cell::Cell::new(false), + }); + } } + let mut masks = SENSITIVE_MASKS.write().unwrap_or_else(|e| e.into_inner()); + let job = masks.get_mut(job_id)?; + if job.secrets.is_empty() { + return None; + } + let compiled = job + .compiled + .get_or_insert_with(|| Arc::new(compile(&job.secrets))); + Some(MaskSnapshot { compiled: compiled.clone(), notice_shown: std::cell::Cell::new(false) }) +} + +fn compile(secrets: &HashSet) -> CompiledMasks { // Sort longest-first so longer secrets are matched before shorter substrings let mut sorted: Vec<&String> = secrets.iter().collect(); sorted.sort_by(|a, b| b.len().cmp(&a.len())); @@ -106,7 +189,7 @@ pub fn snapshot(job_id: &Uuid) -> Option { .build(sorted.iter().map(|s| s.as_str())) .expect("failed to build aho-corasick automaton"); - Some(MaskSnapshot { ac, replacements, notice_shown: std::cell::Cell::new(false) }) + CompiledMasks { ac, replacements } } /// Register a job as currently running. Call this before `handle_queued_job`. @@ -148,20 +231,110 @@ pub fn register_secret_for_all_running_jobs(secret: &str) { let mut masks = SENSITIVE_MASKS.write().unwrap_or_else(|e| e.into_inner()); for job_id in job_ids { - if let Some(set) = masks.get_mut(&job_id) { - set.insert(secret.to_string()); + if let Some(job) = masks.get_mut(&job_id) { + if job.secrets.insert(secret.to_string()) { + job.compiled = None; + } } } } /// Register a secret value for a specific job. -/// Used for `$encrypted:` args where we know the job ID. +/// Used for the job's own token and for `$encrypted:` args, where we know the job ID. pub fn register_secret_for_job(job_id: Uuid, secret: &str) { if secret.len() < MIN_SECRET_LENGTH { return; } let mut masks = SENSITIVE_MASKS.write().unwrap_or_else(|e| e.into_inner()); - if let Some(set) = masks.get_mut(&job_id) { - set.insert(secret.to_string()); + if let Some(job) = masks.get_mut(&job_id) { + if job.secrets.insert(secret.to_string()) { + job.compiled = None; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The compiled automaton is cached per job, so a secret registered after the + /// first snapshot only gets masked if the cache is invalidated. + #[test] + fn snapshot_rebuilds_after_a_new_secret_is_registered() { + let job_id = Uuid::new_v4(); + register_running_job(job_id); + register_secret_for_job(job_id, "firstsecretvalue"); + let _ = snapshot(&job_id) + .expect("secret registered") + .mask("firstsecretvalue"); + + register_secret_for_job(job_id, "secondsecretvalue"); + + let snap = snapshot(&job_id).expect("secrets registered"); + let masked = snap.mask("firstsecretvalue then secondsecretvalue"); + assert!(!masked.contains("firstsecretvalue"), "{masked}"); + assert!(!masked.contains("secondsecretvalue"), "{masked}"); + unregister_running_job(job_id); + } + + /// A line-by-line sink must not repeat the notice on every line, and must still + /// pick up a secret registered after the masker was built. + #[test] + fn job_masker_notices_once_per_secret_set() { + let job_id = Uuid::new_v4(); + register_running_job(job_id); + register_secret_for_job(job_id, "firstsecretvalue"); + let mut masker = JobMasker::new(job_id); + + let first = masker.mask("saw firstsecretvalue").into_owned(); + assert!(!first.contains("firstsecretvalue"), "{first}"); + assert!(first.contains(MASKED_NOTICE), "{first}"); + + let second = masker.mask("saw firstsecretvalue again").into_owned(); + assert!(!second.contains("firstsecretvalue"), "{second}"); + assert!(!second.contains(MASKED_NOTICE), "{second}"); + + register_secret_for_job(job_id, "secondsecretvalue"); + let third = masker.mask("saw secondsecretvalue").into_owned(); + assert!(!third.contains("secondsecretvalue"), "{third}"); + unregister_running_job(job_id); + } + + /// Unregistration must not turn masking off under a sink that is still emitting: + /// the masker keeps working off the masks it last saw rather than going quiet. + #[test] + fn job_masker_masks_after_the_job_is_unregistered() { + let job_id = Uuid::new_v4(); + register_running_job(job_id); + register_secret_for_job(job_id, "supersecretvalue"); + let mut masker = JobMasker::new(job_id); + + unregister_running_job(job_id); + + let masked = masker.mask("logged supersecretvalue here"); + assert!(!masked.contains("supersecretvalue"), "{masked}"); + } + + /// The notice has to end up on a line of its own for both shapes callers pass: + /// a bare line (`handle_child`) and a newline-terminated chunk (nativets). The + /// sinks concatenate what they are given verbatim, so getting this wrong welds + /// the notice onto whichever line follows it. + #[test] + fn notice_lands_on_its_own_line_for_both_caller_shapes() { + let job_id = Uuid::new_v4(); + register_running_job(job_id); + register_secret_for_job(job_id, "supersecretvalue"); + + let line = snapshot(&job_id) + .expect("secret registered") + .mask("tok supersecretvalue"); + assert_eq!(line, format!("tok s*****e\n{MASKED_NOTICE}")); + + let chunk = snapshot(&job_id) + .expect("secret registered") + .mask("tok supersecretvalue\n"); + assert_eq!(chunk, format!("tok s*****e\n{MASKED_NOTICE}\n")); + + unregister_running_job(job_id); } } diff --git a/backend/windmill-common/src/user_drafts.rs b/backend/windmill-common/src/user_drafts.rs index aed9b5c5de..ebd0ce363f 100644 --- a/backend/windmill-common/src/user_drafts.rs +++ b/backend/windmill-common/src/user_drafts.rs @@ -160,6 +160,34 @@ impl UserDraftItemKind { } } + /// The `draft.value` key holding the user-typed target path — where a + /// deploy of this draft would land when the user staged a rename. A script + /// draft round-trips its own `path`; flows and apps write a separate + /// `draft_path`, and only when it differs from the row's path. + /// + /// `None` for every kind with no editor to stage a rename in — a resource, + /// a variable, a trigger. Their drafts carry no such key, so `Some` is also + /// the test for whether a kind can be moved at all. + pub fn typed_path_field(&self) -> Option<&'static str> { + use UserDraftItemKind::*; + match self { + Script => Some("path"), + Flow | App | RawApp => Some("draft_path"), + _ => None, + } + } + + /// The other of the two path keys a draft value can carry. The editors mirror + /// the typed path into it while it differs from the row's path, so a move that + /// rewrote only `typed_path_field` would leave the mirror naming the old + /// location — and the loaders prefer the mirror, which un-does the move. + pub fn mirror_path_field(&self) -> Option<&'static str> { + match self.typed_path_field()? { + "path" => Some("draft_path"), + _ => Some("path"), + } + } + /// Whether OTHER users' drafts at a path are visible to a viewer (the /// "others are editing" list, owner circles, and the `get_draft_for_user` /// View JSON / Fork endpoint). Enabled only for the full-page editor items @@ -212,6 +240,12 @@ pub struct WithDraftOverlay { pub is_draft: bool, #[serde(skip_serializing_if = "Option::is_none")] pub draft_saved_at: Option>, + /// The deployed version the draft forked from (`draft.base`), as text + /// whatever the kind. The editor compares it to the head it loaded to tell + /// a draft that is behind. Absent when there is no draft or it was never + /// forked from a deploy. + #[serde(skip_serializing_if = "Option::is_none")] + pub draft_base: Option, /// True when no deployed row exists at this path: `inner` is only a /// best-effort stand-in synthesized from the draft and only `draft` is /// canonical. Frontend uses this to disable "diff/reset vs deployed" and @@ -297,6 +331,7 @@ where inner: Box::new(deployed), is_draft: false, draft_saved_at: None, + draft_base: None, no_deployed: false, draft: None, other_drafts_users: Vec::new(), @@ -317,7 +352,7 @@ where // row when an owned one exists. let row = sqlx::query!( r#"SELECT value as "value!: sqlx::types::Json>", - created_at + created_at, base FROM draft WHERE workspace_id = $1 AND (email = $2 OR email IS NULL) @@ -338,6 +373,7 @@ where inner: Box::new(deployed), is_draft: false, draft_saved_at: None, + draft_base: None, no_deployed: false, draft: None, other_drafts_users, @@ -350,6 +386,7 @@ where inner: Box::new(deployed), is_draft: true, draft_saved_at: Some(row.created_at), + draft_base: row.base, no_deployed: false, draft: Some(draft_json), other_drafts_users, @@ -468,7 +505,8 @@ pub async fn overlay_or_draft_only( } } -/// Delete the drafts an address owns, across every workspace. +/// Delete the drafts an address owns, and the move records routing saves to them, +/// across every workspace. /// /// `draft.email` carries no foreign key to `password`: a draft's owner is any principal the /// instance authenticates, and an external JWT's subject never has a `password` row. Deleting an @@ -479,12 +517,14 @@ pub async fn overlay_or_draft_only( /// No authorization of its own: it acts instance-wide on whatever address it is handed, so the /// caller must already have authorized removing that account (superadmin, the account's own /// holder, or SCIM). -pub async fn delete_drafts_of_email<'c>( - executor: impl sqlx::PgExecutor<'c>, - email: &str, -) -> Result<()> { +pub async fn delete_drafts_of_email(conn: &mut sqlx::PgConnection, email: &str) -> Result<()> { sqlx::query!("DELETE FROM draft WHERE email = $1", email) - .execute(executor) + .execute(&mut *conn) + .await?; + // The records routing saves to those drafts go with them: one left behind would send + // a later holder of the address to a draft that no longer exists. + sqlx::query!("DELETE FROM draft_move WHERE email = $1", email) + .execute(&mut *conn) .await?; Ok(()) } @@ -526,6 +566,29 @@ pub async fn rename_drafts_of_email( ) .execute(&mut *conn) .await?; + // The records that route saves to those drafts follow the same address, or a save + // still addressed to a path the account moved away from would start a second draft + // there. The moving account's record for a path wins, as its draft just did. + sqlx::query!( + "DELETE FROM draft_move dest + WHERE dest.email = $1 + AND EXISTS (SELECT 1 FROM draft_move src + WHERE src.email = $2 + AND src.workspace_id = dest.workspace_id + AND src.old_path = dest.old_path + AND src.typ = dest.typ)", + new_email, + old_email + ) + .execute(&mut *conn) + .await?; + sqlx::query!( + "UPDATE draft_move SET email = $1 WHERE email = $2", + new_email, + old_email + ) + .execute(&mut *conn) + .await?; Ok(()) } @@ -582,6 +645,247 @@ pub async fn delete_own_draft_for_path( Ok(()) } +/// Carry every draft at `old_path` over to `new_path` when an item MOVES +/// (rename or relocation). A draft is bound to its item by nothing but the path +/// string, so without this a move detaches every draft on the item. No owner +/// filter: teammates' rows and the legacy NULL-email row follow too. +/// +/// **The caller must have authorized the underlying item move first.** This rewrites +/// rows the caller has no permission on and enforces nothing itself, taking both paths +/// on trust: it is safe only inside a deploy that has already cleared them, and is a +/// cross-user write with no gate anywhere else. +/// +/// The value keeps its base version, so every carried draft reads as behind the head the +/// move minted. Of its two path keys, one still naming `old_path` follows the row (the +/// editors write the item's own path there on every save, so it is the item's path and +/// not a staged rename); any other value is a rename the user staged, and is kept. +/// +/// A draft already at `new_path` occupies it the way a deployed item does, so the move is +/// refused with `BadRequest` inside the deploy's transaction, refusing the rename itself: +/// moving onto it would merge two items or strand the row that lost. A row a `draft_move` +/// record already brought there from `old_path` is this rename's own draft and does not +/// count: deploying it would otherwise be refused against itself, with no way out. +pub async fn move_drafts_for_path( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + w_id: &str, + kinds: &[UserDraftItemKind], + old_path: &str, + new_path: &str, +) -> Result<()> { + let typs = kinds.iter().map(|k| k.as_str()).collect::>(); + // Named by workspace username, as the editors name other users' drafts: the + // caller is often not the owner, and cannot clear a draft they cannot find. + let owners = sqlx::query!( + r#"SELECT d.email IS NULL as "legacy!", COALESCE(u.username, p.username) as username + FROM draft d + LEFT JOIN usr u ON u.workspace_id = d.workspace_id AND u.email = d.email + LEFT JOIN password p ON p.email = d.email AND p.super_admin = true + WHERE d.workspace_id = $1 AND d.path = $2 AND d.typ::text = ANY($3::text[]) + -- A row a move already brought here from the very path being renamed is this + -- rename's own draft, not an item in its way: the record says which owners + -- it moved (all of them for an item move, one for a draft-only move). Any + -- other row is a second item and still collides. + AND NOT EXISTS ( + SELECT 1 FROM draft_move m + WHERE m.workspace_id = $1 AND m.typ::text = ANY($3::text[]) + AND m.old_path = $4 AND m.new_path = $2 + AND (m.email IS NULL OR m.email IS NOT DISTINCT FROM d.email) + ) + ORDER BY 2"#, + w_id, + new_path, + &typs as &[&str], + old_path, + ) + .fetch_all(&mut **tx) + .await?; + if !owners.is_empty() { + let names = owners + .into_iter() + .map(|o| match (o.username, o.legacy) { + (Some(name), _) => name, + (None, true) => "a legacy workspace draft".to_string(), + (None, false) => "another user".to_string(), + }) + .collect::>() + .join(", "); + return Err(crate::error::Error::BadRequest(format!( + "'{new_path}' already has a draft on it ({names}) — it must be moved or discarded first" + ))); + } + // `draft.value` is `json`, so a NUL escape left by a pre-sanitizer write makes + // `to_jsonb` raise 22P05. `clean` is `strip_json_nul`'s parity rule in SQL (escaped + // backslashes parked on chr(1), which a `json` value's text cannot hold, so nothing + // collides with it; chr(92) spells the backslash so no escape sequence reaches this + // file). A row whose keys need re-pointing is therefore rewritten clean rather than + // left naming the old path, and one that needs nothing keeps its value byte for byte. + sqlx::query!( + r#"UPDATE draft AS d + SET path = $3::text, + value = ( + SELECT CASE + WHEN s.clean -> 'path' = to_jsonb($2::text) + OR s.clean -> 'draft_path' = to_jsonb($2::text) + THEN to_json( + s.clean + || CASE WHEN s.clean -> 'path' = to_jsonb($2::text) + THEN jsonb_build_object('path', $3::text) + ELSE '{}'::jsonb END + || CASE WHEN s.clean -> 'draft_path' = to_jsonb($2::text) + THEN jsonb_build_object('draft_path', $3::text) + ELSE '{}'::jsonb END + ) + ELSE d.value + END + FROM (SELECT replace(replace(replace(d.value::text, chr(92) || chr(92), chr(1)), + chr(92) || 'u0000', ''), chr(1), chr(92) || chr(92))::jsonb AS clean) s + ) + WHERE d.workspace_id = $1 + AND d.path = $2::text + AND d.typ::text = ANY($4::text[])"#, + w_id, + old_path, + new_path, + &typs as &[&str], + ) + .execute(&mut **tx) + .await?; + record_draft_move(tx, w_id, kinds, old_path, new_path, None).await +} + +/// Record that the drafts at `old_path` now live at `new_path`, so a draft save still +/// addressed to `old_path` lands on them (see `update_draft`). `email` scopes the +/// record to one user's draft-only move; `None` is a deployed item's move, for everyone. +/// +/// Kept to one hop: records pointing at `old_path` are re-pointed (an owner's move +/// leaves the item's own record alone and copies it into its own scope instead), and +/// records leaving either path are replaced, since `new_path` now holds the item. +/// +/// **The caller must have authorized the move first.** A record routes every later +/// draft write at `old_path` (any owner's, for an item move), and enforces nothing +/// itself. +/// +/// A record outlives the editors that need it: it ends when a later move touches either +/// path or an item is deployed at `old_path`, so a save meaning to start a NEW draft at a +/// vacated path would be routed instead. Nothing does that today: every surface parks a +/// new item at a minted `u//draft_` key (`mintDraftPath.ts`). +pub async fn record_draft_move( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + w_id: &str, + kinds: &[UserDraftItemKind], + old_path: &str, + new_path: &str, + email: Option<&str>, +) -> Result<()> { + let typs = kinds.iter().map(|k| k.as_str()).collect::>(); + sqlx::query!( + "DELETE FROM draft_move + WHERE workspace_id = $1 AND typ::text = ANY($2::text[]) + AND old_path IN ($3, $4) AND ($5::text IS NULL OR email = $5)", + w_id, + &typs as &[&str], + old_path, + new_path, + email, + ) + .execute(&mut **tx) + .await?; + // Routes that ended at the destination before this move describe drafts that were + // carried there for an item this one is replacing: left alive, a save addressed to + // the start of that chain would land on this move's draft instead. + sqlx::query!( + "DELETE FROM draft_move + WHERE workspace_id = $1 AND typ::text = ANY($2::text[]) + AND new_path = $4 AND old_path <> $3 AND ($5::text IS NULL OR email = $5)", + w_id, + &typs as &[&str], + old_path, + new_path, + email, + ) + .execute(&mut **tx) + .await?; + sqlx::query!( + "UPDATE draft_move SET new_path = $4 + WHERE workspace_id = $1 AND typ::text = ANY($2::text[]) + AND new_path = $3 AND ($5::text IS NULL OR email = $5)", + w_id, + &typs as &[&str], + old_path, + new_path, + email, + ) + .execute(&mut **tx) + .await?; + // An owner's move must not re-point what everyone else follows, so the item's own + // records ending at `old_path` are copied into this scope: a save addressed to the + // start of that chain still reaches this destination in one hop. Only those: another + // user's owner-scoped record routes their own draft-only item, which this move has + // nothing to do with. + if email.is_some() { + sqlx::query!( + "INSERT INTO draft_move (workspace_id, typ, old_path, new_path, email) + SELECT m.workspace_id, m.typ, m.old_path, $4, $5::text + FROM draft_move m + WHERE m.workspace_id = $1 AND m.typ::text = ANY($2::text[]) + AND m.new_path = $3 AND m.email IS NULL + AND NOT EXISTS ( + SELECT 1 FROM draft_move o + WHERE o.workspace_id = m.workspace_id AND o.typ = m.typ + AND o.old_path = m.old_path AND o.email = $5::text + )", + w_id, + &typs as &[&str], + old_path, + new_path, + email, + ) + .execute(&mut **tx) + .await?; + } + sqlx::query!( + "INSERT INTO draft_move (workspace_id, typ, old_path, new_path, email) + SELECT $1, t::draft_kind, $3, $4, $5 FROM unnest($2::text[]) t", + w_id, + &typs as &[&str], + old_path, + new_path, + email, + ) + .execute(&mut **tx) + .await?; + Ok(()) +} + +/// Drop the move records at `path`: an item was just created there, so saves addressed to +/// it are its own (records leaving `path`). A deploy that is not a rename also drops the +/// records arriving, which point at an item that no longer owns the path; `keep_from`, +/// the path a rename came from, suppresses that, since the chain ending here is the one +/// this very deploy just wrote. +/// +/// **The caller must have authorized the deploy that created the item first.** +/// Dropping a record sends later draft writes at `path` back to `path`. +pub async fn clear_draft_moves_from( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + w_id: &str, + kinds: &[UserDraftItemKind], + path: &str, + keep_from: Option<&str>, +) -> Result<()> { + let typs = kinds.iter().map(|k| k.as_str()).collect::>(); + sqlx::query!( + "DELETE FROM draft_move WHERE workspace_id = $1 AND typ::text = ANY($2::text[]) + AND (old_path = $3 OR (new_path = $3 AND $4::text IS NULL))", + w_id, + &typs as &[&str], + path, + keep_from, + ) + .execute(&mut **tx) + .await?; + Ok(()) +} + /// Fetch the authed user's draft as a standalone payload, for "get by path" /// routes when no deployed row exists but a draft might. Returns it as a /// `WithDraftOverlay` with `inner` and `draft` both set to the same JSON and @@ -600,7 +904,7 @@ pub async fn fetch_draft_only( // Own draft first, legacy NULL-email row as fallback (see `maybe_overlay_draft`). let row = sqlx::query!( r#"SELECT value as "value!: sqlx::types::Json>", - created_at + created_at, base FROM draft WHERE workspace_id = $1 AND (email = $2 OR email IS NULL) @@ -631,6 +935,7 @@ pub async fn fetch_draft_only( inner: Box::new(draft_json.clone()), is_draft: true, draft_saved_at: Some(row.created_at), + draft_base: row.base, no_deployed: true, draft: Some(draft_json), other_drafts_users, diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index fd0bd634ea..29fc9e99f3 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -479,6 +479,28 @@ pub fn paginate(pagination: Pagination) -> (usize, usize) { (per_page, offset) } +/// [`paginate`] for a listing that answers whole unless a size is asked for: the deploy +/// histories, which the history panels and the CLI read unpaged while the diff picker takes +/// a page at a time. An asked-for size is still clamped, and the offset saturates rather +/// than wrapping, so no caller can turn this into an unbounded scan or a negative bind. +pub fn paginate_optional(pagination: Pagination) -> (i64, i64) { + // Naming neither parameter asks for the whole listing, the contract these endpoints + // have always answered on. Naming either makes it a page like any other listing's. + if pagination.page.is_none() && pagination.per_page.is_none() { + return (i64::MAX, 0); + } + let per_page = pagination + .per_page + .unwrap_or(DEFAULT_PER_PAGE) + .clamp(1, MAX_PER_PAGE) as i64; + // Bound before Postgres sees it: an unchecked cast of a caller-controlled page becomes + // a negative OFFSET, which is an error rather than an empty page. + let offset = i64::try_from(pagination.page.unwrap_or(1).max(1) - 1) + .unwrap_or(i64::MAX) + .saturating_mul(per_page); + (per_page, offset) +} + pub fn paginate_without_limits(pagination: Pagination) -> (usize, usize) { let per_page = pagination.per_page.unwrap_or(MAX_PER_PAGE); let offset = (pagination.page.unwrap_or(1).max(1) - 1) * per_page; @@ -1672,6 +1694,31 @@ pub fn truncate_with_ellipsis(s: &str, max_chars: usize) -> String { mod tests { use super::*; + #[test] + fn test_paginate_optional_answers_whole_but_bounds_what_is_asked_for() { + // Nothing asked for: every row there can be, which is what the history panels and + // the CLI read. + assert_eq!( + paginate_optional(Pagination { page: None, per_page: None }), + (i64::MAX, 0) + ); + assert_eq!( + paginate_optional(Pagination { page: Some(3), per_page: Some(20) }), + (20, 40) + ); + // An asked-for size is still capped, so no caller turns this into an unbounded scan. + assert_eq!( + paginate_optional(Pagination { page: None, per_page: Some(usize::MAX) }), + (MAX_PER_PAGE as i64, 0) + ); + // A page nobody could mean lands past the end rather than going negative, which + // Postgres would reject outright. + let (per_page, offset) = + paginate_optional(Pagination { page: Some(usize::MAX), per_page: Some(20) }); + assert_eq!(per_page, 20); + assert_eq!(offset, i64::MAX); + } + /// A 5-field crontab line is the most common way to get a schedule rejected, and both /// parsers report it in terms a crontab user cannot act on, so the seconds field and the /// equivalent expression must reach the caller for v1 and v2 alike. diff --git a/backend/windmill-common/src/workspaces.rs b/backend/windmill-common/src/workspaces.rs index ed54b6f481..390461650e 100644 --- a/backend/windmill-common/src/workspaces.rs +++ b/backend/windmill-common/src/workspaces.rs @@ -184,7 +184,7 @@ pub enum ObjectType { DatatableMigration, } -pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/28969/sync-script-to-git-repo-windmill"; +pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/28971/sync-script-to-git-repo-windmill"; /// Hub script that applies a repository's state back into a workspace /// (the repo → Windmill / "pull" direction). Same script the UI runs from @@ -192,7 +192,7 @@ pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/28969/sync-script-to-git-repo /// ignores the slug, so the slug is kept free of characters that would be /// percent-encoded into the run URL (a `:` becomes `%3A`, which some hardened /// reverse proxies reject as double-encoding when the client re-encodes it). -pub const GIT_SYNC_PULL_SCRIPT_PATH: &str = "hub/28957/git-sync-init-repository-windmill"; +pub const GIT_SYNC_PULL_SCRIPT_PATH: &str = "hub/28970/git-sync-init-repository-windmill"; /// Prefix used to identify fork workspaces. A workspace whose id starts with this string is a /// fork of another workspace. diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index ab91d54287..429701f7e0 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -4650,6 +4650,30 @@ pub fn tag_reads_args(tag: &str) -> bool { RE_ARG_TAG.is_match(tag) } +/// Whether the tag reads the flow's state (`$flow_expr[results.a.foo]`), which only the flow +/// runtime can resolve, right before pushing the step. A malformed placeholder counts too, so it +/// is rejected or dropped instead of queueing the job on its literal text. +pub fn tag_reads_flow_expr(tag: &str) -> bool { + tag.contains("$flow_expr[") +} + +/// Renders the value at the dotted `path` below `root` as a dynamic tag component, shared by +/// `$args[...]` and `$flow_expr[...]`: its JSON text with surrounding quotes trimmed, and empty +/// once a segment is missing. Only object keys are followed, never array indexes. +pub fn render_tag_path(root: Option<&RawValue>, path: &str) -> String { + let mut value = root.map(|x| x.get()).unwrap_or_default().to_string(); + for part in path.split('.').filter(|p| !p.is_empty()) { + match serde_json::from_str::(&value) { + Ok(obj) => value = obj.get(part).map(|v| v.to_string()).unwrap_or_default(), + Err(_) => { + value = String::new(); + break; + } + } + } + value.trim_matches('"').to_string() +} + pub fn interpolate_args(x: String, args: &PushArgs, workspace_id: &str) -> String { // Save this value to avoid parsing twice let workspaced = x.as_str().replace("$workspace", workspace_id).to_string(); @@ -4657,40 +4681,12 @@ pub fn interpolate_args(x: String, args: &PushArgs, workspace_id: &str) -> Strin let mut interpolated = workspaced.clone(); for cap in RE_ARG_TAG.captures_iter(&workspaced) { let arg_name = cap.get(1).unwrap().as_str(); - let arg_value = if arg_name.contains('.') { - let parts: Vec<&str> = arg_name.split('.').collect(); - let root = parts[0]; - let mut value = args - .args - .get(root) - .or(args.extra.as_ref().and_then(|x| x.get(root))) - .map(|x| x.get()) - .unwrap_or_default() - .to_string(); - - for part in parts.iter().skip(1) { - if let Ok(obj) = serde_json::from_str::(&value) { - value = obj - .get(part) - .and_then(|v| Some(v.to_string())) - .unwrap_or_default() - .as_str() - .to_string(); - } else { - value = "".to_string(); // Invalid JSON or missing field - break; - } - } - value.trim_matches('"').to_string() - } else { - args.args - .get(arg_name) - .or(args.extra.as_ref().and_then(|x| x.get(arg_name))) - .map(|x| x.get()) - .unwrap_or_default() - .trim_matches('"') - .to_string() - }; + let (root, rest) = arg_name.split_once('.').unwrap_or((arg_name, "")); + let root_value = args + .args + .get(root) + .or(args.extra.as_ref().and_then(|x| x.get(root))); + let arg_value = render_tag_path(root_value.map(|x| &**x), rest); interpolated = interpolated.replace(format!("$args[{}]", arg_name).as_str(), &arg_value); } @@ -5269,6 +5265,8 @@ pub fn empty_result() -> Box { lazy_static::lazy_static! { pub static ref RE_ARG_TAG: Regex = Regex::new(r#"\$args\[((?:\w+\.)*\w+)\]"#).unwrap(); + pub static ref RE_FLOW_EXPR_TAG: Regex = + Regex::new(r#"\$flow_expr\[((?:\w+\.)*\w+)\]"#).unwrap(); } #[cfg(feature = "cloud")] @@ -6537,7 +6535,10 @@ async fn push_inner<'c, 'd>( ); windmill_common::worker::dedicated_worker_tag(workspace_id, &full_path) } else { - if tag == Some("".to_string()) { + // The flow runtime resolves a step's `$flow_expr[...]` before pushing it, so one still here + // was pushed with no flow state to read (a step test, a dependency job) and would name a + // queue no worker serves: the job runs on its default tag instead. + if tag == Some("".to_string()) || tag.as_deref().is_some_and(tag_reads_flow_expr) { tag = None; } @@ -7922,3 +7923,43 @@ mod result_metadata_tests { assert_eq!(meta.wm_failure.as_deref(), Some("boom")); } } + +#[cfg(test)] +mod render_tag_path_tests { + use super::{interpolate_args, render_tag_path, PushArgs}; + use serde_json::value::RawValue; + use std::collections::HashMap; + + fn render(root: &str, path: &str) -> String { + render_tag_path( + Some(&RawValue::from_string(root.to_string()).unwrap()), + path, + ) + } + + // Existing `$args[...]` tags route on exactly these renderings. + #[test] + fn renders_like_args_tags() { + assert_eq!(render(r#""eu""#, ""), "eu"); + assert_eq!(render(r#"{"a": {"b": "eu"}}"#, "a.b"), "eu"); + assert_eq!(render(r#"{"n": 4}"#, "n"), "4"); + assert_eq!(render("null", ""), "null"); + assert_eq!(render(r#"{"a": 1}"#, "b.c"), ""); + assert_eq!(render(r#"{"a": ["eu"]}"#, "a.0"), ""); + assert_eq!(render_tag_path(None, "a"), ""); + + let args = HashMap::from([("cfg".to_string(), raw(r#"{"lang": "eu"}"#))]); + let push_args = PushArgs { + args: &args, + extra: Some(HashMap::from([("e".to_string(), raw(r#""x""#))])), + }; + assert_eq!( + interpolate_args("w-$args[cfg.lang]-$args[e]".to_string(), &push_args, "ws"), + "w-eu-x" + ); + } + + fn raw(json: &str) -> Box { + RawValue::from_string(json.to_string()).unwrap() + } +} diff --git a/backend/windmill-runtime-nativets/src/lib.rs b/backend/windmill-runtime-nativets/src/lib.rs index d44213d3bc..750bf7ebac 100644 --- a/backend/windmill-runtime-nativets/src/lib.rs +++ b/backend/windmill-runtime-nativets/src/lib.rs @@ -838,6 +838,11 @@ pub async fn eval_fetch_timeout( } } let w_id_for_tracing = w_id_for_tracing; + // nativets delivers logs in-process, so they never reach the masking in + // `handle_child::write_lines` and a `console.log` of `$WM_TOKEN` would be + // persisted verbatim. Mask here rather than in the detached task draining into + // `append_logs`: this loop normally runs while the job is still registered. + let mut masker = windmill_common::sensitive_log_masks::JobMasker::new(job_id); let handle = tokio::spawn(async move { let mut result_stream = String::new(); let mut is_stream = false; @@ -845,10 +850,20 @@ pub async fn eval_fetch_timeout( use windmill_common::result_stream::extract_stream_from_logs; use windmill_common::tracing_init::{OTEL_JOB_LOGS, OTEL_PREFIX}; + let stream = extract_stream_from_logs(&log.trim_end_matches("\n")); + + // A stream chunk is result data, not a log line — it never reaches + // `job_logs`, and `merge_result_stream` can make it the job's result — + // so it stays raw wherever it goes, here and in the mirror below. + // Deliberately unlike `handle_child`, which streams the masked text. + // Routed before masking because the notice is one-shot: spent on a chunk + // no sink persists, a later redaction in `job_logs` would go unexplained. + let logged = stream.is_none().then(|| masker.mask(&log).into_owned()); + // Mirror `process_streaming_log_lines` (EE) + the OTEL_JOB_LOGS // hook from handle_child.rs, neither of which runs for nativets // since nativets delivers logs in-process via the log channel. - for line in log.lines() { + for line in logged.as_deref().unwrap_or(&log).lines() { tracing::info!( target: "windmill:job_log", job_id = ?job_id, @@ -862,7 +877,7 @@ pub async fn eval_fetch_timeout( } } - if let Some(stream) = extract_stream_from_logs(&log.trim_end_matches("\n")) { + if let Some(stream) = stream { if !is_stream { is_stream = true; if let Some(ref f) = stream_notifier_update { @@ -874,8 +889,8 @@ pub async fn eval_fetch_timeout( if let Err(e) = result_stream_sender.send(stream) { tracing::error!("failed to send result stream: {e}"); } - } else { - if let Err(e) = append_logs_sender.send(log) { + } else if let Some(logged) = logged { + if let Err(e) = append_logs_sender.send(logged) { tracing::error!("failed to send log: {e}"); } } diff --git a/backend/windmill-runtime-nativets/src/windmill-client.js b/backend/windmill-runtime-nativets/src/windmill-client.js index 7c7d649042..c2dbd1fef1 100644 --- a/backend/windmill-runtime-nativets/src/windmill-client.js +++ b/backend/windmill-runtime-nativets/src/windmill-client.js @@ -6457,7 +6457,7 @@ var AppService = class { * @param data.workspace * @param data.path * @param data.requestBody update app - * @returns string app updated + * @returns AppDeployed the path the app now lives at and the version this deploy wrote * @throws ApiError */ static updateApp(data) { diff --git a/backend/windmill-types/src/scripts.rs b/backend/windmill-types/src/scripts.rs index 9a059da6e8..9e29731505 100644 --- a/backend/windmill-types/src/scripts.rs +++ b/backend/windmill-types/src/scripts.rs @@ -507,6 +507,10 @@ pub struct ScriptHistory { pub deployment_msg: Option, #[serde(skip_serializing_if = "Option::is_none")] pub created_at: Option>, + /// Who deployed this version — the version picker names it, so a reader can + /// tell their own deploys from a teammate's. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_by: Option, } #[derive(Deserialize)] diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index efc7ffa1b3..3c3a26875b 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -79,6 +79,15 @@ lazy_static::lazy_static! { const DEFAULT_MAX_AGENT_ITERATIONS: usize = 10; const HARD_MAX_AGENT_ITERATIONS: usize = 1000; +/// What a run stopped by `max_iterations` reports back. `Message` rather than +/// `OpenAIMessage` is load-bearing: `agent_action` is `skip_serializing` on the +/// latter and reaches JSON only through this wrapper, so serializing these raw +/// drops every tool name and job id and leaves the partial run unreadable. +#[derive(serde::Serialize)] +struct MaxIterPartialResult<'a> { + messages: Vec>, +} + fn strip_system_messages(messages: &[OpenAIMessage]) -> Vec { messages .iter() @@ -1785,10 +1794,6 @@ pub async fn run_agent( step_id: Option<&'a str>, result: MaxIterPartialResult<'a>, } - #[derive(serde::Serialize)] - struct MaxIterPartialResult<'a> { - messages: &'a [OpenAIMessage], - } return Err(Error::ExecutionRawError( serde_json::value::to_raw_value(&MaxIterError { message: format!( @@ -1797,7 +1802,15 @@ pub async fn run_agent( ), name: "ExecutionErr", step_id: effective_flow_step_id, - result: MaxIterPartialResult { messages: &messages }, + result: MaxIterPartialResult { + messages: messages + .iter() + .map(|m| Message { + message: m, + agent_action: m.agent_action.as_ref(), + }) + .collect(), + }, })?, )); } @@ -2322,6 +2335,33 @@ mod tests { assert!(!streaming_requested(Some(false))); } + #[test] + fn max_iterations_partial_result_keeps_the_action_tags() { + let messages = vec![OpenAIMessage { + role: "tool".to_string(), + content: Some(OpenAIContent::Text("{\"rows\":2}".to_string())), + tool_call_id: Some("call_1".to_string()), + agent_action: Some(AgentAction::ToolCall { + job_id: uuid::Uuid::nil(), + function_name: "list_payouts".to_string(), + module_id: "b".to_string(), + }), + ..Default::default() + }]; + + let partial = MaxIterPartialResult { + messages: messages + .iter() + .map(|m| Message { message: m, agent_action: m.agent_action.as_ref() }) + .collect(), + }; + let json = serde_json::to_value(&partial).unwrap(); + + let action = &json["messages"][0]["agent_action"]; + assert_eq!(action["type"], "tool_call"); + assert_eq!(action["function_name"], "list_payouts"); + } + /// Over 64 characters OpenAI rejects the key outright, which costs a wasted round /// trip per run and silently leaves that step with no prompt caching at all. #[test] diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 56b4b26fb0..55a698a9dd 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -3859,8 +3859,6 @@ pub async fn run_worker( let arc_job = Arc::new(job); - windmill_common::sensitive_log_masks::register_running_job(arc_job.id); - let span = create_span_with_name(&arc_job, &worker_name, Some(hostname), "job"); let log_ctx = log_context_for_job(&arc_job, &worker_name, Some(hostname)); @@ -3976,8 +3974,6 @@ pub async fn run_worker( _ => {} } - windmill_common::sensitive_log_masks::unregister_running_job(job_id); - #[cfg(feature = "prometheus")] if let Some(duration) = _timer.map(|x| x.stop_and_record()) { register_metric( @@ -4512,6 +4508,30 @@ async fn detect_and_store_runtime_assets_from_job_args( } } +/// Holds a job's entry in the log-masking registry for as long as it executes, so +/// that secrets it fetches can be registered against it, and masks the job's own +/// token from the start: `$WM_TOKEN` stays valid well past the run, and a script +/// that echoes it would otherwise leave a live credential in the persisted logs. +/// +/// Lives here rather than at the call sites so that every way of running a job — +/// the poller, the interactive worker shell, an inline AI agent tool — is covered +/// by construction. +struct RunningJobMasks(Uuid); + +impl RunningJobMasks { + fn register(job_id: Uuid, token: &str) -> Self { + windmill_common::sensitive_log_masks::register_running_job(job_id); + windmill_common::sensitive_log_masks::register_secret_for_job(job_id, token); + RunningJobMasks(job_id) + } +} + +impl Drop for RunningJobMasks { + fn drop(&mut self) { + windmill_common::sensitive_log_masks::unregister_running_job(self.0); + } +} + pub async fn handle_queued_job( job: Arc, raw_code: Option, @@ -4533,6 +4553,8 @@ pub async fn handle_queued_job( flow_runners: Option>, #[cfg(feature = "benchmark")] _bench: &mut BenchmarkIter, ) -> windmill_common::error::Result { + let _masks = RunningJobMasks::register(job.id, &client.token); + if job.canceled_by.is_some() { return Err(Error::JsonErr(canceled_job_to_result(&job))); } diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 6c0dcaf279..dbdba3e6ed 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -69,10 +69,10 @@ use windmill_common::{ use windmill_queue::schedule::get_schedule_opt; use windmill_queue::{ add_completed_job, add_completed_job_error, append_logs, get_mini_pulled_job, - insert_concurrency_key_capped, interpolate_args, + insert_concurrency_key_capped, interpolate_args, render_tag_path, report_error_to_workspace_handler_or_critical_side_channel, tag_reads_args, - try_schedule_next_job, CanceledBy, FlowRunners, MiniCompletedJob, MiniPulledJob, PushArgs, - PushIsolationLevel, SameWorkerPayload, WrappedError, + tag_reads_flow_expr, try_schedule_next_job, CanceledBy, FlowRunners, MiniCompletedJob, + MiniPulledJob, PushArgs, PushIsolationLevel, SameWorkerPayload, WrappedError, RE_FLOW_EXPR_TAG, }; use windmill_audit::audit_oss::audit_log; @@ -3115,6 +3115,66 @@ fn resolve_flow_step_tag( } } +/// Resolves each `$flow_expr[root.key.path]` of a step tag by reading `key.path` from `results` +/// (where `key` is a step id), `flow_input` or `flow_env`, rendered as `$args[key.path]` would be. +async fn interpolate_flow_expr_tag( + tag: &str, + db: &DB, + flow_job: &MiniPulledJob, + flow_input: &HashMap>, + flow_env: Option<&HashMap>>, +) -> error::Result { + if RE_FLOW_EXPR_TAG + .replace_all(tag, "") + .contains("$flow_expr[") + { + return Err(Error::ExecutionErr(format!( + "Could not resolve the step tag `{tag}`: each `$flow_expr[...]` must hold a dotted \ + path such as `results.a.b.c`" + ))); + } + let mut rendered: HashMap<&str, String> = HashMap::new(); + for cap in RE_FLOW_EXPR_TAG.captures_iter(tag) { + let path = cap.get(1).unwrap().as_str(); + if rendered.contains_key(path) { + continue; + } + let (root, key_path) = path.split_once('.').unwrap_or((path, "")); + let (key, rest) = key_path.split_once('.').unwrap_or((key_path, "")); + if key.is_empty() || !matches!(root, "results" | "flow_input" | "flow_env") { + return Err(Error::ExecutionErr(format!( + "Could not resolve the step tag `{tag}`: `{path}` must start with \ + `results.`, `flow_input.` or `flow_env.`" + ))); + } + let value = match root { + "flow_input" => render_tag_path(flow_input.get(key).map(|x| &**x), rest), + "flow_env" => render_tag_path(flow_env.and_then(|e| e.get(key)).map(|x| &**x), rest), + _ => match windmill_queue::get_result_by_id( + db.clone(), + flow_job.workspace_id.clone(), + flow_job.id, + key.to_string(), + None, + ) + .await + { + Ok(result) => render_tag_path(Some(&*result), rest), + Err(Error::NotFound(_)) => String::new(), + Err(e) => { + return Err(Error::ExecutionErr(format!( + "Could not resolve the step tag `{tag}`: {e}" + ))) + } + }, + }; + rendered.insert(path, value); + } + Ok(RE_FLOW_EXPR_TAG + .replace_all(tag, |cap: ®ex::Captures| rendered[&cap[1]].clone()) + .into_owned()) +} + #[cfg(test)] mod tag_resolution_tests { use super::resolve_flow_step_tag; @@ -4193,6 +4253,10 @@ async fn push_next_flow_job( None }; + // The `flow_input` the step's input transforms read, which a `$flow_expr[flow_input...]` + // tag must read too: the body of a simple for-loop also sees `iter` there. + let mut step_flow_input = arc_flow_job_args.clone(); + let marc; let me; let args = match &next_status { @@ -4217,8 +4281,10 @@ async fn push_next_flow_job( if let Some(input_transforms) = simple_input_transforms { //previous id is none because we do not want to use previous id if we are in a for loop let ctx = get_transform_context(&flow_job, "", &status); + let args = Marc::new(args); + step_flow_input = args.clone(); let ti = transform_input( - Marc::new(args), + args, flow_env, arc_last_job_result.clone(), input_transforms, @@ -4397,17 +4463,21 @@ async fn push_next_flow_job( payload_tag.tag.as_deref(), ); - // `push_args` is empty once the input transforms failed, so a tag reading `$args[...]` - // interpolates to a queue nobody serves and the step sits there instead of reporting - // the error. Send it to the flow's tag, which a worker is provably serving right now. + // A step whose inputs failed to evaluate, or whose `$flow_expr[...]` tag failed to resolve, + // is pushed only to report the error, and a computed tag can then name a queue nobody + // serves (`push_args` is empty, so `$args[...]` reads nothing), leaving the step stuck + // instead. Send it to the flow's tag, which a worker is provably serving right now. // // A step handed over by id, or one whose tag `push` replaces, never reaches a worker // through its tag, so rewriting theirs would be noise. let step_is_pulled_by_tag = !continue_on_same_worker && !continue_with_runners && !payload_tag.payload.is_dedicated_worker(); - let reroute_to_flow_tag = - err.is_some() && step_is_pulled_by_tag && tag.as_deref().is_some_and(tag_reads_args); + let reroute_to_flow_tag = err.is_some() + && step_is_pulled_by_tag + && tag + .as_deref() + .is_some_and(|t| tag_reads_args(t) || tag_reads_flow_expr(t)); let tag = if reroute_to_flow_tag { Some(flow_job.tag.clone()) } else { @@ -4449,6 +4519,30 @@ async fn push_next_flow_job( .await?; } + // Resolved only after the check: CUSTOM_TAGS allows the template, so its value may name + // any queue, as the value of an `$args[...]` tag does. + let mut tag_err = None; + let tag = match tag { + Some(t) if err.is_none() && tag_reads_flow_expr(&t) => { + match interpolate_flow_expr_tag(&t, db, &flow_job, &step_flow_input, flow_env) + .warn_after_seconds(3) + .await + { + Ok(resolved) => Some(resolved), + Err(e) => { + tag_err = Some(e); + Some(if step_is_pulled_by_tag { + flow_job.tag.clone() + } else { + t + }) + } + } + } + t => t, + }; + let err = err.or(tag_err.as_ref()); + let evaluated_timeout = if let Some(timeout_transform) = &module.timeout { let ctx = get_transform_context(&flow_job, &previous_id, &status); diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 55735ed9fd..ed07c4c140 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.814.0"; +export const VERSION = "v1.815.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/benchmarks/pulumi/package-lock.json b/benchmarks/pulumi/package-lock.json deleted file mode 100644 index ddd1f09e83..0000000000 --- a/benchmarks/pulumi/package-lock.json +++ /dev/null @@ -1,2465 +0,0 @@ -{ - "name": "aws-bench", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "aws-bench", - "dependencies": { - "@pulumi/aws": "^5.0.0", - "@pulumi/awsx": "^1.0.4", - "@pulumi/pulumi": "^3.0.0", - "@pulumi/tailscale": "^0.12.2", - "@pulumi/tls": "^4.10.0" - }, - "devDependencies": { - "@types/node": "^16", - "pulumi": "^0.0.1" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@logdna/tail-file": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@logdna/tail-file/-/tail-file-2.2.0.tgz", - "integrity": "sha512-XGSsWDweP80Fks16lwkAUIr54ICyBs6PsI4mpfTLQaWgEJRtY9xEV+PeyDpJ+sJEGZxqINlpmAwe/6tS1pP8Ng==", - "engines": { - "node": ">=10.3.0" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", - "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-metrics": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-metrics/-/api-metrics-0.32.0.tgz", - "integrity": "sha512-g1WLhpG8B6iuDyZJFRGsR+JKyZ94m5LEmY2f+duEJ9Xb4XRlLHrZvh6G34OH6GJ8iDHxfHb/sWjJ1ZpkI9yGMQ==", - "deprecated": "Please use @opentelemetry/api >= 1.3.0", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.15.2.tgz", - "integrity": "sha512-VAMHG67srGFQDG/N2ns5AyUT9vUcoKpZ/NpJ5fDQIPfJd7t3ju+aHwvDsMcrYBWuCh03U3Ky6o16+872CZchBg==", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.5.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", - "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.15.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.5.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.15.2.tgz", - "integrity": "sha512-j9dPe8tyx4KqIqJAfZ/LCYfkF9+ggsT0V1+bVg9ZKTBNcLf5dTsTMdcxUxc/9s599kgcn6UERnti/tozbzwa6Q==", - "dependencies": { - "@opentelemetry/core": "1.15.2", - "@opentelemetry/resources": "1.15.2", - "@opentelemetry/sdk-trace-base": "1.15.2", - "@opentelemetry/semantic-conventions": "1.15.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.32.0.tgz", - "integrity": "sha512-y6ADjHpkUz/v1nkyyYjsQa/zorhX+0qVGpFvXMcbjU4sHnBnC02c6wcc93sIgZfiQClIWo45TGku1KQxJ5UUbQ==", - "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "require-in-the-middle": "^5.0.3", - "semver": "^7.3.2", - "shimmer": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-grpc": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.32.0.tgz", - "integrity": "sha512-Az6wdkPx/Mi26lT9LKFV6GhCA9prwQFPz5eCNSExTnSP49YhQ7XCjzPd2POPeLKt84ICitrBMdE1mj0zbPdLAQ==", - "dependencies": { - "@opentelemetry/api-metrics": "0.32.0", - "@opentelemetry/instrumentation": "0.32.0", - "@opentelemetry/semantic-conventions": "1.6.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.6.0.tgz", - "integrity": "sha512-aPfcBeLErM/PPiAuAbNFLN5sNbZLc3KZlar27uohllN8Zs6jJbHyJU1y7cMA6W/zuq+thkaG8mujiS+3iD/FWQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/instrumentation/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@opentelemetry/propagator-b3": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.15.2.tgz", - "integrity": "sha512-ZSrL3DpMEDsjD8dPt9Ze3ue53nEXJt512KyxXlLgLWnSNbe1mrWaXWkh7OLDoVJh9LqFw+tlvAhDVt/x3DaFGg==", - "dependencies": { - "@opentelemetry/core": "1.15.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.5.0" - } - }, - "node_modules/@opentelemetry/propagator-jaeger": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.15.2.tgz", - "integrity": "sha512-6m1yu7PVDIRz6BwA36lacfBZJCfAEHKgu+kSyukNwVdVjsTNeyD9xNPQnkl0WN7Rvhk8/yWJ83tLPEyGhk1wCQ==", - "dependencies": { - "@opentelemetry/core": "1.15.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.5.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.15.2.tgz", - "integrity": "sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw==", - "dependencies": { - "@opentelemetry/core": "1.15.2", - "@opentelemetry/semantic-conventions": "1.15.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.5.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz", - "integrity": "sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ==", - "dependencies": { - "@opentelemetry/core": "1.15.2", - "@opentelemetry/resources": "1.15.2", - "@opentelemetry/semantic-conventions": "1.15.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.5.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.15.2.tgz", - "integrity": "sha512-5deakfKLCbPpKJRCE2GPI8LBE2LezyvR17y3t37ZI3sbaeogtyxmBaFV+slmG9fN8OaIT+EUsm1QAT1+z59gbQ==", - "dependencies": { - "@opentelemetry/context-async-hooks": "1.15.2", - "@opentelemetry/core": "1.15.2", - "@opentelemetry/propagator-b3": "1.15.2", - "@opentelemetry/propagator-jaeger": "1.15.2", - "@opentelemetry/sdk-trace-base": "1.15.2", - "semver": "^7.5.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.5.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz", - "integrity": "sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" - }, - "node_modules/@pulumi/aws": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@pulumi/aws/-/aws-5.43.0.tgz", - "integrity": "sha512-ZWI+QvEaFy27wUW8j8EVadgvl6u3926AAgpghflSPxF/9mG5XoDy0rqkRZOgO13ZJinC1qkQDzoMz0ACgmwTiw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@pulumi/pulumi": "^3.0.0", - "aws-sdk": "^2.0.0", - "builtin-modules": "3.0.0", - "mime": "^2.0.0", - "read-package-tree": "^5.2.1", - "resolve": "^1.7.1" - } - }, - "node_modules/@pulumi/awsx": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@pulumi/awsx/-/awsx-1.0.6.tgz", - "integrity": "sha512-zTBsRO6EeSg6V2sEkw1b8OtvExaYMg1rUlaJVfui54W86M3KvFANtGI99a+aScUxJMxGqkS9cb6LdzZjTlKQGA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@pulumi/aws": "^5.35.0", - "@pulumi/docker": "^3.6.1", - "@pulumi/pulumi": "^3.0.0", - "@types/aws-lambda": "^8.10.23", - "mime": "^2.0.0" - } - }, - "node_modules/@pulumi/docker": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@pulumi/docker/-/docker-3.6.1.tgz", - "integrity": "sha512-BZME50QkT556v+LvmTXPT8ssB2xxNkp9+msB5xYFEnUnWcdGAx5yUysQw70RJCb+U0GbkJSbxtlgMJgOQf/now==", - "hasInstallScript": true, - "dependencies": { - "@pulumi/pulumi": "^3.0.0", - "semver": "^5.4.0" - } - }, - "node_modules/@pulumi/pulumi": { - "version": "3.77.1", - "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.77.1.tgz", - "integrity": "sha512-smeip4uKWkaKcNhMiAmR5uW4nXRvmHSjq93C7zx+mMneNxeaD9HNIBnYW0H0FFgp7j1AdRYuiIa2ie9Ay51bpw==", - "dependencies": { - "@grpc/grpc-js": "^1.8.16", - "@logdna/tail-file": "^2.0.6", - "@opentelemetry/api": "^1.2.0", - "@opentelemetry/exporter-zipkin": "^1.6.0", - "@opentelemetry/instrumentation": "^0.32.0", - "@opentelemetry/instrumentation-grpc": "^0.32.0", - "@opentelemetry/resources": "^1.6.0", - "@opentelemetry/sdk-trace-base": "^1.6.0", - "@opentelemetry/sdk-trace-node": "^1.6.0", - "@opentelemetry/semantic-conventions": "^1.6.0", - "@pulumi/query": "^0.3.0", - "execa": "^5.1.0", - "google-protobuf": "^3.5.0", - "ini": "^2.0.0", - "js-yaml": "^3.14.0", - "minimist": "^1.2.6", - "normalize-package-data": "^3.0.0", - "pkg-dir": "^7.0.0", - "read-package-tree": "^5.3.1", - "require-from-string": "^2.0.1", - "semver": "^7.5.2", - "source-map-support": "^0.5.6", - "ts-node": "^7.0.1", - "typescript": "~3.8.3", - "upath": "^1.1.0" - }, - "engines": { - "node": ">=8.13.0 || >=10.10.0" - } - }, - "node_modules/@pulumi/pulumi/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@pulumi/query": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@pulumi/query/-/query-0.3.0.tgz", - "integrity": "sha512-xfo+yLRM2zVjVEA4p23IjQWzyWl1ZhWOGobsBqRpIarzLvwNH/RAGaoehdxlhx4X92302DrpdIFgTICMN4P38w==" - }, - "node_modules/@pulumi/tailscale": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@pulumi/tailscale/-/tailscale-0.12.2.tgz", - "integrity": "sha512-eDzXu4I7awua494d6is9nwV/8G56dTI4oL53G/WTqWtl1ezVHb+v6bKxMrcPvhhYGugbIhK56ECWPiqg9k5gJg==", - "hasInstallScript": true, - "dependencies": { - "@pulumi/pulumi": "^3.0.0" - } - }, - "node_modules/@pulumi/tls": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@pulumi/tls/-/tls-4.10.0.tgz", - "integrity": "sha512-aK1LXJzDbeVYBIlxycUUboZJSjMr2wIkrpngNRrOFPMIyO4QYNzJHWIwRbNkS463hhpbp1it3IC3htEqtP4lzg==", - "hasInstallScript": true, - "dependencies": { - "@pulumi/pulumi": "^3.0.0" - } - }, - "node_modules/@types/aws-lambda": { - "version": "8.10.119", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.119.tgz", - "integrity": "sha512-Vqm22aZrCvCd6I5g1SvpW151jfqwTzEZ7XJ3yZ6xaZG31nUEOEyzzVImjRcsN8Wi/QyPxId/x8GTtgIbsy8kEw==" - }, - "node_modules/@types/node": { - "version": "16.18.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.40.tgz", - "integrity": "sha512-+yno3ItTEwGxXiS/75Q/aHaa5srkpnJaH+kdkTVJ3DtJEwv92itpKbxU+FjPoh2m/5G9zmUQfrL4A4C13c+iGA==" - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", - "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sdk": { - "version": "2.1693.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz", - "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", - "deprecated": "The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/builtin-modules": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.0.0.tgz", - "integrity": "sha512-hMIeU4K2ilbXV6Uv93ZZ0Avg/M91RaKXucQ+4me2Do1txxBDyDZWCBa5bJSLqoNTRpXTLwEzIk1KmloenDDjhg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "engines": { - "node": "*" - } - }, - "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.1.tgz", - "integrity": "sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/es-abstract": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", - "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.1", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.1", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "safe-array-concat": "^1.0.0", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/google-protobuf": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", - "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", - "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", - "dependencies": { - "array.prototype.reduce": "^1.0.5", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.21.2", - "safe-array-concat": "^1.0.0" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/protobufjs": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", - "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/pulumi": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pulumi/-/pulumi-0.0.1.tgz", - "integrity": "sha512-Sow9mG2Yf3vRwQV5pux3EfZNvs7uoEjhYGTUWeGDTVrRT/6w3xybp8/a+SpDkH7dooHnGWDJW4QrSAbd+HvK5A==", - "deprecated": "To install Pulumi, use the @pulumi/pulumi package: 'npm -i @pulumi/pulumi'", - "dev": true - }, - "node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" - }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/read-package-json": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", - "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", - "dependencies": { - "glob": "^7.1.1", - "json-parse-even-better-errors": "^2.3.0", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "node_modules/read-package-json/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "node_modules/read-package-json/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-package-tree": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz", - "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", - "deprecated": "The functionality that this package provided is now in @npmcli/arborist", - "dependencies": { - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "util-promisify": "^2.1.0" - } - }, - "node_modules/readdir-scoped-modules": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dependencies": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-in-the-middle": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", - "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", - "dependencies": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", - "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", - "license": "ISC" - }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ts-node": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", - "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", - "dependencies": { - "arrify": "^1.0.0", - "buffer-from": "^1.1.0", - "diff": "^3.1.0", - "make-error": "^1.1.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "source-map-support": "^0.5.6", - "yn": "^2.0.0" - }, - "bin": { - "ts-node": "dist/bin.js" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-promisify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz", - "integrity": "sha512-K+5eQPYs14b3+E+hmE2J6gCZ4JmMl9DbYS6BeP2CHq6WMuNxErxf5B/n0fz85L8zUuoO6rIzNNmIQDu/j+1OcA==", - "dependencies": { - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", - "integrity": "sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/chat-sdk/package-lock.json b/chat-sdk/package-lock.json index 83475ba1c8..36407f964f 100644 --- a/chat-sdk/package-lock.json +++ b/chat-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-chat", - "version": "1.814.0", + "version": "1.815.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-chat", - "version": "1.814.0", + "version": "1.815.0", "license": "Apache-2.0", "devDependencies": { "@ai-sdk/react": "^4.0.102", diff --git a/chat-sdk/package.json b/chat-sdk/package.json index 57df9050f6..2c42ca3565 100644 --- a/chat-sdk/package.json +++ b/chat-sdk/package.json @@ -1,7 +1,7 @@ { "name": "windmill-chat", "description": "Build chat interfaces on Windmill flows deployed in chat mode, from any frontend or raw app", - "version": "1.814.0", + "version": "1.815.0", "author": "Ruben Fiszel", "license": "Apache-2.0", "homepage": "https://github.com/windmill-labs/windmill/tree/main/chat-sdk#readme", diff --git a/cli/build-npm.ts b/cli/build-npm.ts index 1dbc9261de..776726c3a0 100644 --- a/cli/build-npm.ts +++ b/cli/build-npm.ts @@ -72,7 +72,7 @@ const packageJson = { url: "https://github.com/windmill-labs/windmill/issues", }, dependencies: { - esbuild: "0.28.0", + esbuild: "0.28.2", ...Object.fromEntries(parserPackages.map(p => [p, cliDeps[p] ?? "*"])), }, optionalDependencies: { diff --git a/cli/bun.lock b/cli/bun.lock index cf3a87bf41..0e0885f563 100644 --- a/cli/bun.lock +++ b/cli/bun.lock @@ -11,7 +11,7 @@ "@cliffy/table": "npm:@jsr/cliffy__table@1.0.0", "@windmill-labs/shared-utils": "^1.0.13", "diff": "^5.2.0", - "esbuild": "0.28.0", + "esbuild": "0.28.2", "get-port": "7.1.0", "jszip": "3.8.0", "minimatch": "^10.0.0", @@ -34,7 +34,7 @@ "windmill-parser-wasm-ts": "1.695.0", "windmill-parser-wasm-yaml": "1.770.0", "windmill-yaml-validator": "1.1.1", - "ws": "8.18.0", + "ws": "8.21.3", "yaml": "^2.7.0", }, "devDependencies": { @@ -57,57 +57,57 @@ "@cliffy/table": ["@jsr/cliffy__table@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.0.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-VoLxH0DjofHWPWKUc5N+oCwXB6O6e+carnhp23yJTa7qokBb+SCrTIABEgQdIe/p0bxgmZhz17xt2efaAxXvbQ=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -211,7 +211,7 @@ "diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], - "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": "bin/esbuild" }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], @@ -321,7 +321,7 @@ "windmill-yaml-validator": ["windmill-yaml-validator@1.1.1", "", { "dependencies": { "@stoplight/yaml": "^4.3.0", "ajv": "^8.17.1" } }, "sha512-CVgAwEoBdJhF39q2N012QffhlGPRIyIWd8gj7NnfG+/lMWgH2k5CBLtKIt6cPF8Bxz+6DGC3st1ARSsecDtbTg=="], - "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], diff --git a/cli/package.json b/cli/package.json index 03497dc887..5e7623a7ab 100644 --- a/cli/package.json +++ b/cli/package.json @@ -23,7 +23,7 @@ "@cliffy/table": "npm:@jsr/cliffy__table@1.0.0", "@windmill-labs/shared-utils": "^1.0.13", "diff": "^5.2.0", - "esbuild": "0.28.0", + "esbuild": "0.28.2", "get-port": "7.1.0", "jszip": "3.8.0", "minimatch": "^10.0.0", @@ -46,7 +46,7 @@ "windmill-parser-wasm-ts": "1.695.0", "windmill-parser-wasm-yaml": "1.770.0", "windmill-yaml-validator": "1.1.1", - "ws": "8.18.0", + "ws": "8.21.3", "yaml": "^2.7.0" }, "devDependencies": { diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 138a346594..32cff4a3cb 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -2546,9 +2546,9 @@ export function preservePendingScriptLocks( } // `sync push` never applies the workspace's display name from settings.yaml and -// applies its color only when the local file carries one (see -// pushWorkspaceSettings), so on a push the fields it would not apply must -// compare equal, or the row is listed on every run. +// applies its color and auto_invite.instance_groups only when the local file +// carries them (see pushWorkspaceSettings), so on a push the fields it would not +// apply must compare equal, or the row is listed on every run. const isWorkspaceSettingsFile = (p: string) => /^settings(\.[^./\\]+)?\.(yaml|json)$/.test(p); function stripUnappliedSettingsFields(local: any, remote: any) { @@ -2558,6 +2558,20 @@ function stripUnappliedSettingsFields(local: any, remote: any) { delete local?.color; delete remote?.color; } + // push reads a missing auto_invite as {} on both sides + if (local) local.auto_invite ??= {}; + if (remote) remote.auto_invite ??= {}; + const localInvite = local?.auto_invite; + const remoteInvite = remote?.auto_invite; + if (localInvite?.instance_groups == null) { + for (const invite of [localInvite, remoteInvite]) { + delete invite?.instance_groups; + delete invite?.instance_groups_roles; + } + } else { + localInvite.instance_groups_roles ??= {}; + if (remoteInvite) remoteInvite.instance_groups_roles ??= {}; + } } export async function compareDynFSElement( @@ -3580,12 +3594,15 @@ export async function pull( ) { if ((opts as any).jsonOutput) log.setSilent(true); const originalCliOpts = { ...opts }; - opts = await mergeConfigWithConfigFile(opts); - - // --include-secrets overrides skipSecrets from wmill.yaml - if ((originalCliOpts as any).includeSecrets) { - opts.skipSecrets = false; - } + const withConfigFile = async () => { + const merged = await mergeConfigWithConfigFile({ ...originalCliOpts }); + // --include-secrets overrides skipSecrets from wmill.yaml + if ((originalCliOpts as any).includeSecrets) { + merged.skipSecrets = false; + } + return merged; + }; + opts = await withConfigFile(); // Resolve workspace name for config lookups. // --branch resolves git branch → workspace name (deprecated but still supported). @@ -3620,10 +3637,6 @@ export async function pull( throw error; } - if (opts.stateful) { - await mkdir(path.join(process.cwd(), ".wmill"), { recursive: true }); - } - const workspace = await resolveWorkspace(opts, wsNameForConfig); await requireLogin(opts); @@ -3715,6 +3728,14 @@ export async function pull( }); return; } + + // The pull writes into the branch now checked out, so its wmill.yaml + // applies, not the cloned branch's: a fork branch that turned on + // `dedupeLockfiles` would otherwise get one lockfile per script back. + if (getCurrentGitBranch() !== clonedBranchName) { + opts = await withConfigFile(); + wsNameForConfig = resolveWsNameForConfigFromFlags(opts); + } } // If wsNameForConfig wasn't set from flags, infer from the resolved profile @@ -3749,6 +3770,10 @@ export async function pull( // Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides) opts = mergeCliWithEffectiveOptions(originalCliOpts, effectiveOpts); + if (opts.stateful) { + await mkdir(path.join(process.cwd(), ".wmill"), { recursive: true }); + } + const codebases = await listSyncCodebases(opts); log.info( diff --git a/cli/src/core/constants.ts b/cli/src/core/constants.ts index a399ec1b85..ab1bcf0a87 100644 --- a/cli/src/core/constants.ts +++ b/cli/src/core/constants.ts @@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork"; // (e.g. utils.ts) can read it without importing main.ts and creating a circular // dependency (main → workspace → utils → main) that triggers a TDZ. // Re-exported from main.ts for backwards compatibility. -export const VERSION = "1.814.0"; +export const VERSION = "1.815.0"; diff --git a/cli/src/core/settings.ts b/cli/src/core/settings.ts index 4174ec0358..6a9358b468 100644 --- a/cli/src/core/settings.ts +++ b/cli/src/core/settings.ts @@ -239,8 +239,19 @@ export async function pushWorkspaceSettings( }); } - // Handle auto_invite using grouped format - if (!deepEqual(localSettings.auto_invite, settings.auto_invite)) { + // Handle auto_invite using grouped format. The domain invite and the instance groups + // are applied by separate endpoints, each rewriting only its own keys. + const { + instance_groups: localGroups, + instance_groups_roles: localGroupRoles, + ...localDomainInvite + } = localSettings.auto_invite ?? {}; + const { + instance_groups: remoteGroups, + instance_groups_roles: remoteGroupRoles, + ...remoteDomainInvite + } = settings.auto_invite ?? {}; + if (!deepEqual(localDomainInvite, remoteDomainInvite)) { log.debug(`Updating auto invite...`); const localAutoInvite = localSettings.auto_invite; @@ -278,6 +289,20 @@ export async function pushWorkspaceSettings( } } + // Only when settings.yaml declares instance_groups: clearing a group removes the + // workspace members it granted, so an absent key must never clear it. + if ( + localGroups != undefined && + (!deepEqual(localGroups, remoteGroups) || + !deepEqual(localGroupRoles ?? {}, remoteGroupRoles ?? {})) + ) { + log.debug(`Updating instance groups...`); + await wmill.editInstanceGroups({ + workspace, + requestBody: { groups: localGroups, roles: localGroupRoles ?? {} }, + }); + } + if (!deepEqual(localSettings.ai_config, settings.ai_config)) { log.debug(`Updating copilot settings...`); await wmill.editCopilotConfig({ diff --git a/cli/src/utils/esbuild_loader.ts b/cli/src/utils/esbuild_loader.ts index 4801c51a46..83d44f4534 100644 --- a/cli/src/utils/esbuild_loader.ts +++ b/cli/src/utils/esbuild_loader.ts @@ -25,7 +25,7 @@ type Esbuild = typeof import("esbuild"); // Version to fall back to if the native host's version can't be read. Keep in // sync with the "esbuild" pin in cli/package.json. -const FALLBACK_VERSION = "0.28.0"; +const FALLBACK_VERSION = "0.28.2"; let cached: Esbuild | undefined; let inFlight: Promise | undefined; diff --git a/cli/test/gitsync_deploy_branch_config.test.ts b/cli/test/gitsync_deploy_branch_config.test.ts new file mode 100644 index 0000000000..39c435d872 --- /dev/null +++ b/cli/test/gitsync_deploy_branch_config.test.ts @@ -0,0 +1,91 @@ +import { expect, test } from "bun:test"; +import { execFileSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createRemoteWorkspaceDeps, withTestBackend } from "./test_backend.ts"; + +function git(cwd: string, ...args: string[]): string { + return execFileSync("git", args, { cwd, encoding: "utf8" }).trim(); +} + +// The git-sync deploy callback starts in a clone of the tracked branch and +// switches to the fork's branch before pulling. What it writes there must follow +// that branch's wmill.yaml: a fork branch that turned on `dedupeLockfiles` keeps +// its shared lockfile instead of getting a `.script.lock` per script back. +test("git-sync fork deploy follows the fork branch's wmill.yaml", async () => { + await withTestBackend(async (backend) => { + const post = (path: string, body: unknown) => + backend.apiRequest!(`/api/w/${backend.workspace}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + await createRemoteWorkspaceDeps(backend, "python3", "wmill\n"); + await post("/folders/create", { name: "dedupe" }); + const script = await post("/scripts/create", { + path: "f/dedupe/a", + summary: "", + description: "", + content: "def main():\n pass\n", + language: "python3", + lock: "wmill==1.0.0\n", + }); + expect(script.ok).toBe(true); + + const bare = await mkdtemp(join(tmpdir(), "wmill_deploy_cfg_bare_")); + const seed = await mkdtemp(join(tmpdir(), "wmill_deploy_cfg_seed_")); + const work = await mkdtemp(join(tmpdir(), "wmill_deploy_cfg_work_")); + try { + execFileSync("git", ["init", "--bare", "--initial-branch=main", bare]); + git(seed, "init", "--initial-branch=main"); + git(seed, "config", "user.email", "seed@windmill.dev"); + git(seed, "config", "user.name", "seed"); + git(seed, "remote", "add", "origin", `file://${bare}`); + const wmillYaml = "defaultTs: bun\nincludes:\n - f/dedupe/**\nexcludes: []\n"; + await writeFile(join(seed, "wmill.yaml"), wmillYaml); + git(seed, "add", "-A"); + git(seed, "commit", "-m", "main"); + git(seed, "push", "origin", "main"); + + // Any parent id makes the callback deploy as a fork, to this branch. + const forkBranch = `wm-fork/main/${backend.workspace}`; + git(seed, "checkout", "-b", forkBranch); + await writeFile( + join(seed, "wmill.yaml"), + wmillYaml + "dedupeLockfiles: true\n", + ); + const pulled = await backend.runCLICommand(["sync", "pull", "--yes"], seed); + expect(pulled.code).toBe(0); + expect(existsSync(join(seed, "locks/requirements.in.lock"))).toBe(true); + git(seed, "add", "-A"); + git(seed, "commit", "-m", "dedupe"); + git(seed, "push", "origin", forkBranch); + + git(work, "clone", `file://${bare}`, "."); + const deployed = await backend.runCLICommand( + [ + "sync", + "git-deploy", + "--repository", + "u/test/unused", + "--git-deploy-items", + JSON.stringify([ + { path_type: "script", path: "f/dedupe/a", commit_msg: "deploy" }, + ]), + "--parent-workspace-id", + "parent", + ], + work, + ); + expect(deployed.code).toBe(0); + expect(git(work, "rev-parse", "--abbrev-ref", "HEAD")).toBe(forkBranch); + expect(git(work, "status", "--porcelain")).toBe(""); + } finally { + await rm(bare, { recursive: true, force: true }); + await rm(seed, { recursive: true, force: true }); + await rm(work, { recursive: true, force: true }); + } + }); +}); diff --git a/cli/test/push_diff_convergence_unit.test.ts b/cli/test/push_diff_convergence_unit.test.ts index 67f6d5b4dd..a6f2c132c0 100644 --- a/cli/test/push_diff_convergence_unit.test.ts +++ b/cli/test/push_diff_convergence_unit.test.ts @@ -320,3 +320,29 @@ test("push: settings.yaml differing only by name or an unset color is not a chan }); expect(await diff(otherColor, remote, skips)).toEqual(["edited settings.yaml"]); }); + +// A push applies auto_invite.instance_groups only when the local file declares +// them (see pushWorkspaceSettings). +test("push: settings.yaml without instance_groups is not a change", async () => { + const remote = local({ + "settings.yaml": + "name: prod\nauto_invite:\n enabled: false\n instance_groups:\n - eng\n instance_groups_roles:\n eng: developer\n", + }); + const undeclared = local({ + "settings.yaml": "name: prod\nauto_invite:\n enabled: false\n", + }); + const skips = { includeSettings: true }; + expect(await diff(undeclared, remote, skips)).toEqual([]); + + const groupsOnlyRemote = local({ + "settings.yaml": "name: prod\nauto_invite:\n instance_groups:\n - eng\n", + }); + const noAutoInvite = local({ "settings.yaml": "name: prod\n" }); + expect(await diff(noAutoInvite, groupsOnlyRemote, skips)).toEqual([]); + + const otherGroups = local({ + "settings.yaml": + "name: prod\nauto_invite:\n enabled: false\n instance_groups: []\n", + }); + expect(await diff(otherGroups, remote, skips)).toEqual(["edited settings.yaml"]); +}); diff --git a/cli/test/push_workspace_settings_auto_invite_unit.test.ts b/cli/test/push_workspace_settings_auto_invite_unit.test.ts new file mode 100644 index 0000000000..b04d895056 --- /dev/null +++ b/cli/test/push_workspace_settings_auto_invite_unit.test.ts @@ -0,0 +1,82 @@ +/** + * Regression guard: `sync push` (pushWorkspaceSettings) applies the domain invite and + * the instance groups of `auto_invite` through their own endpoints, and never clears + * instance groups that settings.yaml does not declare. + */ + +import { expect, test, describe, beforeEach, mock } from "bun:test"; + +let editAutoInviteCalls: unknown[] = []; +let editInstanceGroupsCalls: unknown[] = []; +const remoteAutoInvite = { + enabled: true, + domain: "*", + operator: false, + mode: "invite", + instance_groups: ["eng"], + instance_groups_roles: { eng: "developer" }, +}; + +// Every wmill.* call reachable from pushWorkspaceSettings is stubbed: bun shares one +// mocked module across test files, and names missing from whichever mock loads first +// stay missing for the others. +mock.module("../gen/services.gen.ts", () => ({ + getSettings: async () => ({ auto_invite: remoteAutoInvite }), + getWorkspaceName: async () => "phoenix", + changeWorkspaceName: async () => {}, + changeWorkspaceColor: async () => {}, + editWebhook: async () => {}, + editAutoInvite: async (a: unknown) => { + editAutoInviteCalls.push(a); + }, + editInstanceGroups: async (a: unknown) => { + editInstanceGroupsCalls.push(a); + }, + editErrorHandler: async () => {}, + editSuccessHandler: async () => {}, + editCopilotConfig: async () => {}, + editLargeFileStorageConfig: async () => {}, + editWorkspaceGitSyncConfig: async () => {}, + editWorkspaceDefaultApp: async () => {}, + editDefaultScripts: async () => {}, + workspaceMuteCriticalAlertsUi: async () => {}, + updateOperatorSettings: async () => {}, + editDataTableConfig: async () => {}, + editSlackCommand: async () => {}, + setWorkspaceSlackOauthConfig: async () => {}, + deleteWorkspaceSlackOauthConfig: async () => {}, +})); + +const { pushWorkspaceSettings } = await import("../src/core/settings.ts"); + +describe("pushWorkspaceSettings auto_invite", () => { + beforeEach(() => { + editAutoInviteCalls = []; + editInstanceGroupsCalls = []; + }); + + test("an instance-group-only change updates the groups and leaves the domain invite", async () => { + await pushWorkspaceSettings("phoenix", "settings", undefined, { + name: "phoenix", + auto_invite: { ...remoteAutoInvite, instance_groups_roles: { eng: "admin" } }, + }); + expect(editAutoInviteCalls.length).toBe(0); + expect(editInstanceGroupsCalls).toEqual([ + { + workspace: "phoenix", + requestBody: { groups: ["eng"], roles: { eng: "admin" } }, + }, + ]); + }); + + test("a settings.yaml without instance_groups does not clear them", async () => { + const { instance_groups: _g, instance_groups_roles: _r, ...domainInvite } = + remoteAutoInvite; + await pushWorkspaceSettings("phoenix", "settings", undefined, { + name: "phoenix", + auto_invite: { ...domainInvite, operator: true }, + }); + expect(editAutoInviteCalls.length).toBe(1); + expect(editInstanceGroupsCalls.length).toBe(0); + }); +}); diff --git a/cli/test/push_workspace_settings_identity_unit.test.ts b/cli/test/push_workspace_settings_identity_unit.test.ts index c583a53282..5dcf9815ef 100644 --- a/cli/test/push_workspace_settings_identity_unit.test.ts +++ b/cli/test/push_workspace_settings_identity_unit.test.ts @@ -31,6 +31,7 @@ mock.module("../gen/services.gen.ts", () => ({ editWebhookCalls.push(a); }, editAutoInvite: async () => {}, + editInstanceGroups: async () => {}, editErrorHandler: async () => {}, editSuccessHandler: async () => {}, editCopilotConfig: async () => {}, diff --git a/docker/DockerfileExtra b/docker/DockerfileExtra index 9eb2a987f2..8aa24206a4 100644 --- a/docker/DockerfileExtra +++ b/docker/DockerfileExtra @@ -110,11 +110,12 @@ WORKDIR /multiplayer # Copy multiplayer server files COPY multiplayer/package.json . +COPY multiplayer/package-lock.json . COPY multiplayer/server.mjs . COPY multiplayer/gateway.mjs . # Install dependencies -RUN npm install +RUN npm ci --omit=dev # ============================================================================ # Entrypoint Setup diff --git a/docs/auth-surface.md b/docs/auth-surface.md index abeba1fe2b..7a8cbd76ea 100644 --- a/docs/auth-surface.md +++ b/docs/auth-surface.md @@ -12,14 +12,36 @@ Symbols, not line numbers, are cited: they drift less. by `create_session_token` (`windmill-api-users/src/users.rs`). `GET /api/users/refresh_token` mints one for any non-job token but returns plain text, no redirect. - **`tokens/impersonate`** (superadmin) returns a multi-use token and sets no cookie. -- **A token's label decides whether its expiry raises alerts.** When `delete_expired_items` - removes an expired `token` row, the monitor emails the owner and raises a critical alert (if - enabled); rows registered by `register_token_expiry_notification` also get an "expiring soon" - warning first. Neither happens when `is_user_token` (`windmill-common/src/auth.rs`) reserves - the label, so a token the system mints for itself, whether from the backend or from the frontend - through `tokens/create`, needs a reserved label. An `ephemeral-` prefix needs no other change - (keep it clear of `is_server_minted_label` if minted through `tokens/create`); a new prefix - also goes into the SQL and Svelte mirrors that function's doc lists. +- **`max_token_expiration_days`** caps `POST /users/tokens/create` and `tokens/impersonate`, by + shortening the stored expiration (`cap_token_expiration`), never by refusing: the CLI + authorization page, `wmill user create-token` and the editor's language-server token all pick a + lifetime without reading the setting, and CLIs already installed never will. The CLI signs in + again on its own when its token expires, which is why the authorization page labels it + `cli-login:`, reserved in `is_user_token` so its expiry does not email the user. A token + owned by a service account is exempt: one in the workspace the token names, or in any workspace + for a workspace-less token (for `tokens/impersonate`, the impersonated account). Any workspace + admin can therefore create and impersonate a service account to hold an uncapped token, so the + ceiling bounds personal tokens only. Only the stored expiration is capped: the auth lookup never + reads the setting, so tokens that exist when it is turned on or lowered keep theirs, including + none. Deliberately outside it: server-side mints (`create_token_internal` callers such as native + trigger webhook tokens, which never expire for GitHub and Nextcloud), and tokens with their own + fixed lifetime that outlive a short ceiling: sessions (`MAX_SESSION_VALIDITY_SECONDS`, 3 days, and + re-mintable through `GET /users/refresh_token`) and MCP OAuth access tokens (7 days, with a + rotating 30-day refresh token). Any logged-in user can read the setting through `GET + /settings/global/{key}`, which the token form uses to offer only expirations within it. The + settings API and config sync reject any value `parse_max_token_expiration_days` cannot read, since + the token routes would read it as no ceiling; `parseMaxTokenExpirationDays` in the frontend must + accept exactly the same values. +- **A token's label decides whether its expiry raises alerts.** When `delete_expired_items` removes + an expired `token` row, the monitor emails the owner and raises a critical alert (if enabled); + rows registered by `register_token_expiry_notification` also get an "expiring soon" warning first, + except a token whose whole lifetime fits in `TOKEN_EXPIRY_WARNING_DAYS` (7), which gets no row + since the warning would arrive minutes after it was created. Neither happens when `is_user_token` + (`windmill-common/src/auth.rs`) reserves the label, so a token the system mints for itself, + whether from the backend or from the frontend through `tokens/create`, needs a reserved label. An + `ephemeral-` prefix needs no other change (keep it clear of `is_server_minted_label` if minted + through `tokens/create`); a new prefix also goes into the SQL and Svelte mirrors that function's + doc lists. - **Every superadmin route refuses a job token**: `require_super_admin` (`windmill-api-auth/src/lib.rs`) errors on `authed.job_id.is_some()`. A script that needs `users/create`, `tokens/impersonate`, `set_login_type`, … must use a dedicated superadmin user @@ -29,8 +51,10 @@ Symbols, not line numbers, are cited: they drift less. the account into a `password` one in the same statement (an account created ahead of its owner gets its first credential that way, or through the OAuth claim below). - **Login links** (`login_link` table, `POST /users/login_links` superadmin-only, - `GET /auth/login_link/{token}` unauthenticated): single-use, ≤15 min, a session cookie and a - 302 to a same-origin `rd`. `require_login_type` on the mint refuses (409) an account whose + `GET /auth/login_link/{token}` unauthenticated): single-use, ≤2 h, a session cookie and a + 302 to a same-origin `rd`. A link minted with `confirm` is the `/user/login_link` page + instead, which spends it only on a click (`POST` to the same path, answering `{location}`), so + a mail scanner opening it does not. `require_login_type` on the mint refuses (409) an account whose `login_type` has moved on — the way a caller re-entering an account it created stops being able to once the owner has a password or a provider. - **Pre-approved trial offer** (`cloud_trial_offer`, cloud-only routes under diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 935a1aa475..f0c3ca61c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@windmill-labs/components", - "version": "1.814.0", + "version": "1.815.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@windmill-labs/components", - "version": "1.814.0", + "version": "1.815.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -41,8 +41,8 @@ "clone": "^2.1.2", "d3-zoom": "^3.0.0", "date-fns": "^2.30.0", - "diff": "^7.0.0", - "dompurify": "^3.3.1", + "diff": "^8.0.3", + "dompurify": "^3.4.13", "driver.js": "^1.3.0", "esm-env": "^1.0.0", "fast-equals": "^5.0.1", @@ -54,8 +54,8 @@ "lru-cache": "^11.1.0", "lucide-svelte": "^0.540.0", "mdast-util-find-and-replace": "^3.0.2", - "mermaid": "^11.15.0", - "minimatch": "^10.0.1", + "mermaid": "^11.16.1", + "minimatch": "^10.2.3", "modern-screenshot": "^4.7.0", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@=25.0.0", "monaco-languageclient": "10.6.0", @@ -67,7 +67,7 @@ "panzoom": "^9.4.3", "pdfjs-dist": "4.8.69", "quicktype-core": "^23.2.6", - "quill": "^1.3.7", + "quill": "^2.0.3", "rehype-github-alerts": "^3.0.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", @@ -102,7 +102,7 @@ "xterm-readline": "^1.1.2", "y-monaco": "^0.1.4", "y-websocket": "^1.5.4", - "yaml": "^2.8.0", + "yaml": "^2.8.3", "yjs": "^13.6.7", "zod": "^4.1.10" }, @@ -113,14 +113,13 @@ "@melt-ui/svelte": "^0.86.2", "@playwright/test": "^1.57.0", "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.53.4", + "@sveltejs/kit": "^2.69.1", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.8", "@types/d3": "^7.4.0", "@types/d3-zoom": "^3.0.3", - "@types/diff": "^7.0.1", "@types/lodash": "^4.14.195", "@types/vscode": "^1.83.5", "@typescript-eslint/eslint-plugin": "^5.59.8", @@ -144,7 +143,7 @@ "prettier-plugin-svelte": "^3.3.3", "style-to-object": "^0.4.1", "stylelint-config-recommended": "^13.0.0", - "svelte": "^5.53.5", + "svelte": "^5.55.7", "svelte-awesome-color-picker": "^3.0.4", "svelte-check": "^4.4.3", "svelte-fast-check": "^0.4.5", @@ -155,12 +154,12 @@ "svelte-range-slider-pips": "^2.3.1", "svelte-splitpanes": "^8.0.9", "tailwindcss": "^3.4.1", - "tar": "^7.5.4", + "tar": "^7.5.18", "tslib": "^2.6.1", "typescript": "^5.5.0", "vite": "^8.2.0", "vite-plugin-mkcert": "^2.0.0", - "vitest": "^4.1.0", + "vitest": "^4.1.11", "vitest-browser-svelte": "^2.0.1" }, "optionalDependencies": { @@ -1152,9 +1151,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1176,9 +1175,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1290,9 +1289,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1351,27 +1350,6 @@ "@swc/helpers": "^0.5.0" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1613,12 +1591,12 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz", - "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", + "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", "license": "MIT", "dependencies": { - "@chevrotain/types": "~11.1.1" + "@chevrotain/types": "~11.1.2" } }, "node_modules/@noble/hashes": { @@ -2045,9 +2023,9 @@ } }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", - "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -2064,18 +2042,18 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.53.4", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", - "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", + "version": "2.70.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.3.tgz", + "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==", "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", - "acorn": "^8.14.1", + "acorn": "^8.16.0", "cookie": "^0.6.0", - "devalue": "^5.6.3", + "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", @@ -2093,7 +2071,7 @@ "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3", + "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { @@ -2240,16 +2218,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@tutorlatin/svelte-tiny-virtual-list": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.16.tgz", @@ -2552,13 +2520,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/diff": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", - "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2642,7 +2603,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/unist": { "version": "3.0.3", @@ -2987,31 +2949,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3020,7 +2982,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -3032,26 +2994,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -3066,14 +3028,14 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3089,9 +3051,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -3099,15 +3061,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -3221,9 +3183,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3274,9 +3236,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3556,13 +3518,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", - "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "version": "2.11.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", + "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bezier-easing": { @@ -3614,9 +3579,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -3651,9 +3616,9 @@ "license": "MIT" }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", "dev": true, "funding": [ { @@ -3671,11 +3636,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -3781,28 +3746,11 @@ "node": ">=8.10.0" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3816,6 +3764,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3937,9 +3886,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001750", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -4263,9 +4212,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "devOptional": true, "license": "MIT", "engines": { @@ -5257,26 +5206,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-equal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", - "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", - "license": "MIT", - "dependencies": { - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.5.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -5319,44 +5248,10 @@ "node": ">=6" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "dev": true, "license": "MIT" }, @@ -5406,9 +5301,9 @@ } }, "node_modules/devalue": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", - "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.2.tgz", + "integrity": "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==", "license": "MIT" }, "node_modules/devlop": { @@ -5443,9 +5338,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -5542,9 +5437,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", + "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -5605,6 +5500,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5640,9 +5536,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.235", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", - "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", + "version": "1.5.430", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", + "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", "dev": true, "license": "ISC" }, @@ -5719,6 +5615,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5728,6 +5625,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5744,6 +5642,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5963,9 +5862,9 @@ } }, "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6014,9 +5913,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6065,9 +5964,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6148,15 +6047,6 @@ "node": ">=4.0" } }, - "node_modules/esrap": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", - "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -6220,9 +6110,9 @@ } }, "node_modules/eventemitter3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", - "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/events": { @@ -6277,9 +6167,9 @@ "license": "MIT" }, "node_modules/fast-diff": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", - "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "license": "Apache-2.0" }, "node_modules/fast-equals": { @@ -6343,9 +6233,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -6358,6 +6248,15 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastdom": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastdom/-/fastdom-1.0.12.tgz", + "integrity": "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==", + "license": "MIT", + "dependencies": { + "strictdom": "^1.0.1" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -6445,9 +6344,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -6479,17 +6378,17 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -6557,15 +6456,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6604,6 +6495,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6628,6 +6520,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6706,9 +6599,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6811,6 +6704,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6861,9 +6755,9 @@ } }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6903,22 +6797,11 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6931,6 +6814,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6949,9 +6833,10 @@ "license": "MIT" }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7350,22 +7235,6 @@ "node": ">=12" } }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7403,22 +7272,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7509,24 +7362,6 @@ "@types/estree": "^1.0.6" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", @@ -7604,10 +7439,20 @@ "peer": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7752,9 +7597,9 @@ } }, "node_modules/jsdom/node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, "license": "MIT", "engines": { @@ -7845,9 +7690,9 @@ } }, "node_modules/json-refs/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { @@ -8546,9 +8391,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { @@ -8711,6 +8556,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8896,9 +8742,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -9010,26 +8856,27 @@ } }, "node_modules/mermaid": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", - "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "version": "11.17.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.17.2.tgz", + "integrity": "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==", "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^7.1.1", + "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.1.1", + "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", - "cytoscape": "^3.33.1", + "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", - "dayjs": "^1.11.19", - "dompurify": "^3.3.1", + "dayjs": "^1.11.21", + "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", - "katex": "^0.16.25", + "fastdom": "1.0.12", + "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", @@ -9697,20 +9544,41 @@ } }, "node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "license": "ISC", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -9896,9 +9764,9 @@ } }, "node_modules/nanoid": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "dev": true, "funding": [ { @@ -10022,11 +9890,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-package-data": { "version": "3.0.3", @@ -10139,31 +10010,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -10356,9 +10202,9 @@ } }, "node_modules/parchment": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", - "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", "license": "BSD-3-Clause" }, "node_modules/parent-module": { @@ -10735,9 +10581,9 @@ } }, "node_modules/postcss-calc/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10953,9 +10799,9 @@ } }, "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11035,9 +10881,9 @@ } }, "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11075,9 +10921,9 @@ } }, "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11380,9 +11226,9 @@ } }, "node_modules/postcss-unique-selectors/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11401,9 +11247,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "devOptional": true, "funding": [ { @@ -11509,9 +11355,9 @@ } }, "node_modules/protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", "license": "MIT" }, "node_modules/prr": { @@ -11543,13 +11389,14 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -11676,31 +11523,32 @@ } }, "node_modules/quill": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", - "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", "license": "BSD-3-Clause", "dependencies": { - "clone": "^2.1.1", - "deep-equal": "^1.0.1", - "eventemitter3": "^2.0.3", - "extend": "^3.0.2", - "parchment": "^1.1.4", - "quill-delta": "^3.6.2" + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" } }, "node_modules/quill-delta": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", - "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", "license": "MIT", "dependencies": { - "deep-equal": "^1.0.1", - "extend": "^3.0.2", - "fast-diff": "1.1.2" + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" }, "engines": { - "node": ">=0.10" + "node": ">= 12.0.0" } }, "node_modules/rbush": { @@ -11873,26 +11721,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/rehype-github-alerts": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/rehype-github-alerts/-/rehype-github-alerts-3.0.0.tgz", @@ -12240,6 +12068,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -12279,38 +12117,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -12341,15 +12147,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -12361,14 +12167,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -12646,6 +12452,12 @@ "dev": true, "license": "MIT" }, + "node_modules/strictdom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz", + "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -12774,9 +12586,9 @@ } }, "node_modules/stylehacks/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12907,9 +12719,9 @@ "peer": true }, "node_modules/stylelint/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "peer": true, @@ -12969,9 +12781,9 @@ "license": "MIT" }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -12989,9 +12801,10 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -13010,13 +12823,13 @@ } }, "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -13094,23 +12907,22 @@ } }, "node_modules/svelte": { - "version": "5.53.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz", - "integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==", + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.0.tgz", + "integrity": "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.3", + "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.2", + "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -13420,6 +13232,38 @@ "svelte": "^4.2.19 || ^5.1.0" } }, + "node_modules/svelte/node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/svelte/node_modules/esrap": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.7.tgz", + "integrity": "sha512-n2nf7fZR3c9yXf0BPEuHuXqT+KW0SJVj4cN5FMEkpCZ3scLjOQWpiccyCxVzCC2q1wubTghuEGzngJY/7Ah0Ow==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, "node_modules/svelte2tsx": { "version": "0.7.49", "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.49.tgz", @@ -13443,19 +13287,19 @@ "peer": true }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.5.tgz", + "integrity": "sha512-8SQMzdrvWaD8deUmrnYB+ASyxBVgWUOilg+A75nE/76WdLpj6LopCwiAVvkzkcqy/9b7t2Mg7faFLjg0ZRcZ3w==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -13597,9 +13441,9 @@ } }, "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13624,9 +13468,9 @@ } }, "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -13786,9 +13630,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -14021,9 +13865,9 @@ } }, "node_modules/undici": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.1.0.tgz", - "integrity": "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==", + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", "dev": true, "license": "MIT", "engines": { @@ -14156,9 +14000,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -14418,19 +14262,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -14441,8 +14285,8 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14458,13 +14302,15 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -14485,6 +14331,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -14621,18 +14473,18 @@ "license": "MIT" }, "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -15158,9 +15010,9 @@ } }, "node_modules/y-websocket/node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.6.tgz", + "integrity": "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA==", "license": "MIT", "optional": true, "dependencies": { @@ -15188,15 +15040,18 @@ } }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/frontend/package.json b/frontend/package.json index eedca8f2c8..c81465a19a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@windmill-labs/components", - "version": "1.814.0", + "version": "1.815.0", "scripts": { "dev": "vite dev", "dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev", @@ -28,14 +28,13 @@ "@melt-ui/svelte": "^0.86.2", "@playwright/test": "^1.57.0", "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.53.4", + "@sveltejs/kit": "^2.69.1", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.8", "@types/d3": "^7.4.0", "@types/d3-zoom": "^3.0.3", - "@types/diff": "^7.0.1", "@types/lodash": "^4.14.195", "@types/vscode": "^1.83.5", "@typescript-eslint/eslint-plugin": "^5.59.8", @@ -59,7 +58,7 @@ "prettier-plugin-svelte": "^3.3.3", "style-to-object": "^0.4.1", "stylelint-config-recommended": "^13.0.0", - "svelte": "^5.53.5", + "svelte": "^5.55.7", "svelte-awesome-color-picker": "^3.0.4", "svelte-check": "^4.4.3", "svelte-fast-check": "^0.4.5", @@ -70,19 +69,22 @@ "svelte-range-slider-pips": "^2.3.1", "svelte-splitpanes": "^8.0.9", "tailwindcss": "^3.4.1", - "tar": "^7.5.4", + "tar": "^7.5.18", "tslib": "^2.6.1", "typescript": "^5.5.0", "vite": "^8.2.0", "vite-plugin-mkcert": "^2.0.0", - "vitest": "^4.1.0", + "vitest": "^4.1.11", "vitest-browser-svelte": "^2.0.1" }, "overrides": { "monaco-graphql": { "monaco-editor": "$monaco-editor" }, - "tar": "$tar" + "tar": "$tar", + "dompurify": "$dompurify", + "cookie": "^0.7.0", + "handlebars": "^4.7.9" }, "type": "module", "dependencies": { @@ -117,8 +119,8 @@ "clone": "^2.1.2", "d3-zoom": "^3.0.0", "date-fns": "^2.30.0", - "diff": "^7.0.0", - "dompurify": "^3.3.1", + "diff": "^8.0.3", + "dompurify": "^3.4.13", "driver.js": "^1.3.0", "esm-env": "^1.0.0", "fast-equals": "^5.0.1", @@ -130,8 +132,8 @@ "lru-cache": "^11.1.0", "lucide-svelte": "^0.540.0", "mdast-util-find-and-replace": "^3.0.2", - "mermaid": "^11.15.0", - "minimatch": "^10.0.1", + "mermaid": "^11.16.1", + "minimatch": "^10.2.3", "modern-screenshot": "^4.7.0", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@=25.0.0", "monaco-languageclient": "10.6.0", @@ -143,7 +145,7 @@ "panzoom": "^9.4.3", "pdfjs-dist": "4.8.69", "quicktype-core": "^23.2.6", - "quill": "^1.3.7", + "quill": "^2.0.3", "rehype-github-alerts": "^3.0.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", @@ -178,7 +180,7 @@ "xterm-readline": "^1.1.2", "y-monaco": "^0.1.4", "y-websocket": "^1.5.4", - "yaml": "^2.8.0", + "yaml": "^2.8.3", "yjs": "^13.6.7", "zod": "^4.1.10" }, diff --git a/frontend/src/lib/assets/app.css b/frontend/src/lib/assets/app.css index f6dc648234..4217c95831 100644 --- a/frontend/src/lib/assets/app.css +++ b/frontend/src/lib/assets/app.css @@ -3,7 +3,10 @@ @tailwind components; @tailwind utilities; -@media (min-width: 1760px) { +/* Keyed on the screen, not the window: a viewport query rescales the whole app + whenever the window is resized or split. Mirrored in editorFontSize.svelte.ts, + TextInput.svelte's leading classes and FlowGraphPreviewButton.svelte. */ +@media (min-device-width: 1760px) { :root { font-size: 18px; } diff --git a/frontend/src/lib/components/AIAgentLogViewer.svelte b/frontend/src/lib/components/AIAgentLogViewer.svelte deleted file mode 100644 index 2549fbb29f..0000000000 --- a/frontend/src/lib/components/AIAgentLogViewer.svelte +++ /dev/null @@ -1,281 +0,0 @@ - - -{#if job} -
- {}} - mode="aiagent" - /> -
-{/if} diff --git a/frontend/src/lib/components/AIProviderPicker.svelte b/frontend/src/lib/components/AIProviderPicker.svelte index 19b1346b84..f69bb6e8f4 100644 --- a/frontend/src/lib/components/AIProviderPicker.svelte +++ b/frontend/src/lib/components/AIProviderPicker.svelte @@ -3,11 +3,13 @@ import Select from './select/Select.svelte' import { fetchAvailableModels, AI_PROVIDERS } from './copilot/lib' import type { AIProvider, ProviderConfig } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import ResourcePicker from './ResourcePicker.svelte' import Toggle from './Toggle.svelte' import { saveConfig, removeConfig, isSameAsStoredConfig } from './aiProviderStorage' import AIReasoningEffortPicker from './AIReasoningEffortPicker.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { value: ProviderConfig | undefined @@ -26,7 +28,7 @@ workspace = undefined }: Props = $props() - let effectiveWorkspace = $derived(workspace ?? $workspaceStore ?? '') + let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace ?? '') let value = $derived.by(() => { if (!_uncheckedValue || typeof _uncheckedValue !== 'object') return undefined diff --git a/frontend/src/lib/components/AgentResultDisplay.svelte b/frontend/src/lib/components/AgentResultDisplay.svelte new file mode 100644 index 0000000000..0388ab0845 --- /dev/null +++ b/frontend/src/lib/components/AgentResultDisplay.svelte @@ -0,0 +1,116 @@ + + +
+ {#if trace.length > 0} + + {/if} + {#if reasoning} + + (reasoningExpanded = !reasoningExpanded)} + contentClass="font-main" + > + + + {/if} + {#if trace.length > 0 || reasoning} + + Output + + {/if} +
+ {#if textOutput !== undefined} + {#if textOutput === ''} + The agent returned no answer + {:else} + + + {/if} + {:else} + {@render structuredOutput(result.output)} + {/if} + {#if answer.sources} +
+ +
+ {/if} +
+ + +
+ {#if summary.toolCalls > 0} + + {summary.toolCalls} + {summary.toolCalls === 1 ? 'tool call' : 'tool calls'} + + {/if} + {#if summary.webSearches > 0} + + {summary.webSearches} + {summary.webSearches === 1 ? 'web search' : 'web searches'} + + {/if} + {#if summary.tokens !== undefined} + {formatTokenCount(summary.tokens)} tokens + {/if} + {#if summary.cachedTokens} + {formatTokenCount(summary.cachedTokens)} cached + {/if} +
+
diff --git a/frontend/src/lib/components/AgentStreamDisplay.svelte b/frontend/src/lib/components/AgentStreamDisplay.svelte new file mode 100644 index 0000000000..cf1646a614 --- /dev/null +++ b/frontend/src/lib/components/AgentStreamDisplay.svelte @@ -0,0 +1,111 @@ + + + +
+ {#each stream.entries as entry, index (entry.kind === 'tool' ? entry.callId : index)} + {#if entry.kind === 'tool'} + {}} + labelClass={entry.success === false ? 'text-red-500' : ''} + /> + {:else} +
+ +
+ {/if} + {/each} + + {#if stream.current !== ''} +
+ + +
+ {:else if stream.reasoning !== ''} + +
+ +
+ {/if} +
diff --git a/frontend/src/lib/components/AgentTrace.svelte b/frontend/src/lib/components/AgentTrace.svelte new file mode 100644 index 0000000000..91b87fea67 --- /dev/null +++ b/frontend/src/lib/components/AgentTrace.svelte @@ -0,0 +1,137 @@ + + +
+ {#each entries as entry, index (index)} + {#if entry.kind === 'assistant'} +
+ + {#if entry.sources} +
+ +
+ {/if} +
+ {:else if entry.kind === 'search'} + + {}} + /> + {:else} + {@const job = jobOf(entry.jobId)} + toggle(index, entry)} + contentClass="space-y-3" + > + {#if entry.args} + + {/if} + {#if job?.logs} + + {/if} + + {#if entry.resourcePath} +
+ + {entry.resourcePath} +
+ {:else if entry.jobId} + + + Open job + + {/if} +
+ {/if} + {/each} +
diff --git a/frontend/src/lib/components/ApiConnectForm.svelte b/frontend/src/lib/components/ApiConnectForm.svelte index dedeee90e2..2255817c5e 100644 --- a/frontend/src/lib/components/ApiConnectForm.svelte +++ b/frontend/src/lib/components/ApiConnectForm.svelte @@ -1,7 +1,6 @@ @@ -20,7 +22,7 @@ Setup the wmill cli for this workspace & remote:
diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index ddab0681be..4775409252 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -1,7 +1,7 @@ -{#if $userStore?.is_admin || $userStore?.is_super_admin} +{#if actingUser?.is_admin || actingUser?.is_super_admin} diff --git a/frontend/src/lib/components/DefaultScriptsInner.svelte b/frontend/src/lib/components/DefaultScriptsInner.svelte index eaa3d989a0..b062fb4c49 100644 --- a/frontend/src/lib/components/DefaultScriptsInner.svelte +++ b/frontend/src/lib/components/DefaultScriptsInner.svelte @@ -1,10 +1,13 @@
@@ -46,9 +47,6 @@ {onClear} /> {#if showSchemaExplorer && value && assetCanBeExplored({ kind: 'ducklake', path: value })} - + {/if}
diff --git a/frontend/src/lib/components/EditableSchemaForm.svelte b/frontend/src/lib/components/EditableSchemaForm.svelte index bd3399d9d2..79173424d9 100644 --- a/frontend/src/lib/components/EditableSchemaForm.svelte +++ b/frontend/src/lib/components/EditableSchemaForm.svelte @@ -4,7 +4,6 @@ const bubble = createBubbler() import type { Schema } from '$lib/common' import { VariableService, type ScriptLang } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import { Button } from './common' import ItemPicker from './ItemPicker.svelte' import VariableEditor from './VariableEditor.svelte' @@ -36,6 +35,9 @@ import Section from '$lib/components/Section.svelte' import Editor from './Editor.svelte' import AddPropertyV2 from './schema/AddPropertyV2.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() // export let openEditTab: () => void = () => {} const dispatch = createEventDispatcher() @@ -128,7 +130,7 @@ workspace = undefined }: Props = $props() - let ws = $derived(workspace ?? $workspaceStore) + let ws = $derived(workspace ?? $operatingWorkspace) $effect.pre(() => { if (args == undefined) { diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 987aa937eb..6aabc30c2b 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -42,7 +42,6 @@ import { editorConfig, registerWebviewPaste, updateOptions } from '$lib/editorUtils' import { editorFontSize } from '$lib/editorFontSize.svelte' import { createHash as randomHash } from '$lib/editorLangUtils' - import { workspaceStore } from '$lib/stores' import DdlMigrationGuard from './DdlMigrationGuard.svelte' import { type Preview, @@ -120,6 +119,9 @@ import { rawAppLintStore, type MonacoLintError } from './raw_apps/lintStore' import { MarkerSeverity } from 'monaco-editor' import { resource, useDebounce, watch } from 'runed' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() // import EditorTheme from './EditorTheme.svelte' let divEl: HTMLDivElement | null = $state(null) @@ -684,7 +686,7 @@ // via a short-TTL cache — macros are late-bound, so mild staleness is fine. async function addWorkspaceMacroCompletions() { workspaceMacroCompletor?.dispose() - const workspace = $workspaceStore + const workspace = $operatingWorkspace if (!workspace) return let macros: Awaited> = [] try { @@ -739,7 +741,7 @@ provideCompletionItems: async function (model, position) { // Read the store per request, not at registration — the provider // outlives a workspace switch. - const workspace = $workspaceStore + const workspace = $operatingWorkspace if (!workspace) return { suggestions: [] } const before = model.getLineContent(position.lineNumber).slice(0, position.column - 1) if (!/^\s*(\/\/|--|#)\s*(column|data_test|on|materialize)\b/.test(before)) { @@ -780,7 +782,7 @@ $dbSchemas[resourcePath] = await getDbSchemas( lang === 'graphql' ? 'graphql' : (scriptLang ?? ''), resourcePath, - $workspaceStore, + $operatingWorkspace, (e) => console.error(`error getting ${lang} (${scriptLang}) db schema`, e), { customTag } ) @@ -1778,9 +1780,9 @@ let customTsTypesData = resource([() => lang], async () => { if (lang !== 'typescript') return undefined let datatables = ( - await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' }) + await WorkspaceService.listDataTables({ workspace: $operatingWorkspace ?? '' }) ).map((d) => d.name) - let ducklakes = await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' }) + let ducklakes = await WorkspaceService.listDucklakes({ workspace: $operatingWorkspace ?? '' }) return { datatables, ducklakes } }) function setTypescriptCustomTypes() { @@ -1822,7 +1824,7 @@ scriptLang === 'nativets') ) { const resourceTypes = await ResourceService.listResourceType({ - workspace: $workspaceStore ?? '' + workspace: $operatingWorkspace ?? '' }) const namespace = formatResourceTypes( @@ -2023,7 +2025,7 @@ $lspTokenStore = newToken token = newToken } - let root = hostname + '/api/scripts_u/tokened_raw/' + $workspaceStore + '/' + token + let root = hostname + '/api/scripts_u/tokened_raw/' + $operatingWorkspace + '/' + token return root } @@ -2274,10 +2276,10 @@ -{#if datatableForMigrations && $workspaceStore} +{#if datatableForMigrations && $operatingWorkspace} {/if} diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 91516843ab..81ef6c1c96 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -22,7 +22,6 @@ {/snippet}
-
+{/snippet} diff --git a/frontend/src/lib/components/ResourceForm.svelte b/frontend/src/lib/components/ResourceForm.svelte index df1aa5bd99..1d4f091236 100644 --- a/frontend/src/lib/components/ResourceForm.svelte +++ b/frontend/src/lib/components/ResourceForm.svelte @@ -1,4 +1,5 @@ - clearPageDrawerAnchor(VARIABLES_PATH)}> +{#if inline} + {@render content()} +{:else} + clearPageDrawerAnchor(VARIABLES_PATH)}> + {@render content()} + +{/if} + +{#snippet content()} (inline ? onClose?.() : drawer?.closeDrawer())} > {#snippet banner()} {/snippet} - +{/snippet} diff --git a/frontend/src/lib/components/VariableForm.svelte b/frontend/src/lib/components/VariableForm.svelte index 61945eef08..63fdbf7425 100644 --- a/frontend/src/lib/components/VariableForm.svelte +++ b/frontend/src/lib/components/VariableForm.svelte @@ -10,10 +10,13 @@ import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' import { Loader2, RotateCcw } from 'lucide-svelte' import autosize from '$lib/autosize' - import { workspaceStore, type UserExt } from '$lib/stores' + import { type UserExt } from '$lib/stores' import { isOwner } from '$lib/utils' import { isEncryptedDraftValue } from '$lib/encryptedDraft' import EncryptedDraftField from './EncryptedDraftField.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Variable { value: string @@ -56,7 +59,7 @@ actingUser }: Props = $props() - let ws = $derived(workspace ?? $workspaceStore) + let ws = $derived(workspace ?? $operatingWorkspace) // Loading the deployed secret overwrites the draft row this form shares with the AI // chat, so every path that would trigger it has to be blocked while that row stages a diff --git a/frontend/src/lib/components/WorkerTagPicker.svelte b/frontend/src/lib/components/WorkerTagPicker.svelte index ccc3266ced..dae4967e05 100644 --- a/frontend/src/lib/components/WorkerTagPicker.svelte +++ b/frontend/src/lib/components/WorkerTagPicker.svelte @@ -2,6 +2,7 @@ import { Button } from '$lib/components/common' import { ExternalLink, RotateCw, Loader2 } from 'lucide-svelte' import { workerTags, workspaceStore } from '$lib/stores' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' import AssignableTags from './AssignableTags.svelte' import { WorkerService } from '$lib/gen' import WorkerTagSelect from './WorkerTagSelect.svelte' @@ -11,8 +12,8 @@ popupPlacement?: 'bottom-end' | 'top-end' disabled?: boolean placeholder?: string - // Workspace to read tags from; defaults to $workspaceStore. A fork-scoped - // session passes its effective workspace so the picker matches the deploy target. + // Workspace to read tags from; defaults to the operating workspace (see + // `useOperatingWorkspace`). workspaceId?: string } @@ -26,8 +27,11 @@ // See WorkerTagSelect: the shared `workerTags` cache is navigation-scoped, so a // different target workspace reads/writes a local list to avoid clobbering it. - let effectiveWorkspace = $derived(workspaceId ?? $workspaceStore) - let usesLocal = $derived(workspaceId != undefined && workspaceId !== $workspaceStore) + const operatingWorkspace = useOperatingWorkspace() + let effectiveWorkspace = $derived(workspaceId ?? $operatingWorkspace) + let usesLocal = $derived( + effectiveWorkspace != undefined && effectiveWorkspace !== $workspaceStore + ) let localWorkerTags = $state(undefined) let currentTags = $derived(usesLocal ? localWorkerTags : $workerTags) diff --git a/frontend/src/lib/components/WorkerTagSelect.svelte b/frontend/src/lib/components/WorkerTagSelect.svelte index fe00f07f9d..624aeb1bcd 100644 --- a/frontend/src/lib/components/WorkerTagSelect.svelte +++ b/frontend/src/lib/components/WorkerTagSelect.svelte @@ -1,5 +1,6 @@ {#if !onLatest} - - By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff. + + {headUnknown + ? "Deploying may overwrite changes made by other users. Press 'Deploy' to see diff." + : "By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff."}
{/if} @@ -596,7 +613,7 @@ {/if}
- {#if !($userStore?.is_admin || $userStore?.is_super_admin)} + {#if !(actingUser?.is_admin || actingUser?.is_super_admin)} Custom path can only be set by workspace admins @@ -617,7 +634,7 @@ options={{ right: 'Use a custom URL' }} - disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} + disabled={!$enterpriseLicense || !(actingUser?.is_admin || actingUser?.is_super_admin)} /> {#if customPath !== undefined} @@ -625,7 +642,7 @@
Custom path
@@ -192,7 +194,7 @@
@@ -219,7 +221,7 @@ {#if job != undefined && 'result' in job && job?.result != undefined}
diff --git a/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte b/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte index 614d1f4fa4..63bc17a8eb 100644 --- a/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte +++ b/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte @@ -14,12 +14,14 @@ } from '$lib/utils' import { orderedYamlStringify } from '$lib/utils/orderedYaml' import { AppService, type AppWithLastVersion, type AppHistory } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import { Skeleton } from '$lib/components/common' import Button from '$lib/components/common/button/Button.svelte' import { createEventDispatcher, untrack } from 'svelte' import { Pencil, ArrowRight, X, Loader2 } from 'lucide-svelte' import Select from '$lib/components/select/Select.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { appPath: string | undefined @@ -48,7 +50,7 @@ return cached } - const app = await AppService.getAppByVersion({ workspace: $workspaceStore!, id: version }) + const app = await AppService.getAppByVersion({ workspace: $operatingWorkspace!, id: version }) versionCache[version] = app return app } @@ -60,7 +62,7 @@ loading = true versions = await AppService.getAppHistoryByPath({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, path: appPath }) loading = false @@ -90,7 +92,7 @@ return } await AppService.updateAppHistory({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, id: appId, version: appVersion, requestBody: { @@ -107,7 +109,9 @@ } function toVersionLabel(version: AppHistory): string { - return emptyString(version.deployment_msg) ? `Version ${version.version}` : version.deployment_msg! + return emptyString(version.deployment_msg) + ? `Version ${version.version}` + : version.deployment_msg! } let availableVersions = $derived( diff --git a/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts index 122c42bcf9..f799cb864a 100644 --- a/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts +++ b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts @@ -1,7 +1,7 @@ -import { base } from "$lib/base" -import { workspaceStore } from "$lib/stores" -import { get } from "svelte/store" +import { base } from '$lib/base' +import { workspaceStore } from '$lib/stores' +import { get } from 'svelte/store' export function computeSecretUrl(secretUrl: string) { - return `${window.location.origin}${base}/public/${get(workspaceStore)}/${secretUrl}` + return `${window.location.origin}${base}/public/${get(workspaceStore)}/${secretUrl}` } diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte index 3089809f12..293d83b4ae 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte @@ -19,7 +19,6 @@ CtxAppInput } from '../../inputType' import type { AppViewerContext } from '../../types' - import { workspaceStore } from '$lib/stores' import { createEventDispatcher } from 'svelte' import { deepEqual } from 'fast-equals' import { computeFields } from './utils' @@ -35,6 +34,9 @@ import FlowEditorDrawer from '$lib/components/flows/content/FlowEditorDrawer.svelte' import { FlowService, ScriptService, type OpenFlow } from '$lib/gen' import { replaceScriptPlaceholderWithItsValues } from '$lib/hub' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { runnable: RunnableByPath @@ -131,7 +133,7 @@ return } - const loaded = await loadSchema($workspaceStore ?? '', runnable.path, 'flow') + const loaded = await loadSchema($operatingWorkspace ?? '', runnable.path, 'flow') const schema = loaded?.schema ?? emptySchema() if (!deepEqual(runnable.schema, schema)) { runnable.schema = schema @@ -167,7 +169,7 @@ async function openScriptEditor(path: string) { try { const script = await ScriptService.getScriptByPath({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, path }) scriptEditorDrawer?.openDrawer(script.hash, () => { @@ -344,7 +346,7 @@ startIcon={{ icon: Eye }} endIcon={{ icon: ExternalLink }} target="_blank" - href="{base}/flows/get/{runnable.path}?workspace={$workspaceStore}" + href="{base}/flows/get/{runnable.path}?workspace={$operatingWorkspace}" > Details @@ -425,7 +427,7 @@ {#if runnable.runType == 'flow' && isHubFlowPath(runnable.path)} Hub flow not found at {runnable.path} {:else} - {runnable.runType} not found at {runnable.path} in workspace {$workspaceStore} + {runnable.runType} not found at {runnable.path} in workspace {$operatingWorkspace} {/if}
{:else if runnable.runType == 'script' || runnable.runType == 'hubscript'} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte index 8543fc0683..08b8ac6219 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte @@ -11,15 +11,12 @@ import type { Schema } from '$lib/common' import { emptySchema } from '$lib/utils' import { loadSchema } from '$lib/infer' - import { workspaceStore } from '$lib/stores' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() import { buildPathRunnableSelection } from './runnableSelectorUtils' - type TabType = - | 'hubscripts' - | 'hubflows' - | 'workspacescripts' - | 'workspaceflows' - | 'inlinescripts' + type TabType = 'hubscripts' | 'hubflows' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts' interface Props { defaultUserInput?: boolean @@ -60,7 +57,7 @@ path: string, runType: 'script' | 'flow' | 'hubscript' ): Promise<{ schema: Schema; summary: string | undefined }> { - const schema = await loadSchema($workspaceStore!, path, runType) + const schema = await loadSchema($operatingWorkspace!, path, runType) if (!schema.schema.order) { schema.schema.order = Object.keys(schema.schema.properties ?? {}) } diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceFlowList.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceFlowList.svelte index b2cc91fc01..7831e83458 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceFlowList.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceFlowList.svelte @@ -7,9 +7,11 @@ import NoItemFound from '$lib/components/home/NoItemFound.svelte' import RowIcon from '$lib/components/common/table/RowIcon.svelte' import { FlowService, type Flow } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import { emptyString } from '$lib/utils' import { Skeleton } from '$lib/components/common' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { filter?: string @@ -26,7 +28,7 @@ async function loadFlow(): Promise { const loadedFlows = await FlowService.listFlows({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, perPage: 300, withoutDescription: true }) diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceScriptList.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceScriptList.svelte index be261ca5e8..13447c4787 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceScriptList.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/WorkspaceScriptList.svelte @@ -7,9 +7,11 @@ import NoItemFound from '$lib/components/home/NoItemFound.svelte' import RowIcon from '$lib/components/common/table/RowIcon.svelte' import { type Script, ScriptService } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import { emptyString } from '$lib/utils' import { Skeleton } from '$lib/components/common' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { filter?: string @@ -26,7 +28,7 @@ async function loadScripts(): Promise { const loadedScripts = await ScriptService.listScripts({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, perPage: 300, withoutDescription: true }) diff --git a/frontend/src/lib/components/apps/types.ts b/frontend/src/lib/components/apps/types.ts index 960e273ac3..75de2d7faf 100644 --- a/frontend/src/lib/components/apps/types.ts +++ b/frontend/src/lib/components/apps/types.ts @@ -183,6 +183,11 @@ export interface AppEditorProps { // AppEditorHeader as a callback prop rather than `on:restore` forwarding, // which does not propagate through these runes-mode components. onRestore?: (restoredApp: any) => void + // Fired after a successful deploy, which keeps this editor open: `version` is what + // the deploy wrote, for the next draft's fork base, and `head` what is deployed now, + // with its author and time. `version` and `head` differ when another deploy landed + // beside this one, which is when the out-of-date prompt has something to say. + onDeploy?: (e: { version?: number; head?: number; headBy?: string; headAt?: string }) => void } export type App = { diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte index 4b252b9338..6050c2d1cd 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte @@ -48,7 +48,10 @@ import AssetRunsPanel from './AssetRunsPanel.svelte' import { Pane, Splitpanes } from 'svelte-splitpanes' import { fade } from 'svelte/transition' - import { userStore } from '$lib/stores' + import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte' + + const operatingUser = useOperatingUser() + const actingUser = $derived(operatingUser.current) interface Props { // Regular selection — loads the script by path for inline editing. @@ -662,7 +665,7 @@ // hand-rolled here because we need *two* confirm buttons, not one. let removeOpen = $state(false) let removing = $state(false) - let canHardDelete = $derived(!!($userStore?.is_admin || $userStore?.is_super_admin)) + let canHardDelete = $derived(!!(actingUser?.is_admin || actingUser?.is_super_admin)) // React to the parent's remove-signal counter and pop the same modal // the in-pane trash button uses. Skipped for drafts (the parent calls diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte index 815db36b18..e719bfe2bd 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte @@ -19,12 +19,15 @@ XCircle } from 'lucide-svelte' import type { ScriptLang } from '$lib/gen' - import { enterpriseLicense, workspaceStore } from '$lib/stores' + import { enterpriseLicense } from '$lib/stores' import { sendUserToast } from '$lib/utils' import { PIPELINE_LANGUAGES } from './pipelineLanguages' import type { PipelineOutputKind } from './pipelineTemplates' import type { DbtAssetProvenance } from './types' import DbtIcon from '$lib/components/icons/DbtIcon.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() // Shape used for both the data prop and the run callback. Drafts carry // `content` / `language` so the page-level run handler can dispatch to @@ -118,7 +121,7 @@ async function runProducers(e: MouseEvent) { e.stopPropagation() - if (!$workspaceStore || running || !data.onRunProducer) return + if (!$operatingWorkspace || running || !data.onRunProducer) return if (scriptProducers.length === 0) return running = true const handler = data.onRunProducer @@ -170,14 +173,14 @@ } const cols = Object.entries(d.columns ?? {}) if (cols.length) { - lines.push( - `columns: ${cols.map(([c, desc]) => (desc ? `${c} (${desc})` : c)).join(', ')}` - ) + lines.push(`columns: ${cols.map(([c, desc]) => (desc ? `${c} (${desc})` : c)).join(', ')}`) } if (d.freshness) { const f = d.freshness as Record const window = (k: string) => - f[k]?.count != null ? `${k.replace('_after', '')} after ${f[k].count}${f[k].period?.[0] ?? ''}` : '' + f[k]?.count != null + ? `${k.replace('_after', '')} after ${f[k].count}${f[k].period?.[0] ?? ''}` + : '' const windows = ['warn_after', 'error_after'].map(window).filter(Boolean) if (windows.length) lines.push(`freshness: ${windows.join(', ')}`) } diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte index f43259b561..7cee1deb76 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte @@ -8,7 +8,6 @@ // seconds so the user sees status transitions without refreshing — // JobLoader handles streaming for the *selected* job. import { JobService, type Job } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import { onDestroy, untrack } from 'svelte' import { displayDate } from '$lib/utils' import { CheckCircle2, Clock, History, Loader2, XCircle, Ban } from 'lucide-svelte' @@ -21,6 +20,9 @@ import { Popover } from '$lib/components/meltComponents' import { twMerge } from 'tailwind-merge' import DispatchEventsButton from '$lib/components/runs/DispatchEventsButton.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { // Producers of this asset. Each contributes its own listExtendedJobs @@ -88,7 +90,7 @@ // Run only when the *content* of the producer set changes (string // key) or when the parent explicitly bumps `refreshKey` after // dispatching a new run. The producerKey debounce alone isn't - // enough: refresh() synchronously reads $workspaceStore and + // enough: refresh() synchronously reads $operatingWorkspace and // runnableProducers before the first await, and Svelte 5 records // those as deps of the surrounding $effect — which would then // re-fire whenever the parent re-derives `producers` (i.e. on @@ -174,11 +176,11 @@ async function refresh(): Promise { if (refreshInFlight) return refreshInFlight - if (!$workspaceStore || runnableProducers.length === 0) { + if (!$operatingWorkspace || runnableProducers.length === 0) { jobs = [] return } - const ws = $workspaceStore + const ws = $operatingWorkspace // Capture the producer paths *now* — using runnableProducers // directly inside the await would re-read after the array // identity churned, defeating the in-flight guard. @@ -286,12 +288,12 @@ Open ↗ - {#if $workspaceStore} + {#if $operatingWorkspace} {/if} @@ -397,7 +399,7 @@
diff --git a/frontend/src/lib/components/assets/AssetGraph/DataTablePreview.svelte b/frontend/src/lib/components/assets/AssetGraph/DataTablePreview.svelte index 1b86fa6853..5f5dc98b60 100644 --- a/frontend/src/lib/components/assets/AssetGraph/DataTablePreview.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/DataTablePreview.svelte @@ -9,7 +9,6 @@ // just be noise. import DBTable from '$lib/components/DBTable.svelte' import { resource } from 'runed' - import { workspaceStore } from '$lib/stores' import { loadAllTablesMetaData } from '$lib/components/apps/components/display/dbtable/metadata' import { dbTableOpsWithPreviewScripts } from '$lib/components/dbOps' import { WorkspaceService } from '$lib/gen' @@ -19,6 +18,9 @@ import Button from '$lib/components/common/button/Button.svelte' import { base } from '$lib/base' import { twMerge } from 'tailwind-merge' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { // Asset path as parsed from `datatable:///` — @@ -71,7 +73,7 @@ // query, and the user needs to configure it before pipeline scripts // can write to it. let datatables = resource( - () => $workspaceStore, + () => $operatingWorkspace, async (ws) => { if (!ws) return [] as string[] try { @@ -117,9 +119,9 @@ () => [input, refreshKey], async ([_input]) => { colDefsError = undefined - if (!_input || !$workspaceStore) return undefined + if (!_input || !$operatingWorkspace) return undefined try { - return await loadAllTablesMetaData($workspaceStore, _input) + return await loadAllTablesMetaData($operatingWorkspace, _input) } catch (e) { colDefsError = (e as Error)?.message || String(e) return undefined @@ -138,12 +140,12 @@ }) let dbTableOps = $derived( - input && tableColDefs && parsed.table && $workspaceStore + input && tableColDefs && parsed.table && $operatingWorkspace ? dbTableOpsWithPreviewScripts({ input, tableKey: parsed.table, colDefs: tableColDefs, - workspace: $workspaceStore + workspace: $operatingWorkspace }) : undefined ) diff --git a/frontend/src/lib/components/assets/AssetGraph/DucklakeResultPreview.svelte b/frontend/src/lib/components/assets/AssetGraph/DucklakeResultPreview.svelte index f21ca486af..d2d3c03b8a 100644 --- a/frontend/src/lib/components/assets/AssetGraph/DucklakeResultPreview.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/DucklakeResultPreview.svelte @@ -9,7 +9,6 @@ // ("This partition") or show the full table ("Whole table") via a toggle. import DBTable from '$lib/components/DBTable.svelte' import { resource } from 'runed' - import { workspaceStore } from '$lib/stores' import { loadAllTablesMetaData } from '$lib/components/apps/components/display/dbtable/metadata' import { dbTableOpsWithPreviewScripts } from '$lib/components/dbOps' import type { DbInput } from '$lib/components/dbTypes' @@ -18,6 +17,9 @@ import { twMerge } from 'tailwind-merge' import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { // Full asset URI, e.g. `ducklake://main/orders_daily`. @@ -61,9 +63,9 @@ () => [input, refreshKey] as const, async ([_input]) => { colDefsError = undefined - if (!_input || !$workspaceStore) return undefined + if (!_input || !$operatingWorkspace) return undefined try { - return await loadAllTablesMetaData($workspaceStore, _input) + return await loadAllTablesMetaData($operatingWorkspace, _input) } catch (e) { colDefsError = (e as Error)?.message || String(e) return undefined @@ -89,12 +91,12 @@ }) let dbTableOps = $derived.by(() => { - if (!(input && tableColDefs && tableKey && $workspaceStore)) return undefined + if (!(input && tableColDefs && tableKey && $operatingWorkspace)) return undefined const ops = dbTableOpsWithPreviewScripts({ input, tableKey, colDefs: tableColDefs, - workspace: $workspaceStore, + workspace: $operatingWorkspace, whereClause }) // Read-only preview: drop the mutation handlers so DBTable hides its diff --git a/frontend/src/lib/components/assets/AssetGraph/DucklakeVersionPreview.svelte b/frontend/src/lib/components/assets/AssetGraph/DucklakeVersionPreview.svelte index 5afb51fe66..35eff3beae 100644 --- a/frontend/src/lib/components/assets/AssetGraph/DucklakeVersionPreview.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/DucklakeVersionPreview.svelte @@ -7,7 +7,6 @@ // hand is shown above the grid so the affordance is self-documenting. import DBTable from '$lib/components/DBTable.svelte' import { resource } from 'runed' - import { workspaceStore } from '$lib/stores' import { fetchDucklakeColumnsAtVersion, dbTableOpsWithPreviewScripts @@ -17,6 +16,9 @@ import { AlertTriangle, Loader2, ClipboardCopy } from 'lucide-svelte' import { Button } from '$lib/components/common' import { twMerge } from 'tailwind-merge' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { // Full asset URI, e.g. `ducklake://main/orders_daily`. @@ -56,9 +58,10 @@ let columns = resource( () => [ducklake, tableKey, version] as const, async ([_ducklake, _tableKey, _version]) => { - if (!_ducklake || !_tableKey || _version == undefined || !$workspaceStore) return undefined + if (!_ducklake || !_tableKey || _version == undefined || !$operatingWorkspace) + return undefined const colDefs = await fetchDucklakeColumnsAtVersion({ - workspace: $workspaceStore, + workspace: $operatingWorkspace, ducklake: _ducklake, tableKey: _tableKey, version: _version @@ -73,13 +76,13 @@ let tableColDefs = $derived(ready ? columns.current!.colDefs : undefined) let dbTableOps = $derived.by(() => { - if (!(input && tableColDefs && tableKey && $workspaceStore && version != undefined)) + if (!(input && tableColDefs && tableKey && $operatingWorkspace && version != undefined)) return undefined const ops = dbTableOpsWithPreviewScripts({ input, tableKey, colDefs: tableColDefs, - workspace: $workspaceStore, + workspace: $operatingWorkspace, version }) // Historical reads are immutable: drop every mutation handler so DBTable diff --git a/frontend/src/lib/components/assets/AssetGraph/PipelineScriptView.svelte b/frontend/src/lib/components/assets/AssetGraph/PipelineScriptView.svelte index 79a1cf6d47..2450595b6d 100644 --- a/frontend/src/lib/components/assets/AssetGraph/PipelineScriptView.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/PipelineScriptView.svelte @@ -6,7 +6,9 @@ import PipelineRunForm from './PipelineRunForm.svelte' import AssetRunsPanel from './AssetRunsPanel.svelte' import { Pane, Splitpanes } from 'svelte-splitpanes' - import { workspaceStore } from '$lib/stores' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() import { parsePipelineAnnotations } from './parsePipelineAnnotations' interface Props { @@ -201,7 +203,7 @@ bind:args bind:isValid {partitionSpec} - workspace={$workspaceStore ?? ''} + workspace={$operatingWorkspace ?? ''} {materializeTarget} {upstreamAssets} /> diff --git a/frontend/src/lib/components/assets/AssetGraph/PipelineTriggerEditors.svelte b/frontend/src/lib/components/assets/AssetGraph/PipelineTriggerEditors.svelte index e364b29883..4f97039a1c 100644 --- a/frontend/src/lib/components/assets/AssetGraph/PipelineTriggerEditors.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/PipelineTriggerEditors.svelte @@ -1,5 +1,4 @@ { open = false @@ -33,7 +44,11 @@ on:confirmed={() => confirmCallback()} >
- A new version was deployed while you were editing this one. + + {headUnknown + ? 'This editor could not check whether a newer version is deployed, so it cannot tell whether this overwrites newer work.' + : 'A new version was deployed while you were editing this one.'} + {#if diffDrawer} diff --git a/frontend/src/lib/components/common/confirmationModal/DraftChangesConfirmationModal.svelte b/frontend/src/lib/components/common/confirmationModal/DraftChangesConfirmationModal.svelte index dadf814a80..5a51b4cfba 100644 --- a/frontend/src/lib/components/common/confirmationModal/DraftChangesConfirmationModal.svelte +++ b/frontend/src/lib/components/common/confirmationModal/DraftChangesConfirmationModal.svelte @@ -9,9 +9,12 @@ import { Bot, Star } from 'lucide-svelte' import ToggleButtonGroup from '../toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '../toggleButton-v2/ToggleButton.svelte' - import { userStore } from '$lib/stores' import Badge from '../badge/Badge.svelte' import type { LinkedAgentDraft } from '$lib/components/flows/linkedAgentDrafts' + import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte' + + const operatingUser = useOperatingUser() + const actingUser = $derived(operatingUser.current) interface Props { open?: boolean @@ -80,8 +83,8 @@ // Creating http trigger is forbidden for non-admin users const adminOnly = trigger.type === 'http' && - !$userStore?.is_admin && - !$userStore?.is_super_admin && + !actingUser?.is_admin && + !actingUser?.is_super_admin && trigger.isDraft const invalidConfig = !trigger.draftConfig?.canSave diff --git a/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte b/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte index 26366325b5..20c29286b5 100644 --- a/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte +++ b/frontend/src/lib/components/common/confirmationModal/DraftEditorModals.svelte @@ -13,6 +13,10 @@ import type { UserDraftItemKind } from '$lib/gen' import DraftSyncConflictModal from './DraftSyncConflictModal.svelte' import OtherUsersDraftsModal, { type OtherDraftUser } from './OtherUsersDraftsModal.svelte' + import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte' + import { sendUserToast } from '$lib/toast' + import { base } from '$app/paths' + import { goto } from '$app/navigation' import StaleDraftModal from './StaleDraftModal.svelte' import ConfirmationModal from './ConfirmationModal.svelte' import { OtherUserDraftLoad } from '$lib/components/otherUserDraftLoad.svelte' @@ -38,15 +42,27 @@ draftSavedAt?: string | undefined /** ISO timestamp of the latest deploy at this path. */ deployedAt?: string | undefined - /** Precise staleness inputs (flows/apps): the deployed version the draft was - * forked from, and the current deployed head. When both are set they drive - * `isStale` and the dedup key instead of the timestamps — exact, and stable - * across autosaves (the timestamp drifts past `deployedAt` as you keep - * editing). Absent (pre-feature drafts, scripts) ⇒ timestamp fallback. */ - draftBaseVersion?: number | undefined - deployedHeadVersion?: number | undefined + /** Precise staleness inputs: the deployed version the draft forked from + * (`draft_base` on the get-by-path response) and the current deployed head, + * both as text whatever the kind. When both are set they drive `isStale` and + * the dedup key instead of the timestamps, which drift past `deployedAt` as + * you keep editing. Absent (a draft never forked from a deploy) ⇒ timestamp + * fallback. */ + draftBaseVersion?: string | undefined + deployedHeadVersion?: string | undefined + /** Who deployed the head, named in the stale prompt. */ + deployedBy?: string | undefined /** Discard the draft and reload deployed (same as "Reset to deployed"). */ onLoadLatestDeploy?: () => void | Promise + /** Opens the editor's Deployed↔Current diff from the stale prompt, so the + * choice between keeping and discarding is informed. Omit where the editor + * has no diff drawer; the action is then not rendered. */ + onViewDiff?: () => void | Promise + /** Runs before this editor follows its draft to the item's new path: the + * editor's own draft save, which materializes text the code editor still + * holds. Without it, keystrokes typed since the relocating save are lost + * to the navigation. */ + onBeforeRelocate?: () => void | Promise /** Defaults to true; set to false to suppress all modals. */ enabled?: boolean } @@ -65,7 +81,10 @@ deployedAt = undefined, draftBaseVersion = undefined, deployedHeadVersion = undefined, + deployedBy = undefined, onLoadLatestDeploy, + onViewDiff, + onBeforeRelocate, enabled = true }: Props = $props() @@ -74,9 +93,9 @@ let staleAlertKey = $state(undefined) let staleModalOpen = $state(false) - // Prefer the exact version comparison (flows/apps) over the timestamp: the - // draft's pinned fork base never drifts, whereas `draftSavedAt` advances past - // `deployedAt` once you keep editing a stale draft, hiding the staleness. + // Prefer the version comparison over the timestamp for every kind that supplies + // one: `draftSavedAt` advances past `deployedAt` as you keep editing, hiding the + // staleness outright. const useVersion = $derived(draftBaseVersion != null && deployedHeadVersion != null) const isStale = $derived( !!onLoadLatestDeploy && @@ -105,6 +124,48 @@ } }) }) + + const EDITOR_SEGMENT: Partial> = { + script: 'scripts/edit', + flow: 'flows/edit', + app: 'apps/edit', + raw_app: 'apps_raw/edit' + } + + // The item was moved while this editor was open: the draft row followed it + // and the save just landed there. Follow it too — the route reloads the item + // at its new path, and the stale prompt above then says what changed. Edits + // typed since that save are flushed first, so leaving this path drops none. + $effect(() => { + if (!enabled || !workspace || !path) return + const seg = EDITOR_SEGMENT[itemKind] + if (!seg) return + const query = { workspace, itemKind, path } + // The flush below saves again and can land here a second time, and a second move + // can land while it runs: the last destination reported is the one to follow. + let relocating = false + let destination: string | undefined = undefined + return UserDraftDbSyncer.onRelocated(query, async (newPath) => { + destination = newPath + if (relocating) return + relocating = true + await onBeforeRelocate?.() + await UserDraftDbSyncer.flush(query) + // `flush` resolves on a failed or rejected save as well, and leaving the + // route drops what it was carrying: stay, so the editor keeps the edits + // and its own failure indicator. + if ( + UserDraftDbSyncer.getState(query).failureMessage || + UserDraftDbSyncer.getConflict(query).conflict + ) { + relocating = false + return + } + const target = destination ?? newPath + sendUserToast(`This item was moved to ${target}. You are now editing it there.`) + await goto(`${base}/${seg}/${target}`) + }) + }) {#if enabled && workspace && path} @@ -130,9 +191,14 @@ {#if onLoadLatestDeploy} {/if} /** - * Modal opened on editor mount when the authed user's per-user draft - * is older than the latest deployed version at the same path — i.e. - * someone else deployed a new version while the draft was sitting - * around. The user is asked to either pick up the latest deploy - * (discards the stale draft) or keep editing what they had. - * - * The parent threads `draftSavedAt` and `deployedAt` raw and the - * modal computes staleness internally; this keeps each route from - * re-implementing the comparison and the threshold (we treat a - * draft as stale only when it's strictly older — a deploy at the - * exact same instant is treated as not stale). + * The prompt for a draft that is behind: someone deployed a newer version + * of the item after the draft forked from it. Opened on every load while + * that holds (the parent computes it; see DraftEditorModals), it names the + * two versions and offers two ways out: look at the diff, or drop the draft + * for the latest deploy. Closing it is "keep editing". Taking the latest as + * the new base while keeping the edits lives in the diff drawer, where the + * user can see what they are taking. * * Open-state is bindable so the parent can dismiss programmatically * (e.g. after the load-latest-deploy callback completes). */ import Modal2 from '$lib/components/common/modal/Modal2.svelte' import Button from '$lib/components/common/button/Button.svelte' - import { AlertTriangle } from 'lucide-svelte' + import { AlertTriangle, GitCompare } from 'lucide-svelte' import { sendUserToast } from '$lib/toast' + import type { UserDraftItemKind } from '$lib/gen' type Props = { isOpen: boolean - /** ISO timestamp the authed user's draft was saved. */ + itemKind: UserDraftItemKind + /** ISO timestamp the authed user's draft was saved. Shown only when the + * versions below are unknown (a draft that predates the base). */ draftSavedAt: string | undefined /** ISO timestamp the latest deploy at this path landed. */ deployedAt: string | undefined + /** The version the draft forked from and the deployed head, as text. */ + draftBaseVersion?: string | undefined + deployedHeadVersion?: string | undefined + /** Who deployed the head. */ + deployedBy?: string | undefined /** Discards the draft and reloads the deployed payload — the route * already has this callback for the AutosaveIndicator's "Reset to * deployed" button; pass the same function in. */ onLoadLatestDeploy: () => void | Promise + /** Opens the editor's own Deployed↔Current diff. Without it the user is + * asked to choose between keeping and discarding their draft with no way + * to see what actually differs — and after a rename the difference is + * often only the path. Omitted where the editor has no diff drawer. */ + onViewDiff?: () => void | Promise } - let { isOpen = $bindable(), draftSavedAt, deployedAt, onLoadLatestDeploy }: Props = $props() + let { + isOpen = $bindable(), + itemKind, + draftSavedAt, + deployedAt, + draftBaseVersion = undefined, + deployedHeadVersion = undefined, + deployedBy = undefined, + onLoadLatestDeploy, + onViewDiff + }: Props = $props() let loading = $state(false) + // Scripts are versioned by hash, the other kinds by a numeric version id; + // the diff picker renders them the same way. + function formatVersion(v: string): string { + return itemKind === 'script' ? v.slice(0, 8) : v + } + async function loadLatestDeploy() { if (loading) return loading = true @@ -49,6 +74,13 @@ } } + // Dismisses on the way out: the diff drawer opens behind this modal, so + // leaving it up would cover the thing the user asked to see. + async function viewDiff() { + isOpen = false + await onViewDiff?.() + } + function formatTs(ts: string | undefined): string { if (!ts) return '' try { @@ -64,22 +96,38 @@
-

- A newer version was deployed after you started editing. Your draft is based on the older - deploy. -

-

- Draft saved {formatTs(draftSavedAt)} · Deployed {formatTs(deployedAt)} -

+

A newer version was deployed after you started editing.

+ {#if draftBaseVersion && deployedHeadVersion} +

+ Your draft is based on {formatVersion(draftBaseVersion)} + · latest is {formatVersion(deployedHeadVersion)} + {#if deployedBy}by {deployedBy}{/if}{#if deployedAt}, {formatTs(deployedAt)}{/if} +

+ {:else} +

+ Draft saved {formatTs(draftSavedAt)} · Deployed {formatTs(deployedAt)} +

+ {/if}
-
- - + {:else} +
+ {/if} + +
diff --git a/frontend/src/lib/components/common/drawer/DrawerContent.svelte b/frontend/src/lib/components/common/drawer/DrawerContent.svelte index ed2ed68519..9664f3a27e 100644 --- a/frontend/src/lib/components/common/drawer/DrawerContent.svelte +++ b/frontend/src/lib/components/common/drawer/DrawerContent.svelte @@ -30,6 +30,8 @@ * the content hug it with tight top padding; new entities keep normal padding. */ bannerReserved?: boolean + /** For content rendered in place of a drawer, which has nothing to close. */ + hideClose?: boolean children?: import('svelte').Snippet } @@ -50,6 +52,7 @@ titleExtra, banner, bannerReserved = false, + hideClose = false, children }: Props = $props() @@ -69,19 +72,26 @@ )} {id} > -
+
-
{ - dispatch('close') - } - }} - > - -
+ {#if !hideClose} +
{ + dispatch('close') + } + }} + > + +
+ {/if} {title ?? ''} {#if tooltip != '' || documentationLink} diff --git a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte index 8d6529764b..c1a29fae70 100644 --- a/frontend/src/lib/components/common/fileDownload/FileDownload.svelte +++ b/frontend/src/lib/components/common/fileDownload/FileDownload.svelte @@ -1,8 +1,10 @@
-
-
+ -
-
+ {#if expanded}
$workspaceStore) + const datatables = createDatatablesResource(() => $operatingWorkspace) const hasNoDatatables = $derived((datatables.current?.length ?? 0) === 0) diff --git a/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte b/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte index 364e7fceaf..adc3fe6184 100644 --- a/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte +++ b/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte @@ -34,8 +34,8 @@ title }: Props = $props() - // The drawers ride with the docked chat, so a surface can render this pill with nothing - // able to open one. + // The drawers ride with the docked chat and the session tabs with the sessions page, so a + // surface can render this pill with nothing able to open one. const available = $derived.by(() => { const action = workspaceItemAction(wmKind, wmPath, wmTargetKind, wmRawApp === 'true') return action && hasToolDisplayActionHandler(action.type) ? action : undefined @@ -113,8 +113,8 @@ variant="subtle" iconOnly startIcon={{ icon: PanelRight }} - title="Open in drawer" - aria-label="Open {wmPath} in drawer" + title="Open in editor" + aria-label="Open {wmPath} in editor" wrapperClasses="ml-0.5 inline-flex self-center shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" btnClasses="!w-auto !rounded !p-0.5 !text-tertiary" onClick={openDrawer} diff --git a/frontend/src/lib/components/copilot/chat/RunScriptCard.svelte b/frontend/src/lib/components/copilot/chat/RunScriptCard.svelte index edd1704bb2..4aff51d002 100644 --- a/frontend/src/lib/components/copilot/chat/RunScriptCard.svelte +++ b/frontend/src/lib/components/copilot/chat/RunScriptCard.svelte @@ -9,6 +9,7 @@ import DisplayResult from '$lib/components/DisplayResult.svelte' import { msToReadableTime } from '$lib/utils' import JobArgs from '$lib/components/JobArgs.svelte' + import { JobService, type CompletedJob, type Job } from '$lib/gen' import { base } from '$lib/base' import { getAiChatManager } from './aiChatManagerContext' import ChatCollapsibleCard from './ChatCollapsibleCard.svelte' @@ -16,51 +17,108 @@ import ToolContentDisplay from './ToolContentDisplay.svelte' import ToolPreviewCard from './ToolPreviewCard.svelte' import { scrollFades } from './scrollFades.svelte' - import { isActiveRunForm, MAX_LOG_LENGTH, type ToolDisplayMessage } from './shared' + import { + deriveChatJobStatus, + isActiveRunForm, + MAX_LOG_LENGTH, + type ToolDisplayMessage + } from './shared' const aiChatManager = getAiChatManager() + const LOGS_UNREADABLE = { logs: '', logsFailed: true } as const + interface Props { message: ToolDisplayMessage } let { message }: Props = $props() - const runForm = $derived(message.runForm!) - const runnableKind = $derived(runForm.runnableKind ?? 'script') + const runForm = $derived(message.runForm) + // The run this call inspected instead of starting. Exclusive with runForm: a call + // either ran something or looked at a run. + const inspected = $derived(message.inspectedRun) // The loop is parked on the form and nothing has run yet: the card is the form. const pending = $derived(isActiveRunForm(message)) + // An inspected run is read from the job itself rather than from the tool result, which + // is the copy capped for the model. Fetched on first expand, and only then: a transcript + // of collapsed inspections must not fire a request per row. + let fetched = $state< + { callId: string; job: Job; logs: string; logsFailed?: boolean } | undefined + >(undefined) + let fetchFailed = $state(undefined) + // Keyed by call id like the rest of this card's state, not by the job it read: a summarized + // transcript hands a surviving instance a different row, and two rows can inspect one job + // at different moments, so a job-keyed snapshot would serve the earlier row's reading. + const inspectedJob = $derived(fetched?.callId === message.tool_call_id ? fetched : undefined) + + const inspectedStatus = $derived(inspectedJob ? deriveChatJobStatus(inspectedJob.job) : undefined) + const chatJob = $derived( aiChatManager.backgroundJobs.find((j) => j.toolCallId === message.tool_call_id) ) + // The run the card is about, whichever way it got here. The inspected one is known by + // id before it is read, which is what lets the preview chip work on a collapsed card. + const job = $derived( + chatJob ?? + (inspected + ? { + jobId: inspected.jobId, + workspace: inspected.workspace, + status: inspectedStatus + } + : undefined) + ) + const runnableKind = $derived( + runForm?.runnableKind ?? + (inspectedJob?.job.job_kind === 'flow' || inspectedJob?.job.job_kind === 'flowpreview' + ? 'flow' + : 'script') + ) // Declining the form, stopping the turn and cancelling the job all land here, and none // of them is a failure: the run stopped because someone said so. const canceled = $derived( - Boolean(message.declinedByUser) || Boolean(runForm.canceled) || chatJob?.status === 'canceled' + Boolean(message.declinedByUser) || Boolean(runForm?.canceled) || job?.status === 'canceled' + ) + const failed = $derived( + inspected ? inspectedStatus === 'failure' : Boolean(message.error) && !canceled ) - const failed = $derived(Boolean(message.error) && !canceled) // A cancelled form never reached a job, so it has no logs and no outcome to offer. const ran = $derived( - Boolean(runForm.started) || Boolean(message.logs) || message.result !== undefined || !!chatJob + Boolean(runForm?.started) || Boolean(message.logs) || message.result !== undefined || !!job ) // A run can outlive the turn that started it, so "the tool call returned" is not // "the run finished": a detached job keeps the card in its running state until - // the background poller lands an outcome on it or the tray sees the job end. + // the background poller lands an outcome on it or the tray sees the job end. An + // inspected run has no poller behind it — the status it was read at is the answer. const settled = $derived( - !pending && - !message.isLoading && - (message.result !== undefined || - failed || - canceled || - (chatJob !== undefined && ['success', 'failure', 'canceled'].includes(chatJob.status))) + inspected + ? ['success', 'failure', 'canceled'].includes(inspectedStatus ?? '') + : !pending && + !message.isLoading && + (message.result !== undefined || + failed || + canceled || + (chatJob !== undefined && ['success', 'failure', 'canceled'].includes(chatJob.status))) ) const running = $derived(!pending && !settled) + // The job the card is about has not been read yet, or could not be: no pane has anything + // to show, but the call's own result still has. + const jobPending = $derived(Boolean(inspected) && !inspectedJob) + // An inspected run's panes come from the job: the tool's own parameters are the + // address it was called with, and its result is the model's abridged view. const parameters = $derived( - message.parameters && typeof message.parameters === 'object' ? message.parameters : {} + inspected + ? (inspectedJob?.job.args ?? {}) + : message.parameters && typeof message.parameters === 'object' + ? message.parameters + : {} + ) + const logs = $derived( + inspected ? (inspectedJob?.logs ?? '') : typeof message.logs === 'string' ? message.logs : '' ) - const logs = $derived(typeof message.logs === 'string' ? message.logs : '') const logLineCount = $derived(logs.trim() ? logs.trimEnd().split('\n').length : 0) // What the job has streamed of its result so far. Only ever set while it runs: the // terminal patch clears it, so a settled card reads its outcome off `result` alone. @@ -74,6 +132,12 @@ // pretty view buys. A string that happens to be JSON parses back as JSON, and the // text it was stored as is one toggle away in the raw view. const resultValue = $derived.by(() => { + // Only a completed job carries a result; a queued or running one has none, and reading + // it off that job is a type error rather than an undefined. + if (inspected) + return inspectedJob && 'success' in inspectedJob.job + ? (inspectedJob.job as CompletedJob).result + : undefined if (message.result === undefined) return undefined if (typeof message.result !== 'string') return message.result try { @@ -82,20 +146,32 @@ return message.result } }) - // The row is the card's whole heading, in the tense the call is in: a run cancelled // before it started never ran, so it is still the thing that was going to be run. A // test says so, since what it ran is the draft rather than what is deployed. const verbs = $derived( - runForm.kind === 'test' + runForm?.kind === 'test' ? { present: 'Testing', past: 'Tested', future: 'Test' } : { present: 'Running', past: 'Ran', future: 'Run' } ) + // Where the runnable is filed, for the preview chip's title. An inspected preview run + // has no path at all, so the card falls back to naming the job. + const path = $derived(runForm?.path ?? inspectedJob?.job.script_path ?? '') // What the script is called on its own page and in the picker, so the row names the thing // that ran rather than where it is filed. Not every script has one, so the path stays the // fallback — and stays on the preview chip either way, since two folders can hold one name. - const runnableName = $derived(runForm.summary || runForm.path) - const verb = $derived(running ? verbs.present : settled && ran ? verbs.past : verbs.future) + // An inspection names the run instead: it is about that run, the address is what the call + // was made with, and naming the runnable would rewrite the row once the job is read. + const runnableName = $derived( + inspected + ? `${inspected.step ? `step ${inspected.step} of ` : ''}run ${inspected.runId}` + : runForm?.summary || runForm?.path || '' + ) + // Inspecting is done the moment the tool returned, whatever the run it looked at is + // still doing — the tense belongs to the call, not to its subject. + const verb = $derived( + inspected ? 'Inspected' : running ? verbs.present : settled && ran ? verbs.past : verbs.future + ) // Being cancelled is an outcome like any other, and it is the one the card has to say out // loud: nothing came back, so no other tab can carry it. @@ -130,9 +206,63 @@ const activeTab = $derived(steered && tabs.some((t) => t.value === steered) ? steered : autoTab) // Keyed by call id: a bare flag would carry one card's collapse onto the next message - // reusing this instance. Open by default, since the run is what was asked for. + // reusing this instance. Open by default, since the run is what was asked for — + // except for an inspection, which is usually a step in the reasoning rather than + // the answer, and which pays a fetch for being opened. let toggled = $state<{ id: string; open: boolean } | undefined>(undefined) - const expanded = $derived(toggled?.id === message.tool_call_id ? toggled.open : true) + const expanded = $derived(toggled?.id === message.tool_call_id ? toggled.open : !inspected) + + $effect(() => { + const target = inspected + // Collapsing clears a failure so reopening tries again, which is how the rest of the + // chat treats a load that did not land — a dropped connection must not be permanent. + if (!target || !expanded) { + fetchFailed = undefined + // A job whose logs did not land is dropped with it: the job itself is cached, so + // reopening would otherwise keep serving the unreadable logs for the session. + if (fetched?.logsFailed) fetched = undefined + return + } + const callId = message.tool_call_id + if (fetched?.callId === callId || fetchFailed === callId) return + const jobReq = JobService.getJob({ + workspace: target.workspace, + id: target.jobId, + noCode: true, + noLogs: true + }) + // The dedicated endpoint, as get_run uses it, and the whole log does come down for a + // 4000-char tail. The cheap reads cannot replace it: the job's own `logs` field is + // `right(job_logs.logs, 20000)`, and compaction leaves as few as 3000 characters in + // that column, so a large log would show less here than the model was given. + const logsReq = JobService.getJobLogs({ + workspace: target.workspace, + id: target.jobId, + removeAnsiWarnings: true + }) + let live = true + Promise.all([ + // Something back is the success test, not "it did not throw": the generated client + // resolves nothing when it cannot read the body. + jobReq.then((j) => j ?? Promise.reject(new Error('job unreadable'))), + // Here an empty string is a real answer — a flow's own job prints nothing. + logsReq.then( + (l) => (typeof l === 'string' ? { logs: l.slice(-MAX_LOG_LENGTH) } : LOGS_UNREADABLE), + () => LOGS_UNREADABLE + ) + ]) + .then(([j, l]) => { + if (live) fetched = { callId, job: j, ...l } + }) + .catch(() => { + if (live) fetchFailed = callId + }) + return () => { + live = false + jobReq.cancel() + logsReq.cancel() + } + }) // The panel mounts the chat's own form on this call, so the card must not mount a second // one: two views binding the one draft would each reorder the schema SchemaForm edits in @@ -217,7 +347,7 @@ if (canceled) return 'text-tertiary' if (failed) return 'text-red-800 dark:text-red-300' if (!ran) return 'text-tertiary' - switch (chatJob?.status) { + switch (job?.status) { case 'running': return 'text-blue-800 dark:text-blue-200' case 'suspended': @@ -248,16 +378,16 @@ ? aiChatManager.openRunForm ? ('form' as const) : undefined - : chatJob + : job ? ('run' as const) : undefined ) const previewTitle = $derived( previewTarget === 'form' - ? `Open this form in the preview panel: ${runForm.path}` + ? `Open this form in the preview panel: ${path}` : aiChatManager.openRunInPreview - ? `Open this run in the preview panel: ${runForm.path}` - : `Open this run in a new tab: ${runForm.path}` + ? `Open this run in the preview panel: ${path || runnableName}` + : `Open this run in a new tab: ${path || runnableName}` ) function openPreview() { @@ -266,16 +396,12 @@ aiChatManager.openRunForm?.({ toolCallId: message.tool_call_id, label }) return } - if (!chatJob) return + if (!job) return // Outside a session there is no panel, so the run opens where the jobs tray sends it. if (aiChatManager.openRunInPreview) { - aiChatManager.openRunInPreview({ jobId: chatJob.jobId, workspace: chatJob.workspace, label }) + aiChatManager.openRunInPreview({ jobId: job.jobId, workspace: job.workspace, label }) } else { - window.open( - `${base}/run/${chatJob.jobId}?workspace=${chatJob.workspace}`, - '_blank', - 'noreferrer' - ) + window.open(`${base}/run/${job.jobId}?workspace=${job.workspace}`, '_blank', 'noreferrer') } } @@ -285,7 +411,9 @@ that number is still moving. `font-medium` because the row is a button and the base layer sets those semibold, which would leave this the one bold word in the header. --> {#snippet status()} - {#if !pending} + + {#if !pending && !inspected} {statusTime} @@ -297,7 +425,7 @@ tab it already opened. The row's only control, as on every other tool call. --> {#snippet previewChip()} These inputs are open in the preview panel.
- {:else if pending} + {:else if pending && runForm} {:else} -
+
+
+ {#if fetchFailed === message.tool_call_id} + This run could not be read. It may have been deleted, or be in another workspace. + Its result is on the JSON toggle. + {:else} + + + Loading this run... + + {/if} +
{:else} {:else if activeTab === 'logs'} @@ -433,15 +580,25 @@ >{logs} {:else} -

No logs yet.

+

+ {inspectedJob?.logsFailed + ? 'Logs could not be read.' + : running + ? 'No logs yet.' + : 'No logs.'} +

{/if} - {#if running} + {#if running && !inspected}
streaming
{/if} - {:else if failed} + + {:else if !inspected && failed}
{message.error}
@@ -465,8 +622,8 @@ which the row already owns. --> diff --git a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte index 42df0f1e84..5405a6ea25 100644 --- a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte @@ -129,7 +129,9 @@ // The run card owns this call from the form to whatever settled it, cancelling included: // the card is the call, and a run the user stopped is not a different kind of thing. - const isRunCard = $derived(Boolean(message.runForm)) + // A call that inspected a run rather than starting one gets the same card, bound to + // the job it named — what happened in a run reads the same either way. + const isRunCard = $derived(Boolean(message.runForm || message.inspectedRun)) // The preview chip sits on the header row (to the right of the tool-call text); // shown once the tool settled, never while loading/erroring/awaiting confirmation. diff --git a/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte b/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte index db2a0e22c4..84ee04d06f 100644 --- a/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolMessageActions.svelte @@ -125,8 +125,9 @@
{card.title}
{card.subtitle}
- + {#if hasToolDisplayActionHandler(action.type)} - - {#if targets('archive').length > 0 || targets('unarchive').length === 0} + + {items.length} selected + +
- {/if} - {#if targets('unarchive').length > 0} - - {/if} - open('discard'), - disabled: targets('discard').length === 0, - // Only while blocked: a disabled entry can't open its modal, so the reason - // has to live here — and an enabled one would render a pointless ⓘ. - tooltip: targets('discard').length === 0 ? actionTitle('discard') : undefined - }, - { - displayName: `Delete${countSuffix('delete')}`, - icon: Trash, - type: 'delete' as const, - action: () => open('delete'), - disabled: targets('delete').length === 0, - tooltip: targets('delete').length === 0 ? actionTitle('delete') : undefined - } - ]} - > - {#snippet buttonReplacement()} + + {#if targets('archive').length > 0 || targets('unarchive').length === 0} - {/snippet} - -
- + {/if} + open('discard'), + disabled: targets('discard').length === 0, + // Only while blocked: a disabled entry can't open its modal, so the reason + // has to live here — and an enabled one would render a pointless ⓘ. + tooltip: targets('discard').length === 0 ? actionTitle('discard') : undefined + }, + { + displayName: `Delete${countSuffix('delete')}`, + icon: Trash, + type: 'delete' as const, + action: () => open('delete'), + disabled: targets('delete').length === 0, + tooltip: targets('delete').length === 0 ? actionTitle('delete') : undefined + } + ]} + > + {#snippet buttonReplacement()} + + {/snippet} + +
+
`${i.path} → ${movedPath(i, target)}`) + pendingItems.map((i) => `${sourcePath(i)} → ${movedPath(i, target)}`) )} {/if} {:else if pending === 'discard'} diff --git a/frontend/src/lib/components/home/CreateActionsMenu.svelte b/frontend/src/lib/components/home/CreateActionsMenu.svelte index 2cf1f28d54..7c5bb4986e 100644 --- a/frontend/src/lib/components/home/CreateActionsMenu.svelte +++ b/frontend/src/lib/components/home/CreateActionsMenu.svelte @@ -247,7 +247,8 @@ } } - let activeKey = $state(allOptions[0]?.key) + // the doc panel only shows while an option is hovered or focused, so the menu opens compact + let activeKey: string | undefined = $state(undefined) // every option's import action, surfaced together under the bottom "Import" submenu. // The hub project leads and is separated below: the others each paste one artifact the // user already holds, while this one brings a whole project in from somewhere else. @@ -350,6 +351,7 @@ if (isOpen && !wasOpen) { logFeatureUsage('home', 'new_menu_open', { key: source }) } + if (!isOpen) activeKey = undefined wasOpen = isOpen }) @@ -360,8 +362,18 @@ // only persist the non-default (hidden) state, so a cleared key means "shown" storeLocalSetting(SHOW_DOC_SETTING, value ? undefined : 'false') } - let active = $derived(allOptions.find((o) => o.key === activeKey) ?? allOptions[0]) - let activeAc = $derived(accentClasses[active.accent]) + // The pointer crosses the gutter outside the menu on its way into the Workflow-as-Code + // submenu, so leaving the menu keeps the panel while that submenu is open; it clears + // once the submenu closes with neither pointer nor focus left on the menu. + let menuEl: HTMLDivElement | undefined = $state(undefined) + $effect(() => { + if ($wacSubOpen || !menuEl) return + if (!menuEl.matches(':hover') && !menuEl.contains(document.activeElement)) { + activeKey = undefined + } + }) + let active = $derived(allOptions.find((o) => o.key === activeKey)) + let activeAc = $derived(active ? accentClasses[active.accent] : undefined) // shared YAML/JSON import drawer, reused by every "Import …" extra let importDrawer: Drawer | undefined = $state(undefined) @@ -427,179 +439,195 @@ {/if}
-{#if $open && active} +{#if $open} +
{ + if (!$wacSubOpen) activeKey = undefined + }} > - {#if showDoc} - -
-
-
- -
-
-
-

{active.label}

- {#if active.badge} - - {active.badge.label} - - {/if} -
-

{active.tagline}

-
-
- -

{active.description}

- -
    - {#each active.bullets as bullet (bullet)} -
  • - - {bullet} -
  • - {/each} -
- - -
- {/if} - - -
- {#snippet rowBody(option: Option, ac: (typeof accentClasses)[string])} -
- -
- - {option.label} - - {#if option.badge} - - {option.badge.label} - - {/if} - {/snippet} - {#each allOptions as option (option.key)} - {@const ac = accentClasses[option.accent]} - {@const rowClass = - 'w-full flex flex-row items-center gap-2.5 rounded-md px-2 py-1.5 text-left cursor-pointer transition-colors focus:outline-none data-[highlighted]:bg-surface-hover hover:bg-surface-hover'} - {#if option.variants} - - {#if $wacSubOpen} +
+ {#if showDoc && active && activeAc} + +
+
- {#each option.variants ?? [] as variant (variant.label)} - {@const VariantIcon = variant.icon} - - {/each} +
- {/if} - {:else} - - {/if} - {/each} +
+
+

{active.label}

+ {#if active.badge} + + {active.badge.label} + + {/if} +
+

{active.tagline}

+
+
- -
-
- - Import - - - - {#if $importSubOpen} -
- {#each importActions as action, i (action.label)} + {/if} + + +
+ {#snippet rowBody(option: Option, ac: (typeof accentClasses)[string])} +
+ +
+ + {option.label} + + {#if option.badge} + + {option.badge.label} + + {/if} + {/snippet} + {#each allOptions as option (option.key)} + {@const ac = accentClasses[option.accent]} + {@const rowClass = + 'w-full flex flex-row items-center gap-2.5 rounded-md px-2 py-1.5 text-left cursor-pointer transition-colors focus:outline-none data-[highlighted]:bg-surface-hover hover:bg-surface-hover'} + {#if option.variants} + + {#if $wacSubOpen} +
+ {#each option.variants ?? [] as variant (variant.label)} + {@const VariantIcon = variant.icon} + + {/each} +
+ {/if} + {:else} - {#if onImportHubProject && i === 0} -
- {/if} - {/each} -
- {/if} + {/if} + {/each} - {#if !showDoc} + +
- {/if} + {#if $importSubOpen} +
+ {#each importActions as action, i (action.label)} + + {#if onImportHubProject && i === 0} +
+ {/if} + {/each} +
+ {/if} + + {#if !showDoc} + + {/if} +
{/if} diff --git a/frontend/src/lib/components/home/HomeConnectDrawer.svelte b/frontend/src/lib/components/home/HomeConnectDrawer.svelte index e7e4a306d9..e08ac2ddea 100644 --- a/frontend/src/lib/components/home/HomeConnectDrawer.svelte +++ b/frontend/src/lib/components/home/HomeConnectDrawer.svelte @@ -5,10 +5,12 @@ import CopyableCodeBlock from '$lib/components/details/CopyableCodeBlock.svelte' import { Bot, ExternalLink, Terminal } from 'lucide-svelte' import { shell } from 'svelte-highlight/languages' + import { mcpTokenUrlDisabled } from '$lib/mcpAuth' type ConnectTab = 'cli' | 'mcp' let drawer: Drawer | undefined = $state() + let tokenUrlDisabled = $state(false) let selectedTab: ConnectTab = $state('cli') let openVersion = $state(0) @@ -24,6 +26,10 @@ wmill sync pull`) export function openDrawer(tab: ConnectTab = 'cli') { selectedTab = tab openVersion += 1 + // Falls back like CreateToken below, which shows the bare URL when the read fails. + void mcpTokenUrlDisabled() + .then((v) => (tokenUrlDisabled = v)) + .catch(() => (tokenUrlDisabled = true)) drawer?.openDrawer() } @@ -96,8 +102,13 @@ wmill sync pull`)

MCP URL

- Generate an MCP server URL for the current workspace and choose which - scripts, flows, and endpoints the client can access. + {#if tokenUrlDisabled} + The MCP server URL for the current workspace. Your client signs in to + Windmill to use it. + {:else} + Generate an MCP server URL for the current workspace and choose which + scripts, flows, and endpoints the client can access. + {/if}

diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index 7038a316fa..f86ec9d5bf 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -20,7 +20,6 @@ import type uFuzzy from '@leeoniya/ufuzzy' import { ArrowDownUp, - CheckSquare, ChevronsDownUp, ChevronsUpDown, Code2, @@ -1805,20 +1804,10 @@ {/if} {#if !loading && !contentActive && !workspaceEmpty} - +
- {#if homeSelection.available && !homeSelection.active} - {:else} - download + download {/if} +
+ {:else} + {#key `${item.kind}:${triggerKey}:${item.path}:${workspaceId}:${reloadNonce}:${savedNonce}`} + {#if item.kind === 'variable'} + + {:else if item.kind === 'resource'} + { + if (path !== undefined) onSaved(path) + }} + onRestored={() => savedNonce++} + /> + {:else if triggerKey} + {#await TRIGGER_EDITORS[triggerKey]()} + {@render loading()} + {:then Module} + onSaved(path)} + /> + {/await} + {/if} + {/key} + {/if} +
diff --git a/frontend/src/lib/components/sessions/PreviewTabHost.svelte b/frontend/src/lib/components/sessions/PreviewTabHost.svelte index 3dee7dd1b3..b88907bf02 100644 --- a/frontend/src/lib/components/sessions/PreviewTabHost.svelte +++ b/frontend/src/lib/components/sessions/PreviewTabHost.svelte @@ -103,12 +103,19 @@ applyPageIframeTheme(darkMode) }) + // A page item's editor reads its draft only when it loads, so a reload remounts it. + let pageItemReloadNonce = $state(0) + export function reload() { // A live editor shares the runtime store the chat mutates, so generic chat // edits are already reflected — no reload needed. Deploys refresh it via // each editor view's onDeploy → runtime.syncPreviewWithDeployed. So only the // iframe fallback (a separate page) has to be told to refresh. if (slot.kind === 'editor') return + if (slot.kind === 'pageitem') { + pageItemReloadNonce++ + return + } try { const win = frame?.contentWindow if (!win) return @@ -311,6 +318,22 @@ {/await} {/if}
+{:else if slot.kind === 'pageitem' && mounted && runtime} +
+ + {#if overlayHostEl} + {#await import('./PageItemEditorView.svelte')} + {@render editorLoading()} + {:then Module} + + {/await} + {/if} +
{:else if slot.kind === 'artifact' && mounted}
+ workspaceId) // This tab's storage path, for the editor below: several tabs are mounted at // once and only this one knows which item each is open on. diff --git a/frontend/src/lib/components/sessions/appDraftCodec.test.ts b/frontend/src/lib/components/sessions/appDraftCodec.test.ts index ba0d6f5f2d..8a6ac74f86 100644 --- a/frontend/src/lib/components/sessions/appDraftCodec.test.ts +++ b/frontend/src/lib/components/sessions/appDraftCodec.test.ts @@ -78,3 +78,25 @@ describe('appDraftCodec — draft_path round-trip', () => { expect(back.draft_path).toBe('u/admin/renamed') }) }) + +describe('appDraftCodec — fork base round-trip', () => { + it('carries the draft version both ways', () => { + const draft = runtimeRawAppToDraft(runtime({ parent_version: 3 })) + expect(draft.parent_version).toBe(3) + expect(applyDraftToRuntimeRawApp(runtime({ parent_version: 1 }), draft).parent_version).toBe(3) + }) + + it('keeps an unknown base unknown rather than adopting the runtime version', () => { + const dv: RawAppDraft = { + summary: 'app', + files: {}, + runnables: {}, + data: { tables: [] } as any + } + // This result round-trips into the next save: a fallback here would claim the + // content forked from the version the session happens to hold. + expect(applyDraftToRuntimeRawApp(runtime({ parent_version: 2 }), dv).parent_version).toBe( + undefined + ) + }) +}) diff --git a/frontend/src/lib/components/sessions/appDraftCodec.ts b/frontend/src/lib/components/sessions/appDraftCodec.ts index 10752b8d8f..7d9f4b048f 100644 --- a/frontend/src/lib/components/sessions/appDraftCodec.ts +++ b/frontend/src/lib/components/sessions/appDraftCodec.ts @@ -16,6 +16,9 @@ export type RawAppDraft = { // friendly name (they read `value->>'draft_path'`) — and so editing the path // in the editor changes the persisted draft and triggers an autosave. draft_path?: string + // The app_version the draft forked from. The server derives `draft.base` from + // it, which is what tells a draft that is behind the deployed head. + parent_version?: number } // The shape a raw-app cell's store (`RawAppRuntimeValue` in @@ -30,6 +33,7 @@ export type RuntimeRawApp = { policy: any custom_path?: string draft_path?: string + parent_version?: number } // Strip runtime-only metadata (just `path`, the storage key) when persisting @@ -43,7 +47,8 @@ export function runtimeRawAppToDraft(raw: RuntimeRawApp): RawAppDraft { data: raw.data, policy: raw.policy, custom_path: raw.custom_path, - draft_path: raw.draft_path + draft_path: raw.draft_path, + parent_version: raw.parent_version } } @@ -58,6 +63,10 @@ export function applyDraftToRuntimeRawApp(raw: RuntimeRawApp, dv: RawAppDraft): data: dv.data, policy: dv.policy ?? raw.policy, custom_path: dv.custom_path ?? raw.custom_path, - draft_path: dv.draft_path ?? raw.draft_path + draft_path: dv.draft_path ?? raw.draft_path, + // The incoming draft's own fork base, absence included: this round-trips into the + // next save, so falling back to the runtime's version would give that content a + // base it never forked from and hide that it is behind. + parent_version: dv.parent_version } } diff --git a/frontend/src/lib/components/sessions/pageDrawerSession.ts b/frontend/src/lib/components/sessions/pageDrawerSession.ts index ca7379be20..16d8d9204a 100644 --- a/frontend/src/lib/components/sessions/pageDrawerSession.ts +++ b/frontend/src/lib/components/sessions/pageDrawerSession.ts @@ -11,6 +11,7 @@ import type { UserDraftItemKind } from '$lib/gen' // flow editors, where pulling the filter schemas that module reads views from would make // every trigger's save utils eager. import { + drawerHashFor, pageHref, stripBase, TRIGGER_PAGES, @@ -20,6 +21,7 @@ import { type TriggerKind } from './previewPaths' import type { OpenInSessionSource } from './OpenInSessionButton.svelte' +import { isSessionPreviewFrame } from './sessionMode.svelte' // The draft each page's drawer edits. The preview loads the page in its own // document and reads the draft back from the server, so opening a session has to @@ -63,11 +65,6 @@ async function flushOrRefuse(query: Parameters[0 } } -// How each page addresses a row in its hash. Resources route theirs through an extra -// segment; every other page names the path directly. -const drawerHashFor = (pagePath: string, itemPath: string) => - pagePath === RESOURCES_PATH ? `/resource/${itemPath}` : itemPath - /** * Deep-link the row whose drawer just opened, so the location says what is on screen — a * drawer opened from a row's Edit button is as open as one reached by link, and the chat @@ -87,6 +84,34 @@ export function setPageDrawerAnchor(pagePath: string, itemPath: string | undefin history.replaceState(history.state, '', `${pathname}${search}${anchor}`) } +/** + * Inside a session preview frame, hand a list page row up to the session, which edits it in + * a tab of its own. True when handed off: the caller must then not open its drawer. False + * off that page, and outside a preview frame, where the drawer is how the row is edited. + */ +export function handOffPageDrawer(pagePath: string, itemPath: string | undefined): boolean { + if (!itemPath || !isSessionPreviewFrame()) return false + if (stripBase(window.location.pathname) !== pagePath) return false + try { + window.parent.postMessage( + { type: 'wm.session.openPageItem', pagePath, path: itemPath }, + window.location.origin + ) + } catch { + return false + } + // A frame left on a row's hash claims a row nobody has open here, and reopens its tab on + // every reload. Not through the router: these pages open their drawer from the hash. Once + // more after the event: a row link's `href="#"` lands after its click handler. + const dropAnchor = () => { + const { pathname, search, hash } = window.location + if (hash) history.replaceState(history.state, '', `${pathname}${search}`) + } + dropAnchor() + setTimeout(dropAnchor, 0) + return true +} + /** * Drop the row a list page deep-links, once its drawer closes. The hash is how the row was * requested; leaving it behind makes the location claim a drawer that is no longer open — diff --git a/frontend/src/lib/components/sessions/pageItemLookup.ts b/frontend/src/lib/components/sessions/pageItemLookup.ts new file mode 100644 index 0000000000..bee3fc41c1 --- /dev/null +++ b/frontend/src/lib/components/sessions/pageItemLookup.ts @@ -0,0 +1,62 @@ +import { + AmqpTriggerService, + ApiError, + AzureTriggerService, + EmailTriggerService, + GcpTriggerService, + HttpTriggerService, + KafkaTriggerService, + MqttTriggerService, + NatsTriggerService, + PostgresTriggerService, + ResourceService, + ScheduleService, + SqsTriggerService, + VariableService, + WebsocketTriggerService +} from '$lib/gen' +import type { PageItemRef, TriggerKind } from './previewPaths' + +type Get = (args: { workspace: string; path: string; getDraft: boolean }) => Promise + +const TRIGGER_GETS: Record = { + http: (a) => HttpTriggerService.getHttpTrigger(a), + websocket: (a) => WebsocketTriggerService.getWebsocketTrigger(a), + postgres: (a) => PostgresTriggerService.getPostgresTrigger(a), + kafka: (a) => KafkaTriggerService.getKafkaTrigger(a), + nats: (a) => NatsTriggerService.getNatsTrigger(a), + mqtt: (a) => MqttTriggerService.getMqttTrigger(a), + amqp: (a) => AmqpTriggerService.getAmqpTrigger(a), + sqs: (a) => SqsTriggerService.getSqsTrigger(a), + gcp: (a) => GcpTriggerService.getGcpTrigger(a), + azure: (a) => AzureTriggerService.getAzureTrigger(a), + email: (a) => EmailTriggerService.getEmailTrigger(a) +} + +function getFor(ref: PageItemRef): Get { + switch (ref.kind) { + case 'variable': + return (a) => VariableService.getVariable(a) + case 'resource': + return (a) => ResourceService.getResource(a) + case 'schedule': + return (a) => ScheduleService.getSchedule(a) + case 'trigger': + return TRIGGER_GETS[ref.triggerKind] + } +} + +/** A page item as its editor would load it — deployed, or only a draft — or undefined when it + * is neither: deleted, or a draft that was discarded. Any other failure is thrown, for the + * caller to leave to the editor rather than report as a missing item. */ +export async function lookupPageItem( + ref: PageItemRef, + workspace: string +): Promise | undefined> { + try { + return (await getFor(ref)({ workspace, path: ref.path, getDraft: true })) as Record + } catch (e) { + if (e instanceof ApiError && e.status === 404) return undefined + throw e + } +} diff --git a/frontend/src/lib/components/sessions/previewPaths.ts b/frontend/src/lib/components/sessions/previewPaths.ts index 8d10dc1a32..13d5abca33 100644 --- a/frontend/src/lib/components/sessions/previewPaths.ts +++ b/frontend/src/lib/components/sessions/previewPaths.ts @@ -48,6 +48,87 @@ export const TRIGGER_PAGES: Record + pagePath === RESOURCES_PATH ? `/resource/${itemPath}` : itemPath + +/** The full page a page item is edited on: its list page, with the row's drawer open. */ +export function pageItemPageHref(ref: PageItemRef): string { + const listPath = pageItemListPath(ref) + return `${pageHref(listPath)}#${drawerHashFor(listPath, ref.path)}` +} + +/** The list page a page item is edited from. */ +export function pageItemListPath(ref: PageItemRef): string { + switch (ref.kind) { + case 'variable': + return VARIABLES_PATH + case 'resource': + return RESOURCES_PATH + case 'schedule': + return SCHEDULES_PATH + case 'trigger': + return TRIGGER_PAGES[ref.triggerKind].path + } +} + +/** The page item a list page's row names, or undefined for a page that lists none. */ +export function pageItemForListPath(pagePath: string, path: string): PageItemRef | undefined { + const clean = stripBase(pagePath) + if (clean === VARIABLES_PATH) return { kind: 'variable', path } + if (clean === RESOURCES_PATH) return { kind: 'resource', path } + if (clean === SCHEDULES_PATH) return { kind: 'schedule', path } + const trigger = Object.entries(TRIGGER_PAGES).find(([, p]) => p.path === clean) + return trigger ? { kind: 'trigger', triggerKind: trigger[0] as TriggerKind, path } : undefined +} + +const PAGE_ITEM_ROUTE = /^pageitem:(variable|resource|schedule|trigger\.([a-z]+))\/([^?#]+)$/ + +// A scheme rather than a path, like artifacts: the tab mounts the item's editor in process, +// so there is no page a frame could load. The path is encoded whole, so its slashes cannot +// be read as part of the scheme. +export function pageItemUrl(ref: PageItemRef): string { + const kind = ref.kind === 'trigger' ? `trigger.${ref.triggerKind}` : ref.kind + return `pageitem:${kind}/${encodeURIComponent(ref.path)}` +} + +export function parsePageItemRoute(url: string): PageItemRef | null { + const m = url.match(PAGE_ITEM_ROUTE) + if (!m) return null + let path: string + try { + path = decodeURIComponent(m[3]) + } catch { + return null + } + if (m[2] !== undefined) { + if (!(m[2] in TRIGGER_PAGES)) return null + return { kind: 'trigger', triggerKind: m[2] as TriggerKind, path } + } + return { kind: m[1] as 'variable' | 'resource' | 'schedule', path } +} + +/** Singular human name of a page item's kind, e.g. "Kafka trigger". */ +export function pageItemKindLabel(ref: PageItemRef): string { + switch (ref.kind) { + case 'variable': + return 'Variable' + case 'resource': + return 'Resource' + case 'schedule': + return 'Schedule' + case 'trigger': + return TRIGGER_PAGES[ref.triggerKind].label.replace(/s$/, '') + } +} + /** Label a trigger list page from its (base-stripped) pathname, or undefined. */ export function triggerLabelForPath(path: string): string | undefined { const clean = stripBase(path) diff --git a/frontend/src/lib/components/sessions/previewReload.test.ts b/frontend/src/lib/components/sessions/previewReload.test.ts index de11805b9a..ce9491d334 100644 --- a/frontend/src/lib/components/sessions/previewReload.test.ts +++ b/frontend/src/lib/components/sessions/previewReload.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest' import { toolReloadEffect, tabsToReload } from './previewReload' import type { SessionPreviewTab } from './sessionState.svelte' +import { pageItemUrl } from './previewPaths' describe('toolReloadEffect', () => { it('maps a non-item mutation to its own list page only', () => { @@ -52,6 +53,26 @@ describe('toolReloadEffect', () => { }) }) +describe('page item tabs', () => { + const tab = (url: string): SessionPreviewTab => ({ id: url, url, loc: url }) + const kafkaA = tab('pageitem:trigger.kafka/u%2Fme%2Fa') + const kafkaB = tab('pageitem:trigger.kafka/u%2Fme%2Fb') + const list = tab('/kafka_triggers') + + it('reloads only the trigger a write names, and its list page', () => { + const { pages, items } = toolReloadEffect('write_trigger', { + kind: 'kafka', + config: { path: 'u/me/a' } + }) + const named = new Set(items.map((i) => pageItemUrl(i))) + expect(tabsToReload([kafkaA, kafkaB, list], new Set(pages), named)).toEqual([kafkaA, list]) + }) + + it('reloads every tab of the kind when the tool names no item', () => { + expect(tabsToReload([kafkaA, kafkaB], new Set(['/kafka_triggers']))).toEqual([kafkaA, kafkaB]) + }) +}) + describe('tabsToReload', () => { const scheduleTab: SessionPreviewTab = { id: 's', url: '/schedules', loc: '/schedules' } const resourceTab: SessionPreviewTab = { id: 'r', url: '/resources', loc: '/resources' } diff --git a/frontend/src/lib/components/sessions/previewReload.ts b/frontend/src/lib/components/sessions/previewReload.ts index 6e9d750a1f..bdfbeaa3ff 100644 --- a/frontend/src/lib/components/sessions/previewReload.ts +++ b/frontend/src/lib/components/sessions/previewReload.ts @@ -1,6 +1,14 @@ import type { SessionPreviewTab } from './sessionState.svelte' import { whereIs } from './sessionPreviewTabs.svelte' -import { stripBase, TRIGGER_PAGES, type TriggerKind } from './previewPaths' +import { + pageItemListPath, + pageItemUrl, + parsePageItemRoute, + stripBase, + TRIGGER_PAGES, + type PageItemRef, + type TriggerKind +} from './previewPaths' // Which list pages a completed chat tool can change, as base-stripped paths // (e.g. `/schedules`). This allowlist is the single source of truth for "does @@ -13,23 +21,27 @@ import { stripBase, TRIGGER_PAGES, type TriggerKind } from './previewPaths' // deliberately absent: every editable item is a live in-process editor that // self-syncs from the store the chat mutates, so its tab needs no reload — and // no list page we preview lists open drafts. They fall through to NO_RELOAD. -// This "live editors self-sync, only list pages reload" invariant is the reason -// the callers below and in the sessions page reload nothing for item tabs. -export type ToolReloadEffect = { pages: string[] } -const NO_RELOAD: ToolReloadEffect = { pages: [] } +// +// Page items (variables, resources, schedules, triggers) are the exception among +// in-process tabs: their editors read a draft only when they open, so a write to +// one reloads its tab too. `items` names it when the tool's args do; without a +// path, every tab of that kind reloads. +export type ToolReloadEffect = { pages: string[]; items: PageItemRef[] } +const NO_RELOAD: ToolReloadEffect = { pages: [], items: [] } export function toolReloadEffect(name: string, args: any): ToolReloadEffect { switch (name) { case 'write_schedule': - return { pages: ['/schedules'] } + return withItem(['/schedules'], itemRef('schedule', args)) case 'write_trigger': - return { pages: triggerPages(args?.kind) } + // Its path sits in the trigger's own config, not beside `kind`. + return withItem(triggerPages(args?.kind), itemRef('trigger', args?.config, args?.kind)) case 'write_resource': - return { pages: ['/resources'] } + return withItem(['/resources'], itemRef('resource', args)) case 'write_variable': - return { pages: ['/variables'] } + return withItem(['/variables'], itemRef('variable', args)) case 'create_folder': - return { pages: ['/folders'] } + return { pages: ['/folders'], items: [] } // Generic item tools carry a workspace-item `type`; refresh its list page // when it lives on one (schedule/resource/variable/trigger). script/flow/app // have their own live editor tab and no previewed list page → nothing. @@ -37,12 +49,31 @@ export function toolReloadEffect(name: string, args: any): ToolReloadEffect { case 'discard_local_draft': case 'deploy_workspace_item': case 'rebase_draft': - return { pages: pagesForItemType(args?.type, args) } + return withItem( + pagesForItemType(args?.type, args), + itemRef(args?.type, args, args?.trigger_kind) + ) default: return NO_RELOAD } } +function withItem(pages: string[], item: PageItemRef | undefined): ToolReloadEffect { + return { pages, items: item && pages.length ? [item] : [] } +} + +function itemRef(type: unknown, args: any, triggerKind?: unknown): PageItemRef | undefined { + const path = args?.path + if (typeof path !== 'string' || !path) return undefined + if (type === 'variable' || type === 'resource' || type === 'schedule') { + return { kind: type, path } + } + if (type === 'trigger' && (triggerKind as string) in TRIGGER_PAGES) { + return { kind: 'trigger', triggerKind: triggerKind as TriggerKind, path } + } + return undefined +} + function pagesForItemType(type: unknown, args: any): string[] { switch (type) { case 'schedule': @@ -63,14 +94,23 @@ function triggerPages(kind: unknown): string[] { return page ? [page.path] : [] } -// The open tabs a page-reload should refresh: those whose observed page path is -// in `pages`. Item-editor and pipeline tab routes are never list pages, so they -// never match (see the self-sync invariant above). Pure over a tab snapshot so -// the sessions page can reload by id and this stays unit-testable. +// The open tabs a reload should refresh: list-page tabs whose observed page path is +// in `pages`, and page item tabs on those pages — only the named ones when a tool +// named its item. Item-editor and pipeline tab routes are never list pages, so they +// never match (see the self-sync invariant above). Pure over a tab snapshot so the +// sessions page can reload by id and this stays unit-testable. export function tabsToReload( tabs: SessionPreviewTab[], - pages: ReadonlySet + pages: ReadonlySet, + items: ReadonlySet = new Set() ): SessionPreviewTab[] { if (pages.size === 0) return [] - return tabs.filter((t) => pages.has(stripBase(whereIs(t)))) + return tabs.filter((t) => { + const pageItem = parsePageItemRoute(t.url) + if (!pageItem) return pages.has(stripBase(whereIs(t))) + const listPath = pageItemListPath(pageItem) + if (!pages.has(listPath)) return false + const named = [...items].some((u) => pageItemListPath(parsePageItemRoute(u)!) === listPath) + return !named || items.has(pageItemUrl(pageItem)) + }) } diff --git a/frontend/src/lib/components/sessions/previewRouter.test.ts b/frontend/src/lib/components/sessions/previewRouter.test.ts index 7d56f9e717..3bc76882e5 100644 --- a/frontend/src/lib/components/sessions/previewRouter.test.ts +++ b/frontend/src/lib/components/sessions/previewRouter.test.ts @@ -14,8 +14,27 @@ import { previewLocationContext, previewLocationLabel, resolvePreviewTab, - runFormUrl + runFormUrl, + workspacePageHref } from './previewRouter' +import { pageItemUrl } from './previewPaths' + +describe('workspacePageHref', () => { + it('sends a page item tab to its list page with the row open, never to its scheme', () => { + expect(workspacePageHref(pageItemUrl({ kind: 'resource', path: 'u/me/db' }))).toBe( + '/resources#/resource/u/me/db' + ) + expect( + workspacePageHref(pageItemUrl({ kind: 'trigger', triggerKind: 'kafka', path: 'f/a/b' })) + ).toBe('/kafka_triggers#f/a/b') + }) + + it('has no page for a tab that belongs to the chat', () => { + expect(workspacePageHref(artifactUrl('a1', 'Plan'))).toBeUndefined() + expect(workspacePageHref(runFormUrl('call_1', 'Run'))).toBeUndefined() + expect(workspacePageHref('/runs?path=u/me/x')).toBe('/runs?path=u/me/x') + }) +}) describe('drawerAnchorFor', () => { it('reads the anchored row on the pages that deep-link one', () => { diff --git a/frontend/src/lib/components/sessions/previewRouter.ts b/frontend/src/lib/components/sessions/previewRouter.ts index 9f312ea389..13e7626e70 100644 --- a/frontend/src/lib/components/sessions/previewRouter.ts +++ b/frontend/src/lib/components/sessions/previewRouter.ts @@ -3,8 +3,13 @@ import { AUDIT_LOGS_PATH, FOLDERS_PATH, GROUPS_PATH, + pageItemForListPath, + pageItemListPath, + pageItemPageHref, + pageItemUrl, pageKey, pageHref, + parsePageItemRoute, parsePreviewItemRoute, RESOURCES_PATH, RUNS_PATH, @@ -14,17 +19,22 @@ import { WORKSPACE_SETTINGS_PATH, triggerLabelForPath, TRIGGER_PAGES, + type PageItemRef, type PreviewItemRoute, type TriggerKind } from './previewPaths' // Re-exported so the preview code that already reads locations through this module keeps // one import, while a caller needing only a path can reach for the leaf instead. export { + pageItemListPath, + pageItemUrl, pageKey, pageHref, + parsePageItemRoute, parsePreviewItemRoute, stripBase, TRIGGER_PAGES, + type PageItemRef, type PreviewItemRoute, type TriggerKind } @@ -68,6 +78,7 @@ export type PreviewTarget = | { type: 'item'; item: WorkspaceItem } | { type: 'artifact'; id: string; name: string; version?: ArtifactVersionTarget } | { type: 'runform'; toolCallId: string; label: string } + | { type: 'pageitem'; ref: PageItemRef } export type PreviewPage = { label: string; path: string; icon: DrillIcon } @@ -117,6 +128,26 @@ export function drawerAnchorFor(location: string): string | undefined { return location.slice(hashAt + 1).replace(/^\/resource\//, '') || undefined } +/** The item a list-page location deep-links, as a tab of its own: a session edits these + * in process, so the list page's drawer is never where one belongs. */ +export function pageItemForLocation(location: string): PageItemRef | undefined { + const anchor = drawerAnchorFor(location) + if (!anchor) return undefined + let path: string + try { + path = decodeURIComponent(anchor) + } catch { + return undefined + } + return pageItemForListPath(location, path) +} + +/** A location with a deep-linked row replaced by that row's own tab; any other unchanged. */ +export function pageItemLocation(location: string): string { + const ref = pageItemForLocation(location) + return ref ? pageItemUrl(ref) : location +} + // Query params the preview host injects into an iframe URL (`nomenubar` hides the nav, // `workspace` scopes the page). Never part of what a location means. const INJECTED_PARAMS = ['nomenubar', 'workspace'] as const @@ -126,7 +157,7 @@ const INJECTED_PARAMS = ['nomenubar', 'workspace'] as const export function canonicalizeObservedLoc(loc: string): string { // An artifact or a run form is a scheme, not a path — `new URL` would happily parse it // and hand back a pathname with the scheme gone. - if (parseArtifactRoute(loc) || parseRunFormRoute(loc)) return loc + if (parseArtifactRoute(loc) || parseRunFormRoute(loc) || parsePageItemRoute(loc)) return loc try { const u = new URL(loc, 'http://_') for (const p of INJECTED_PARAMS) u.searchParams.delete(p) @@ -201,6 +232,8 @@ export function describeLocation(loc: string): PreviewLocation { // Identity is the call, never the label: that carries the script's summary, so folding it // in would open a second tab for the same form whenever the summary differed. if (runForm) return { identity: `runform:${runForm.toolCallId}`, view: '', anchor: '' } + const pageItem = parsePageItemRoute(loc) + if (pageItem) return { identity: pageItemUrl(pageItem), view: '', anchor: '' } const canonical = canonicalizeObservedLoc(loc) const path = stripBase(canonical) const bare = canonical.split('#')[0] @@ -322,6 +355,15 @@ export function previewLocationContext(loc: string): { location: string open?: string } { + // Told as its list page with the item open, the shape the model already reads for a row + // whose drawer is open — which is all a page item tab is to it. + const pageItem = parsePageItemRoute(loc) + if (pageItem) { + return { + ...previewLocationContext(pageItemListPath(pageItem)), + open: promptSafe(pageItem.path) + } + } const { identity, anchor } = describeLocation(loc) const bare = canonicalizeObservedLoc(loc).split('#')[0] const query = bare.includes('?') ? bare.slice(bare.indexOf('?') + 1) : '' @@ -370,6 +412,8 @@ export function previewLocationLabel(url: string): string { if (artifact) return artifact.name || 'Artifact' const runForm = parseRunFormRoute(url) if (runForm) return runForm.label || 'Run form' + const pageItem = parsePageItemRoute(url) + if (pageItem) return pageItem.path.split('/').pop() || pageItem.path const page = matchReusablePage(url) if (page) return page.label const trigger = triggerLabelForPath(url) @@ -492,6 +536,7 @@ export type PreviewSlot = | { kind: 'editor'; editorKind: SessionTargetKind | 'pipeline'; path: string } | { kind: 'artifact'; id: string; version?: number } | { kind: 'runform'; toolCallId: string } + | { kind: 'pageitem'; ref: PageItemRef } | { kind: 'iframe' } export function resolvePreviewTab(url: string): PreviewSlot { @@ -499,6 +544,8 @@ export function resolvePreviewTab(url: string): PreviewSlot { if (artifact) return { kind: 'artifact', id: artifact.id, version: artifact.version } const runForm = parseRunFormRoute(url) if (runForm) return { kind: 'runform', toolCallId: runForm.toolCallId } + const pageItem = parsePageItemRoute(url) + if (pageItem) return { kind: 'pageitem', ref: pageItem } const pipelineFolder = parsePipelineRoute(url) if (pipelineFolder) { return { kind: 'editor', editorKind: 'pipeline', path: pipelineFolder } @@ -516,3 +563,24 @@ export function resolvePreviewTab(url: string): PreviewSlot { if (!editorKind) return { kind: 'iframe' } return { kind: 'editor', editorKind, path: route.itemPath } } + +/** The full workspace page showing what a tab shows ("Open in workspace"), or undefined when + * there is none. Every tab kind answers here, so a new one cannot fall through to its url being + * navigated as a path — an artifact or page item url is a scheme, not a route. */ +export function workspacePageHref(location: string): string | undefined { + const slot = resolvePreviewTab(location) + switch (slot.kind) { + case 'artifact': + case 'runform': + return undefined + case 'pageitem': + return pageItemPageHref(slot.ref) + case 'editor': + case 'iframe': + return location + default: { + const unhandled: never = slot + return unhandled + } + } +} diff --git a/frontend/src/lib/components/sessions/sessionMode.svelte.ts b/frontend/src/lib/components/sessions/sessionMode.svelte.ts index a4972a9afb..a2c20e08a8 100644 --- a/frontend/src/lib/components/sessions/sessionMode.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionMode.svelte.ts @@ -47,6 +47,17 @@ export function withMenuHidden(url: string, workspaceId?: string): string { } } +// True when this window is a sessions-preview iframe: embedded, with the `nomenubar` flag +// the preview always sets and the logged layout stickies into sessionStorage. +export function isSessionPreviewFrame(): boolean { + if (typeof window === 'undefined' || window.self === window.top) return false + try { + return sessionStorage.getItem('nomenubar_embedded') === 'true' + } catch { + return false + } +} + // Append `?workspace=` to a canonical route so a full-page navigation (e.g. // "Open in workspace") lands on the session's effective workspace instead of // the navigation workspace. Unlike withMenuHidden, the menu is kept visible — diff --git a/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts b/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts index a9db01e0fa..a90eb83cc4 100644 --- a/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionPreviewTabs.svelte.ts @@ -8,7 +8,10 @@ import { describeLocation, matchPreviewPage, showsView, + pageItemLocation, + pageItemUrl, parseArtifactRoute, + parsePageItemRoute, parsePipelineRoute, previewLocationContext, promptSafe, @@ -24,6 +27,12 @@ import { import type { SessionPreviewTab, SessionTarget } from './sessionState.svelte' import type { Kind } from '$lib/utils_deployable' import { pipelineFolderFromBundlePath } from '$lib/pipelinePaths' +import { + pageItemKindLabel, + TRIGGER_PAGES, + type PageItemRef, + type TriggerKind +} from './previewPaths' // The single live owner of a session's preview tabs. Runs behind a small // interface both the sessions page (renderer) and the `open_preview` tool cross, @@ -78,7 +87,10 @@ function keptVersion( // scheme. `onto` is the tab about to be written, passed wherever one is being re-pointed so // that every such path keeps its pin. function targetUrl(target: PreviewTarget, onto?: SessionPreviewTab): string { - if (target.type === 'page') return target.href + // A list page asked for with a row anchored is that row's own tab: its drawer would only + // open the editor a page item tab already hosts, inside a frame of its own. + if (target.type === 'page') return pageItemLocation(target.href) + if (target.type === 'pageitem') return pageItemUrl(target.ref) if (target.type === 'artifact') { return artifactUrl(target.id, target.name, keptVersion(target, onto)) } @@ -152,10 +164,9 @@ export function previewTargetForSessionTarget( // Adapt a deployable item's layout kind (the session review dock speaks `Kind`, // not SessionTarget) to a preview destination: the three live editors, data -// pipelines, plus legacy drag-and-drop apps, which the panel hosts as an iframe -// over their edit route. Every other kind maps to undefined — not for lack of any -// route (a variable or trigger has a list page the panel can host) but because -// there is no item editor to preview, so their row falls back to the diff. The +// pipelines, page items (variables, resources, schedules, triggers), plus legacy +// drag-and-drop apps, which the panel hosts as an iframe over their edit route. +// Every other kind maps to undefined, and its row falls back to the diff. The // undefined is also the caller's test for "can this row be previewed?". export function previewTargetForDeployKind(kind: Kind, path: string): PreviewTarget | undefined { if (kind === 'app') { @@ -164,6 +175,16 @@ export function previewTargetForDeployKind(kind: Kind, path: string): PreviewTar if (kind === 'script' || kind === 'flow' || kind === 'raw_app') { return previewTargetForSessionTarget(kind, path) } + if (kind === 'variable' || kind === 'resource' || kind === 'schedule') { + return { type: 'pageitem', ref: { kind, path } } + } + const triggerKind = kind.endsWith('_trigger') ? kind.slice(0, -'_trigger'.length) : undefined + if (triggerKind && triggerKind in TRIGGER_PAGES) { + return { + type: 'pageitem', + ref: { kind: 'trigger', triggerKind: triggerKind as TriggerKind, path } + } + } // A pipeline's editor is its folder's graph view, not its bundle path. if (kind === 'data_pipeline') { const folder = pipelineFolderFromBundlePath(path) @@ -189,7 +210,14 @@ export function hydratePreviewTabs(session: { seen.add(t.id) // Rebuilt field-by-field so stray properties on old saved records (e.g. the // retired `pinned` flag) don't survive hydration and get persisted back. - tabs.push({ id: t.id, url: t.url, loc: t.loc || t.url }) + // A list page saved with a row's drawer open comes back as that row's own tab. Read from + // where the frame was, not from what was last commanded: the user may have moved to + // another row since, or closed the drawer, and the observation is what they saw. + const loc = t.loc || t.url + const item = pageItemLocation(t.loc ? loc : t.url) + tabs.push( + parsePageItemRoute(item) ? { id: t.id, url: item, loc: item } : { id: t.id, url: t.url, loc } + ) } if (tabs.length > 0) { const wantActive = session.activePreviewTabId @@ -290,22 +318,14 @@ export class SessionPreviewTabs { // Drift is a change of what the frame *shows*, not of its URL string: a page // writing its own filter defaults back is not the user navigating away. const drifted = !showsView(tab.loc, url) - // Both cases the browser will not act on, decided here because this is where the - // old and new commands are both in hand: re-commanding the URL a drifted frame - // already carries moves nothing, and moving to another fragment resolves within the - // same document — so a list page never re-runs the `#` read that opens a row. - // Dropping the fragment is not one of them: the same-document path applies only to a - // target that has one, so the browser loads the page — closing the drawer by itself — - // and forcing a second load races that one back onto the row. - const fragmentOnly = - !commandUnchanged && url.includes('#') && tab.url.split('#')[0] === url.split('#')[0] + // Decided here because this is where the old and new commands are both in hand: + // re-commanding the URL a drifted frame already carries moves nothing. retargetTab(tab, url) - if ((commandUnchanged && drifted) || fragmentOnly) this.pulseReload(tab.id) + if (commandUnchanged && drifted) this.pulseReload(tab.id) } - // Force the host to reload the iframe. A navigation onto the tab's exact current URL - // changes nothing, so URL-driven behavior — a `#` opening a drawer the user has - // since closed — would never re-fire. + // Force the host to reload the tab. A navigation onto the tab's exact current URL + // changes nothing, so URL-driven behavior would never re-fire. pulseReload(id: string): void { this.#reloadPulse = { id, nonce: this.#reloadPulse.nonce + 1 } } @@ -435,21 +455,21 @@ export class SessionPreviewTabs { // shows this. The tab on this exact view wins over any other on the page — // `new_tab` puts two views side by side, and retargeting whichever sits first // would overwrite the other and leave both on the same row. - const shown = opts?.forceNewTab - ? undefined - : (this.#tabs.find((t) => showsView(t.loc, url)) ?? - this.#tabs.find((t) => describeLocation(t.loc).identity === describeLocation(url).identity)) + // A page item stays one tab whatever the opener asks: two would hold two drafts of it. + const shown = + opts?.forceNewTab && !parsePageItemRoute(url) + ? undefined + : (this.#tabs.find((t) => showsView(t.loc, url)) ?? + this.#tabs.find( + (t) => describeLocation(t.loc).identity === describeLocation(url).identity + )) if (shown) { const same = showsView(shown.loc, url) if (same) { // The frame is already here, but record what was asked for: `url` is what the // tab persists and remounts from, so leaving it on where the frame started - // sends a refresh back to the row the user has since moved off. + // sends a refresh back to the view the user has since moved off. recordCommand(shown, url) - // Nothing to navigate to, so nothing would re-run: the list pages read their - // `#` once per document, and the drawer it opens may since have been - // closed. Only a forced load can bring it back. - if (describeLocation(url).anchor) this.pulseReload(shown.id) } else { this.#retarget(shown, url) } @@ -512,7 +532,17 @@ export class SessionPreviewTabs { return } } - this.#retarget(t, targetUrl(target, t)) + // One tab per page item, as for editors: two would hold two drafts of one item. + const url = targetUrl(target, t) + if (parsePageItemRoute(url)) { + const existing = this.#tabs.find((x) => x.url === url) + if (existing && existing.id !== t.id) { + this.#activeId = existing.id + this.#flush() + return + } + } + this.#retarget(t, url) this.#flush() } @@ -574,6 +604,17 @@ export class SessionPreviewTabs { this.#flush() } + /** Follow a page item its editor saved under a new path, in place. */ + retargetPageItem(from: PageItemRef, to: PageItemRef): void { + const fromUrl = pageItemUrl(from) + const toUrl = pageItemUrl(to) + if (fromUrl === toUrl) return + const tab = this.#tabs.find((t) => t.url === fromUrl) + if (!tab) return + retargetTab(tab, toUrl) + this.#flush() + } + closeArtifact(artifactId: string): void { const tab = this.#tabs.find((t) => parseArtifactRoute(t.url)?.id === artifactId) if (tab) this.close(tab.id) @@ -606,23 +647,14 @@ export class SessionPreviewTabs { } // Feed back the location an iframe reported on load (only the page can read - // contentWindow.location). Updates the observed `loc`; `url` follows only when a - // drawer closed (below), and the host navigates on a command it isn't already at, - // so that write does not move the frame. + // contentWindow.location). Updates the observed `loc` only: the host navigates on a + // command it isn't already at, and an in-frame move is the user browsing. observeLocation(id: string, loc: string): void { const t = this.#tabs.find((x) => x.id === id) if (!t) return const canonical = canonicalizeObservedLoc(loc) if (t.loc === canonical) return t.loc = canonical - // Closing a drawer drops the row from the frame's URL. The command has to follow, or - // the tab reopens it on the next mount — the iframe loads `url`, not `loc`. Only the - // anchor: any other in-frame move is the user browsing, which must not re-command. - const commanded = describeLocation(t.url) - const observed = describeLocation(canonical) - if (commanded.anchor && !observed.anchor && commanded.identity === observed.identity) { - t.url = t.url.split('#')[0] - } this.#flush() } @@ -730,6 +762,7 @@ export function describePreview( const lines = tabs.map((t) => { const where = whereIs(t) const artifact = parseArtifactRoute(where) + const pageItem = parsePageItemRoute(where) const page = matchPreviewPage(where) const pipelineFolder = parsePipelineRoute(where) const route = parsePreviewItemRoute(where) @@ -737,16 +770,19 @@ export function describePreview( ? // A pinned tab is not showing what the assistant last wrote, and nothing else in this // summary would tell it so. `artifact "${artifact.name || 'Artifact'}"${artifact.version ? ` (pinned to v${artifact.version})` : ''}` - : page - ? `page "${page.label}"${previewLocationDetail(where)}` - : pipelineFolder - ? `pipeline "${pipelineFolder}"` - : route - ? `${route.raw_app ? 'raw_app' : route.kind} "${route.itemPath}"` - : // Trigger list pages land here (they're outside PREVIEW_PAGES), and - // their `#` is the trigger the drawer has open. - `${stripBase(where)}${previewLocationDetail(where)}` - const live = resolvePreviewTab(t.url).kind === 'editor' ? ', live editor' : '' + : pageItem + ? `${pageItemKindLabel(pageItem).toLowerCase()} "${pageItem.path}"` + : page + ? `page "${page.label}"${previewLocationDetail(where)}` + : pipelineFolder + ? `pipeline "${pipelineFolder}"` + : route + ? `${route.raw_app ? 'raw_app' : route.kind} "${route.itemPath}"` + : // Trigger list pages land here (they're outside PREVIEW_PAGES), and + // their `#` is the trigger the drawer has open. + `${stripBase(where)}${previewLocationDetail(where)}` + const slotKind = resolvePreviewTab(t.url).kind + const live = slotKind === 'editor' || slotKind === 'pageitem' ? ', live editor' : '' const active = t.id === activeId ? ', active' : '' // One list entry per tab: an artifact's name, a pipeline folder and an item path // all arrive decoded from a URL, so any of them could otherwise write a line here. diff --git a/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts b/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts index b71de62c23..8b1ba27ea0 100644 --- a/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts +++ b/frontend/src/lib/components/sessions/sessionPreviewTabs.test.ts @@ -177,10 +177,88 @@ describe('previewTargetForDeployKind', () => { pipelineTarget ) }) + it('routes variables, resources, schedules and triggers to their own tab', () => { + expect(previewTargetForDeployKind('schedule', 'u/me/s')).toEqual({ + type: 'pageitem', + ref: { kind: 'schedule', path: 'u/me/s' } + }) + expect(previewTargetForDeployKind('http_trigger', 'u/me/t')).toEqual({ + type: 'pageitem', + ref: { kind: 'trigger', triggerKind: 'http', path: 'u/me/t' } + }) + expect(previewTargetForDeployKind('variable', 'u/me/v')).toEqual({ + type: 'pageitem', + ref: { kind: 'variable', path: 'u/me/v' } + }) + }) + it('has no destination for kinds the preview panel cannot host', () => { - expect(previewTargetForDeployKind('schedule', 'u/me/s')).toBeUndefined() - expect(previewTargetForDeployKind('http_trigger', 'u/me/t')).toBeUndefined() - expect(previewTargetForDeployKind('variable', 'u/me/v')).toBeUndefined() + expect(previewTargetForDeployKind('folder', 'f/x')).toBeUndefined() + expect(previewTargetForDeployKind('resource_type', 'x')).toBeUndefined() + }) +}) + +describe('page item tabs', () => { + const variable: PreviewTarget = { + type: 'pageitem', + ref: { kind: 'variable', path: 'u/me/token' } + } + + it('opens a list page anchored at a row as that row’s own tab, beside the list', () => { + const o = owner() + o.open({ type: 'page', href: '/routes', label: 'HTTP routes' }) + o.open({ type: 'page', href: '/routes#u/me/a', label: 'HTTP routes' }) + expect(o.tabs.map((t) => t.url)).toEqual(['/routes', 'pageitem:trigger.http/u%2Fme%2Fa']) + + // Resources address their row through an extra segment. + o.open({ type: 'page', href: '/resources?owner=u#/resource/u/me/db', label: 'Resources' }) + expect(o.tabs.at(-1)!.url).toBe('pageitem:resource/u%2Fme%2Fdb') + }) + + it('keeps one tab per item, whatever the opener asks', () => { + const o = owner() + o.open(variable) + o.open({ type: 'page', href: '/runs', label: 'Runs' }) + expect(o.open({ type: 'page', href: '/variables#u/me/token', label: 'V' }).status).toBe( + 'focused' + ) + expect(o.open(variable, { forceNewTab: true }).status).toBe('focused') + o.navigate(variable) + expect(o.tabs).toHaveLength(2) + expect(o.activeId).toBe(o.tabs[0].id) + }) + + it('follows an item saved under a new path in place', () => { + const o = owner() + o.open(variable) + const id = o.tabs[0].id + o.retargetPageItem( + { kind: 'variable', path: 'u/me/token' }, + { kind: 'variable', path: 'f/x/token' } + ) + expect(o.tabs).toEqual([ + { id, url: 'pageitem:variable/f%2Fx%2Ftoken', loc: 'pageitem:variable/f%2Fx%2Ftoken' } + ]) + }) + + it('restores a tab saved on a row’s drawer as the row it was last on', () => { + const snap = hydratePreviewTabs({ + previewTabs: [ + { id: 'a', url: '/schedules#u/me/daily', loc: '/schedules?path=u#u/me/daily' }, + // A drawer opened inside the frame: the command is still the bare list. + { id: 'b', url: '/variables', loc: '/variables#u/me/token' }, + // The user moved on to another row inside the frame… + { id: 'c', url: '/variables#u/me/token', loc: '/variables#u/me/other' }, + // …or closed the drawer, leaving no row to restore. + { id: 'd', url: '/variables#u/me/token', loc: '/variables' } + ] + }) + expect(snap.tabs).toEqual([ + { id: 'a', url: 'pageitem:schedule/u%2Fme%2Fdaily', loc: 'pageitem:schedule/u%2Fme%2Fdaily' }, + { id: 'b', url: 'pageitem:variable/u%2Fme%2Ftoken', loc: 'pageitem:variable/u%2Fme%2Ftoken' }, + { id: 'c', url: 'pageitem:variable/u%2Fme%2Fother', loc: 'pageitem:variable/u%2Fme%2Fother' }, + { id: 'd', url: '/variables#u/me/token', loc: '/variables' } + ]) }) }) @@ -232,93 +310,34 @@ describe('SessionPreviewTabs.open', () => { expect(o.activeId).toBe(firstId) }) - // A trigger list page is not a `matchReusablePage`, so the runtime's - // navigate-in-place path doesn't cover it: re-pointing the tab has to happen - // here or the panel keeps showing the previously opened row. - it('re-points a page tab whose hash target changed instead of only focusing it', () => { - const o = owner() - const routes = (href: string) => ({ type: 'page' as const, href, label: 'HTTP routes' }) - o.open(routes('/routes#u/me/a')) - const firstId = o.activeId - - // 'retargeted', not 'opened': the tab count is unchanged, and the caller - // reports that to the model. - const res = o.open(routes('/routes#u/me/b')) - expect(res.status).toBe('retargeted') - expect(o.tabs).toHaveLength(1) - expect(o.activeId).toBe(firstId) - expect(o.tabs[0].url).toBe('/routes#u/me/b') - - // Back to the bare list: still the same tab, no longer anchored at a row. - expect(o.open(routes('/routes')).status).toBe('retargeted') - expect(o.tabs).toHaveLength(1) - expect(o.tabs[0].url).toBe('/routes') - - // ...and asking for the view it already shows is a plain focus. - expect(o.open(routes('/routes')).status).toBe('focused') - }) - // The list pages rewrite their own filter defaults into the URL after mount, // and `loc` follows that rewrite. Matching on anything but the path made a tab // stop recognizing itself, so every later open spawned a duplicate. it('still recognizes a tab after the page rewrote its own filter params', () => { const o = owner() const routes = (href: string) => ({ type: 'page' as const, href, label: 'HTTP routes' }) - o.open(routes('/routes#u/me/a')) - const id = o.tabs[0].id - o.observeLocation(id, '/routes?filter_path_of=trigger#u/me/a') + o.open(routes('/routes')) + o.observeLocation(o.tabs[0].id, '/routes?filter_path_of=trigger') - const res = o.open(routes('/routes#u/me/b')) - expect(res.status).toBe('retargeted') + expect(o.open(routes('/routes')).status).toBe('focused') expect(o.tabs).toHaveLength(1) - expect(o.tabs[0].url).toBe('/routes#u/me/b') }) // `new_tab` deliberately keeps two views of one page side by side. Reopening one of // them must focus the tab already showing it, not retarget whichever tab happens to - // sit first in the strip — that would overwrite the other view and leave two tabs - // on the same row. + // sit first in the strip — that would overwrite the other view. it('focuses the tab already showing the exact location before retargeting by path', () => { const o = owner() - const routes = (href: string) => ({ type: 'page' as const, href, label: 'HTTP routes' }) - o.open(routes('/routes#u/me/a')) + const runs = (href: string) => ({ type: 'page' as const, href, label: 'Runs' }) + o.open(runs('/runs?path=u/me/a')) const first = o.tabs[0].id - o.open(routes('/routes#u/me/b'), { forceNewTab: true }) + o.open(runs('/runs?path=u/me/b'), { forceNewTab: true }) const second = o.tabs[1].id - expect(o.open(routes('/routes#u/me/b')).status).toBe('focused') + expect(o.open(runs('/runs?path=u/me/b')).status).toBe('focused') expect(o.activeId).toBe(second) expect(o.tabs).toHaveLength(2) - expect(o.tabs.find((t) => t.id === first)?.url).toBe('/routes#u/me/a') - }) - - // The list pages read their `#` once per document, so a drawer the user closed - // inside the frame only comes back on a forced load — and re-commanding the location - // the tab already shows produces no navigation the host could act on. - it('forces a load when the requested row is the one the tab already shows', () => { - const o = owner() - const routes = (href: string) => ({ type: 'page' as const, href, label: 'HTTP routes' }) - o.open(routes('/routes#u/me/a')) - const id = o.tabs[0].id - o.observeLocation(id, '/routes?filter_path_of=trigger#u/me/a') - const before = o.reloadPulse.nonce - - expect(o.open(routes('/routes#u/me/a')).status).toBe('focused') - expect(o.reloadPulse).toEqual({ id, nonce: before + 1 }) - }) - - // Dropping the fragment is a load in itself, so the forced one lands on top of a - // navigation still in flight — and reloads the row the command asked to leave. - it('does not force a load when the requested location drops the row', () => { - const o = owner() - const routes = (href: string) => ({ type: 'page' as const, href, label: 'HTTP routes' }) - o.open(routes('/routes#u/me/a')) - const id = o.tabs[0].id - const before = o.reloadPulse.nonce - - o.navigate(routes('/routes')) - expect(o.tabs.find((t) => t.id === id)?.url).toBe('/routes') - expect(o.reloadPulse.nonce).toBe(before) + expect(o.tabs.find((t) => t.id === first)?.url).toBe('/runs?path=u/me/a') }) // Runs restores the user's "hide schedules" preference into the URL whenever a load @@ -365,29 +384,11 @@ describe('SessionPreviewTabs.open', () => { expect(o.tabs[0].url).toBe('/apps/edit/u/me/dash') }) - // Re-commanding the URL a tab is already pointed at changes nothing the host can - // see, so the frame would stay wherever the user navigated it inside the page. - it('forces a reload when the request matches the command but the frame drifted', () => { - const o = owner() - const routes = (href: string) => ({ type: 'page' as const, href, label: 'HTTP routes' }) - o.open(routes('/routes#u/me/a')) - const id = o.tabs[0].id - // The user clicked another trigger inside the iframe. - o.observeLocation(id, '/routes#u/me/b') - const before = o.reloadPulse.nonce - - const res = o.open(routes('/routes#u/me/a')) - expect(res.status).toBe('retargeted') - expect(o.tabs).toHaveLength(1) - expect(o.tabs[0].loc).toBe('/routes#u/me/a') - expect(o.reloadPulse.nonce).toBe(before + 1) - }) - it('forceNewTab opts a page out of the location dedupe', () => { const o = owner() const routes = (href: string) => ({ type: 'page' as const, href, label: 'HTTP routes' }) - o.open(routes('/routes#u/me/a')) - const res = o.open(routes('/routes#u/me/b'), { forceNewTab: true }) + o.open(routes('/routes')) + const res = o.open(routes('/routes'), { forceNewTab: true }) expect(res.status).toBe('opened') expect(o.tabs).toHaveLength(2) }) @@ -512,59 +513,6 @@ describe('SessionPreviewTabs.open', () => { }) }) -describe('SessionPreviewTabs.open — commanded url', () => { - it('records the requested row even when the frame is already showing it', () => { - const o = owner() - o.open({ type: 'page', href: '/routes#u/me/a', label: 'R' }) - // The user moves to another row inside the frame. - o.observeLocation(o.tabs[0].id, '/routes#u/me/b') - o.open({ type: 'page', href: '/routes#u/me/b', label: 'R' }) - // `url` is what a refresh and a remount reload from, so it has to follow. - expect(o.tabs[0].url).toBe('/routes#u/me/b') - expect(o.tabs).toHaveLength(1) - }) -}) - -describe('SessionPreviewTabs.observeLocation', () => { - it('drops the row from the command when the frame closes its drawer', () => { - const o = owner() - o.open({ type: 'page', href: '/routes#u/me/a', label: 'R' }) - // The page clears its own hash when the drawer closes. - o.observeLocation(o.tabs[0].id, '/routes?filter_path_of=trigger') - // The iframe mounts from `url`, so a remount would otherwise reopen the drawer. - expect(o.tabs[0].url).toBe('/routes') - }) - - it('leaves the command alone when the user just browses inside the frame', () => { - const o = owner() - o.open({ type: 'page', href: '/routes#u/me/a', label: 'R' }) - o.observeLocation(o.tabs[0].id, '/routes#u/me/b') - expect(o.tabs[0].url).toBe('/routes#u/me/a') - }) -}) - -describe('SessionPreviewTabs.open — forced loads', () => { - it('pulses when only the fragment changes, since the browser would not load', () => { - const o = owner() - o.open({ type: 'page', href: '/routes#u/me/a', label: 'R' }) - const before = o.reloadPulse.nonce - o.open({ type: 'page', href: '/routes#u/me/b', label: 'R' }) - // Same document: the browser resolves the new fragment without a load, so the - // list page never re-runs the `#` read that opens the row. - expect(o.reloadPulse.nonce).toBeGreaterThan(before) - expect(o.tabs).toHaveLength(1) - }) - - it('does not pulse when the document itself changes', () => { - const o = owner() - o.open({ type: 'page', href: '/routes#u/me/a', label: 'R' }) - const before = o.reloadPulse.nonce - o.open({ type: 'page', href: '/schedules#u/me/a', label: 'S' }) - // Different page: src changes, the browser loads it, nothing to force. - expect(o.reloadPulse.nonce).toBe(before) - }) -}) - describe('SessionPreviewTabs.navigate', () => { it('retargets the active tab to an editor item', () => { const o = owner() diff --git a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts index cca8c7cdfb..640b484478 100644 --- a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts @@ -56,7 +56,10 @@ import { selectPreviewTabsToClose, whereIs } from './sessionPreviewTabs.svelte' +import { pageItemKindLabel } from './previewPaths' import { + pageItemForLocation, + pageItemLocation, parsePreviewItemRoute, previewLocationContext, previewLocationLabel, @@ -128,6 +131,9 @@ export interface RawAppRuntimeValue { path: string custom_path?: string draft_path?: string + /** The app_version this cell's content forked from, carried so the editor's deploy + * guard and diff drawer compare the same pair the full-page editor does. */ + parent_version?: number } // The deployed baseline a raw-app cell diffs against (topbar Diff drawer). export interface RawAppSavedValue { @@ -143,6 +149,9 @@ export interface RawAppSavedValue { /** No deployed counterpart (draft-only); disables the topbar Diff. */ no_deployed?: boolean custom_path?: string + /** The deployed head at load time, which the editor's deploy guard falls back to when + * the draft carries no base of its own. */ + deployed_version?: number } // One editor cell per (kind, path) the session loads: the load slot plus the @@ -381,7 +390,8 @@ function createRuntime(session: Session): SessionRuntime { // What the side panel is showing, stamped on each user message so the chat // knows the page (and the row whose drawer is open) without spending a // get_preview_status round-trip. Live editors are skipped: they register - // themselves as the ACTIVE EDITOR through UserDraft's live-draft registry. + // themselves as the ACTIVE EDITOR through UserDraft's live-draft registry. A page + // item tab is not one of them, and reads as its list page with the item open. manager.activePreviewResolver = () => { const owner = getRuntime(session.id)?.previewTabs // What is on screen, not merely which tab is selected: the rule tells the model @@ -389,7 +399,8 @@ function createRuntime(session: Session): SessionRuntime { // point those at a page the user cannot see. const tab = owner?.displayedTab if (!tab) return undefined - if (resolvePreviewTab(tab.url).kind !== 'iframe') return undefined + const slotKind = resolvePreviewTab(tab.url).kind + if (slotKind !== 'iframe' && slotKind !== 'pageitem') return undefined return previewLocationContext(whereIs(tab)) } // Pre-flight: materialise the (still-transient) session, then commit @@ -587,8 +598,9 @@ function createRuntime(session: Session): SessionRuntime { // when the path has never been deployed. const aiDraft = UserDraft.get('flow', path, { workspace }) - // getDraft=true omits version_id (the plain getFlowByPath has it) — - // stamp it on so the flow doesn't always diff. Best-effort. + // Fallback for the head: the payload fetches below carry `version_id`, and + // this one covers a response that does not. Best-effort, and a request of + // its own, so the same response wins wherever both are available. let deployedVersionId: number | undefined try { deployedVersionId = (await FlowService.getFlowByPath({ workspace, path }))?.version_id @@ -602,12 +614,16 @@ function createRuntime(session: Session): SessionRuntime { // yet on the backend — draft-only flows are a valid state. try { const result = await FlowService.getFlowByPath({ workspace, path, getDraft: true }) - saved.val = result as SavedFlow + // The editor's deploy guard compares against the head, so keep this + // response's own and fall back to the one fetched above. + saved.val = { + ...(result as SavedFlow), + version_id: (result as SavedFlow).version_id ?? deployedVersionId + } } catch { saved.val = undefined } await initFlow(aiDraft, store, stateStore, workspace) - if (deployedVersionId != null && store.val) store.val.version_id = deployedVersionId slot.loadedPath = path slot.loadedWorkspace = workspace return @@ -615,8 +631,12 @@ function createRuntime(session: Session): SessionRuntime { // No local draft yet — seed from `result.draft ?? result`. const result = await FlowService.getFlowByPath({ workspace, path, getDraft: true }) - saved.val = result as SavedFlow - const flow: Flow = ((result as SavedFlow).draft ?? (result as Flow)) as Flow + saved.val = { + ...(result as SavedFlow), + version_id: (result as SavedFlow).version_id ?? deployedVersionId + } + const serverDraft = (result as SavedFlow).draft as Flow | undefined + const flow: Flow = (serverDraft ?? (result as Flow)) as Flow // Seed the per-tab last_sync from the server draft's timestamp so the // seeding save below attaches a matching last_sync and the server can // reject stale writes (see loadRawApp). Without this a server draft — @@ -629,7 +649,13 @@ function createRuntime(session: Session): SessionRuntime { ) UserDraft.save('flow', path, flow, { workspace }) await initFlow(flow, store, stateStore, workspace) - if (deployedVersionId != null && store.val) store.val.version_id = deployedVersionId + // A draft keeps the base it forked from, unknown included (it then falls + // back to the timestamps); only a fresh checkout takes the head, which is + // also what keeps it from always diffing. See loadScript. The head comes + // from the response that supplied the payload, so a deploy landing between + // the two requests cannot label this checkout as forked from the older one. + const head = (result as SavedFlow).version_id ?? deployedVersionId + if (head != null && store.val && !serverDraft) store.val.version_id = head slot.loadedPath = path slot.loadedWorkspace = workspace } catch (err) { @@ -672,11 +698,10 @@ function createRuntime(session: Session): SessionRuntime { } // Clone before layering the AI draft on top, else we'd mutate // `saved.val` in place and lose the pristine diff baseline. + const savedDraft = saved.val?.draft as NewScript | undefined const baseline: NewScript = saved.val ? (structuredClone( - $state.snapshot( - (saved.val.draft as NewScript | undefined) ?? (saved.val as NewScript) - ) + $state.snapshot(savedDraft ?? (saved.val as NewScript)) ) as NewScript) : { // Seed from the draft's own path (a rename lives in `draft_path`, @@ -694,9 +719,8 @@ function createRuntime(session: Session): SessionRuntime { schema: emptySchema(), language: (aiDraft.language ?? 'bun') as any } - if (saved.val?.hash) { - baseline.parent_hash = saved.val.hash - } + // Only a fresh checkout forks from the head; see the branch below. + if (!savedDraft && saved.val?.hash) baseline.parent_hash = saved.val.hash baseline.content = aiDraft.content if (aiDraft.language) baseline.language = aiDraft.language if (aiDraft.summary !== undefined) baseline.summary = aiDraft.summary @@ -711,10 +735,13 @@ function createRuntime(session: Session): SessionRuntime { saved.val = result as SavedScript // Clone before mutating, else `baseline` aliases `result` and // `baseline.parent_hash` corrupts the diff baseline. - const baseline = structuredClone( - ((result as SavedScript).draft as NewScript | undefined) ?? (result as NewScript) - ) - baseline.parent_hash = result.hash + const serverDraft = (result as SavedScript).draft as NewScript | undefined + const baseline = structuredClone(serverDraft ?? (result as NewScript)) + // Only a fresh checkout forks from the head. A draft keeps the base it has, + // unknown included: `draft.base` is derived from `parent_hash` on every + // save, so stamping the head over it would say this draft is up to date + // when it is not. An unknown base falls back to the timestamps. + if (!serverDraft) baseline.parent_hash = result.hash // Seed the per-tab last_sync from the server draft's timestamp so the // seeding save below attaches a matching last_sync and the server can // reject stale writes (see loadRawApp). Without this a server draft — @@ -775,7 +802,10 @@ function createRuntime(session: Session): SessionRuntime { path: result.path, policy: result.policy, custom_path: result.custom_path, - no_deployed: result.no_deployed + no_deployed: result.no_deployed, + deployed_version: Array.isArray(result.versions) + ? result.versions[result.versions.length - 1] + : undefined } } catch { saved.val = undefined @@ -811,7 +841,10 @@ function createRuntime(session: Session): SessionRuntime { path: result.path, policy: result.policy, custom_path: result.custom_path, - no_deployed: result.no_deployed + no_deployed: result.no_deployed, + deployed_version: Array.isArray(result.versions) + ? result.versions[result.versions.length - 1] + : undefined } // Prefer the server draft over the deployed value (mirrors the // flow/script `result.draft ?? result`). A raw-app draft is already @@ -841,7 +874,14 @@ function createRuntime(session: Session): SessionRuntime { summary: draftValue?.summary ?? result.summary ?? '', path: result.path, custom_path: draftValue?.custom_path ?? result.custom_path, - draft_path: draftValue?.draft_path + draft_path: draftValue?.draft_path, + // Only a fresh checkout forks from the head; a draft keeps its own base, + // unknown included, or it would read as up to date. See loadScript. + parent_version: draftValue + ? draftValue.parent_version + : Array.isArray(result.versions) + ? result.versions[result.versions.length - 1] + : undefined } // Seed the per-tab last_sync from the server draft's timestamp so // later saves attach a matching last_sync and the server can reject @@ -1066,7 +1106,9 @@ async function applyRemoteTurnEnd(sessionId: string, chatId: string): Promise { // open_page dispatches here to show a workspace page (Runs/Schedules) as a page // tab in the calling session's preview panel. Returns undefined when there is no // session so open_page can fall back to browser navigation. -setOpenPagePreviewHandler(({ sessionId: callerSessionId, href, label, newTab }) => { +setOpenPagePreviewHandler(({ sessionId: callerSessionId, href, label: pageLabel, newTab }) => { const sessionId = callerSessionId ?? sessionState.currentSessionId if (!sessionId) return undefined const session = sessionState.sessions.find((s) => s.id === sessionId) if (!session) return undefined const owner = getOrCreateRuntime(session).previewTabs + // A page opened on one item is that item's tab, and the report has to name what opened. + const pageItem = pageItemForLocation(href) + const label = pageItem + ? `the ${pageItemKindLabel(pageItem).toLowerCase()} ${promptSafe(pageItem.path)}` + : pageLabel // open() owns the whole decision — which tab already shows this page, whether the // requested view differs from what it shows, and whether a forced load is needed to // re-fire a drawer. Deciding any of that again here means two predicates for one diff --git a/frontend/src/lib/components/settings/CreateToken.svelte b/frontend/src/lib/components/settings/CreateToken.svelte index 20e4891189..8ee5391443 100644 --- a/frontend/src/lib/components/settings/CreateToken.svelte +++ b/frontend/src/lib/components/settings/CreateToken.svelte @@ -1,15 +1,22 @@ diff --git a/frontend/src/lib/components/triggers/AddTriggersButton.svelte b/frontend/src/lib/components/triggers/AddTriggersButton.svelte index 4027bf9081..17387d13e0 100644 --- a/frontend/src/lib/components/triggers/AddTriggersButton.svelte +++ b/frontend/src/lib/components/triggers/AddTriggersButton.svelte @@ -8,7 +8,9 @@ import { isCloudHosted } from '$lib/cloud' import { CloudOff } from 'lucide-svelte' import { isServiceAvailable } from './native/utils' - import { workspaceStore } from '$lib/stores' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { setDropdownWidthToButtonWidth?: boolean @@ -41,15 +43,15 @@ let githubAvailable = $state(false) async function setNextcloudState() { - nextcloudAvailable = await isServiceAvailable('nextcloud', $workspaceStore!) + nextcloudAvailable = await isServiceAvailable('nextcloud', $operatingWorkspace!) } async function setGoogleState() { - googleAvailable = await isServiceAvailable('google', $workspaceStore!) + googleAvailable = await isServiceAvailable('google', $operatingWorkspace!) } async function setGithubState() { - githubAvailable = await isServiceAvailable('github', $workspaceStore!) + githubAvailable = await isServiceAvailable('github', $operatingWorkspace!) } setNextcloudState() diff --git a/frontend/src/lib/components/triggers/CaptureSection.svelte b/frontend/src/lib/components/triggers/CaptureSection.svelte index eebfe3c1a7..5b1980e1b4 100644 --- a/frontend/src/lib/components/triggers/CaptureSection.svelte +++ b/frontend/src/lib/components/triggers/CaptureSection.svelte @@ -26,7 +26,6 @@ import { Pane, Splitpanes } from 'svelte-splitpanes' import { Popover } from '$lib/components/meltComponents' import { CaptureService } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import { isObject, sendUserToast } from '$lib/utils' import { triggerIconMap } from './utils' import { formatDateShort } from '$lib/utils' @@ -37,6 +36,9 @@ import { twMerge } from 'tailwind-merge' import { FlaskConical } from 'lucide-svelte' import Alert from '../common/alert/Alert.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { disabled?: boolean | undefined @@ -139,7 +141,7 @@ if (!captureInfo.path) return const captures = await CaptureService.listCaptures({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, runnableKind: captureInfo.isFlow ? 'flow' : 'script', path: captureInfo.path, triggerKind: captureType, @@ -167,7 +169,7 @@ try { const captures = await CaptureService.listCaptures({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, runnableKind: captureInfo.isFlow ? 'flow' : 'script', path: captureInfo.path, triggerKind: captureType, @@ -213,7 +215,7 @@ try { isLoadingBigPayload = true const fullCapture = await CaptureService.getCapture({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, id: capture.id }) diff --git a/frontend/src/lib/components/triggers/CaptureTable.svelte b/frontend/src/lib/components/triggers/CaptureTable.svelte index 2d20ac02e1..ea0cf5e256 100644 --- a/frontend/src/lib/components/triggers/CaptureTable.svelte +++ b/frontend/src/lib/components/triggers/CaptureTable.svelte @@ -10,7 +10,6 @@ import { createEventDispatcher, onDestroy, untrack } from 'svelte' import { type TriggerKind } from '../triggers' import { CaptureService } from '$lib/gen' - import { workspaceStore } from '$lib/stores' import { type CaptureTriggerKind } from '$lib/gen' import CaptureButton from '$lib/components/triggers/CaptureButton.svelte' import InfiniteList from '../InfiniteList.svelte' @@ -19,6 +18,9 @@ import type { Capture } from '$lib/gen' import { AwsIcon, MqttIcon, AmqpIcon } from '../icons' import GoogleCloudIcon from '../icons/GoogleCloudIcon.svelte' + import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte' + + const operatingWorkspace = useOperatingWorkspace() interface Props { path: string @@ -33,9 +35,8 @@ limitPayloadSize?: boolean noBorder?: boolean captureActiveIndicator?: boolean | undefined - // Workspace to scope capture list/get/delete calls to. Defaults to the nav - // `$workspaceStore`; an AI-session live editor passes the session's acting - // workspace (a fork) so captures hit the right workspace. + // Workspace to scope capture list/get/delete calls to. Defaults to the operating + // workspace (see `useOperatingWorkspace`). workspace?: string } @@ -55,7 +56,7 @@ workspace = undefined }: Props = $props() - let ws = $derived(workspace ?? $workspaceStore) + let ws = $derived(workspace ?? $operatingWorkspace) let selected: number | undefined = $state(undefined) let testKind: 'preprocessor' | 'main' = $state('main') diff --git a/frontend/src/lib/components/triggers/CaptureWrapper.svelte b/frontend/src/lib/components/triggers/CaptureWrapper.svelte index bfffbfc56b..9b616454fd 100644 --- a/frontend/src/lib/components/triggers/CaptureWrapper.svelte +++ b/frontend/src/lib/components/triggers/CaptureWrapper.svelte @@ -1,5 +1,4 @@ {#if captureInfo} diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerEditorConfigSection.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerEditorConfigSection.svelte index 3a60d3be71..4a44e01b30 100644 --- a/frontend/src/lib/components/triggers/email/EmailTriggerEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/email/EmailTriggerEditorConfigSection.svelte @@ -2,8 +2,6 @@ import { Alert } from '$lib/components/common' import Required from '$lib/components/Required.svelte' import Section from '$lib/components/Section.svelte' - import { userStore, workspaceStore } from '$lib/stores' - import { getTriggerWorkspace } from '$lib/components/triggers/triggerWorkspace' // import { page } from '$app/state' import { getEmailAddress, getEmailDomain } from './utils' import { isCloudHosted } from '$lib/cloud' @@ -12,6 +10,10 @@ import { untrack } from 'svelte' import { EmailTriggerService } from '$lib/gen' import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte' + import { + useOperatingUser, + useOperatingWorkspace + } from '$lib/components/operatingWorkspace.svelte' interface Props { initialTriggerPath?: string | undefined dirtyLocalPart?: boolean @@ -35,8 +37,10 @@ isDraftOnly = true, showTestingBadge = false }: Props = $props() - const triggerWs = getTriggerWorkspace() - const wsId = $derived(triggerWs?.() ?? $workspaceStore) + const operatingWorkspace = useOperatingWorkspace() + const operatingUser = useOperatingUser() + const actingUser = $derived(operatingUser.current) + const wsId = $derived($operatingWorkspace) let validateTimeout: number | undefined = undefined @@ -95,7 +99,7 @@ local_part === undefined && (local_part = '') }) - let userIsAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin) + let userIsAdmin = $derived(actingUser?.is_admin || actingUser?.is_super_admin) let userCanEditConfig = $derived(userIsAdmin || isDraftOnly) // User can edit config if they are admin or if the trigger is a draft which will not be saved diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte index d2133cf865..30f848004f 100644 --- a/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/email/EmailTriggerEditorInner.svelte @@ -2,6 +2,7 @@ import { Button } from '$lib/components/common' import { clearPageDrawerAnchor, + handOffPageDrawer, setPageDrawerAnchor } from '$lib/components/sessions/pageDrawerSession' import { TRIGGER_PAGES } from '$lib/components/sessions/previewPaths' @@ -17,8 +18,7 @@ type Retry, type TriggerMode } from '$lib/gen' - import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores' - import { getTriggerWorkspace } from '$lib/components/triggers/triggerWorkspace' + import { usedTriggerKinds } from '$lib/stores' import { canWrite, capitalize, emptyString, sendUserToast } from '$lib/utils' import Section from '$lib/components/Section.svelte' import { Loader2 } from 'lucide-svelte' @@ -38,9 +38,16 @@ import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte' import TriggerSuspendedJobsAlert from '../TriggerSuspendedJobsAlert.svelte' import TriggerSuspendedJobsModal from '../TriggerSuspendedJobsModal.svelte' + import { + useOperatingUser, + useOperatingWorkspace, + useOperatingWorkspaceHref + } from '$lib/components/operatingWorkspace.svelte' let { useDrawer = true, + inline = false, + onClose = undefined, hideTarget = false, description = undefined, isEditor = false, @@ -55,8 +62,11 @@ trigger = undefined, customSaveBehavior = undefined } = $props() - const triggerWs = getTriggerWorkspace() - const wsId = $derived(triggerWs?.() ?? $workspaceStore) + const operatingWorkspace = useOperatingWorkspace() + const operatingUser = useOperatingUser() + const actingUser = $derived(operatingUser.current) + const operatingHref = useOperatingWorkspaceHref() + const wsId = $derived($operatingWorkspace) // Form data state let initialPath = $state('') @@ -75,7 +85,13 @@ let workspaced_local_part = $state(false) let drawerLoading = $state(true) let showLoader = $state(false) - let can_write = $state(true) + let permsPath = $state(undefined) + let permsForWrite = $state | undefined>(undefined) + // The acting user in the operating workspace arrives asynchronously, and an unknown user + // refuses — so the editor stays read-only until the lookup lands, which is the safe answer. + const can_write = $derived( + permsPath === undefined ? true : canWrite(permsPath, permsForWrite ?? {}, actingUser) + ) let extraPerms = $state | undefined>(undefined) let error_handler_path: string | undefined = $state() let error_handler_args: Record = $state({}) @@ -95,7 +111,7 @@ let originalConfig = $state(undefined) let hasChanged = $derived(!deepEqual(getEmailTriggerConfig(), originalConfig ?? {})) - const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin) + const isAdmin = $derived(actingUser?.is_admin || actingUser?.is_super_admin) const emailConfig = $derived.by(getEmailTriggerConfig) const draftSync = useTriggerDraftSync({ @@ -127,6 +143,9 @@ defaultConfig?: Partial, fixedScriptPath_?: string ) { + if (handOffPageDrawer(TRIGGER_PAGES.email.path, ePath)) return + // A `whoami` that failed earlier would otherwise pin this workspace to "unknown user". + operatingUser.forgetFailures() drawerLoading = true let loader = setTimeout(() => { showLoader = true @@ -206,6 +225,8 @@ } function loadTriggerConfig(cfg?: Partial): void { + // The loaded trigger says what it runs; an opener's `isFlow` is only its guess. + if (cfg?.is_flow !== undefined) itemKind = cfg.is_flow ? 'flow' : 'script' script_path = cfg?.script_path ?? '' initialScriptPath = cfg?.script_path ?? '' is_flow = cfg?.is_flow ?? false @@ -213,7 +234,8 @@ local_part = cfg?.local_part ?? '' workspaced_local_part = cfg?.workspaced_local_part ?? false extraPerms = cfg?.extra_perms ?? undefined - can_write = canWrite(path, cfg?.extra_perms ?? {}, $userStore) + permsPath = path + permsForWrite = cfg?.extra_perms ?? {} error_handler_path = cfg?.error_handler_path error_handler_args = cfg?.error_handler_args ?? {} retry = cfg?.retry @@ -265,7 +287,7 @@ saveCfg, edit, wsId!, - !!$userStore?.is_admin || !!$userStore?.is_super_admin, + !!actingUser?.is_admin || !!actingUser?.is_super_admin, usedTriggerKinds ) if (isSaved) { @@ -405,7 +427,7 @@ bind:scriptPath={script_path} {initialScriptPath} canWrite={can_write} - isOperator={!!$userStore?.operator} + isOperator={!!actingUser?.operator} promptClass="text-xs mt-3 mb-1 text-primary" > {#snippet createButton()} @@ -414,7 +436,9 @@ btnClasses="ml-4" variant="accent" size="xs" - href={itemKind === 'flow' ? '/flows/add?hub=72' : '/scripts/add?hub=hub%2F19813'} + href={operatingHref( + itemKind === 'flow' ? '/flows/add?hub=72' : '/scripts/add?hub=hub%2F19813' + )} target="_blank">Create from template {/if} @@ -486,36 +510,44 @@ {/if} {/snippet} -{#if useDrawer} +{#snippet drawerBody()} + (inline ? onClose?.() : drawer?.closeDrawer())} + > + {#snippet actions()} + {@render saveButton()} + {/snippet} + {#snippet banner()} + draftSync.deployed} + reserveSpace={draftSync.hasBaseline} + getCurrent={() => draftSync.current} + onDiscard={() => draftSync.resetToDeployed(initialPath)} + disabled={!can_write} + /> + {/snippet} + {@render config()} + +{/snippet} + +{#if useDrawer && inline} + {@render drawerBody()} +{:else if useDrawer} clearPageDrawerAnchor(TRIGGER_PAGES.email.path)} > - drawer?.closeDrawer()} - > - {#snippet actions()} - {@render saveButton()} - {/snippet} - {#snippet banner()} - draftSync.deployed} - reserveSpace={draftSync.hasBaseline} - getCurrent={() => draftSync.current} - onDiscard={() => draftSync.resetToDeployed(initialPath)} - disabled={!can_write} - /> - {/snippet} - {@render config()} - + {@render drawerBody()} {:else}
diff --git a/frontend/src/lib/components/triggers/email/EmailTriggerPanel.svelte b/frontend/src/lib/components/triggers/email/EmailTriggerPanel.svelte index 12ec403aeb..7eac6a8971 100644 --- a/frontend/src/lib/components/triggers/email/EmailTriggerPanel.svelte +++ b/frontend/src/lib/components/triggers/email/EmailTriggerPanel.svelte @@ -1,11 +1,15 @@
{#if resourceId} -
+
Selected: {resourceName || resourceId}
{:else} -
+
No file selected
@@ -198,21 +196,27 @@
+ {/if} + diff --git a/frontend/src/routes/user/login_link_expired/+page.svelte b/frontend/src/routes/user/login_link_expired/+page.svelte index b160985bb2..6c99d35408 100644 --- a/frontend/src/routes/user/login_link_expired/+page.svelte +++ b/frontend/src/routes/user/login_link_expired/+page.svelte @@ -17,7 +17,7 @@ diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 0140c38739..36c353e28a 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -6,7 +6,10 @@ import mkcert from 'vite-plugin-mkcert' const file = fileURLToPath(new URL('package.json', import.meta.url)) const json = readFileSync(file, 'utf8') -const version = JSON.parse(json) +// Only the version is exposed to the client (see `define` below). Defining the +// whole parsed package.json would inline it — scripts, dependency lists, ... — +// into every chunk that reads `__pkg__.version`. +const { version } = JSON.parse(json) // The postinstall downloads the pinned UI Builder artifact into static/ui_builder, // which SvelteKit serves at /ui_builder. Serve that directly; only proxy to a @@ -278,7 +281,7 @@ const config = { assertAcyclicChunks(), assertLeanPublicAppRoutes() ], - define: { __pkg__: version }, + define: { '__pkg__.version': JSON.stringify(version) }, optimizeDeps: { include: ['highlight.js', 'highlight.js/lib/core', 'monaco-vim'], exclude: [ diff --git a/lsp/Pipfile b/lsp/Pipfile index 054896a5ce..ab4f26aab3 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.814.0" +wmill = ">=1.815.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/multiplayer/package-lock.json b/multiplayer/package-lock.json index 0d8c1985ce..532dbd226e 100644 --- a/multiplayer/package-lock.json +++ b/multiplayer/package-lock.json @@ -47,9 +47,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -88,9 +88,9 @@ } }, "node_modules/y-websocket": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.0.0.tgz", - "integrity": "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-3.1.0.tgz", + "integrity": "sha512-ZNzwH84Ysxv7zjpFNZHjTJvrBZgcAqMljTe+6zrWciAML9LQ18aVylyPNH9faxCXqEOV8I0JY4TGtrIHFX+Xwg==", "license": "MIT", "dependencies": { "lib0": "^0.2.102", @@ -109,9 +109,9 @@ } }, "node_modules/yjs": { - "version": "13.6.29", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz", - "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==", + "version": "13.6.32", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.32.tgz", + "integrity": "sha512-lfiJIIC4Xayt5ItynE407ehlE03pCjeOc4hkR4yxxvvNJ4kuiN25B0g+Qp8XagYz361LLL7DCzR5bvFJ81QKtQ==", "license": "MIT", "dependencies": { "lib0": "^0.2.99" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 73c11300d7..6ed1038148 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.814.0 + version: 1.815.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index 7abe5afa7c..b35fbd5ecc 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.814.0' + ModuleVersion = '1.815.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index ad591ded27..7a9f55641e 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.814.0" +version = "1.815.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill/uv.lock b/python-client/wmill/uv.lock index 6c1c3ab219..1c366b42bd 100644 --- a/python-client/wmill/uv.lock +++ b/python-client/wmill/uv.lock @@ -79,11 +79,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -115,16 +115,16 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pytest" -version = "9.0.2" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -133,7 +133,7 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] diff --git a/rust-client/.gitignore b/rust-client/.gitignore index 4924b1576c..777fcbdb05 100644 --- a/rust-client/.gitignore +++ b/rust-client/.gitignore @@ -1,3 +1,5 @@ windmill-api/ windmill_api/ api/ +# library crate: the lockfile is regenerated by dev.nu and not consumed by downstream users +Cargo.lock diff --git a/rust-client/Cargo.lock b/rust-client/Cargo.lock deleted file mode 100644 index 54464b9aa2..0000000000 --- a/rust-client/Cargo.lock +++ /dev/null @@ -1,1857 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "cc" -version = "1.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dyn-clone" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" -dependencies = [ - "equivalent", - "hashbrown 0.15.3", - "serde", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quinn" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "ref-cast" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "reqwest" -version = "0.12.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustls" -version = "0.23.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" -dependencies = [ - "base64", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.9.0", - "schemars", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.9.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" -dependencies = [ - "getrandom 0.3.3", - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windmill-api" -version = "1.518.2" -dependencies = [ - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serde_with", - "url", - "uuid", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "wmill" -version = "1.518.2" -dependencies = [ - "anyhow", - "futures", - "once_cell", - "serde", - "serde_json", - "serde_yaml", - "thiserror", - "tokio", - "uuid", - "windmill-api", -] - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 753b906793..e375863aa3 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.814.0", + "version": "1.815.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts", "./wacError.ts"] diff --git a/typescript-client/package-lock.json b/typescript-client/package-lock.json index 20b2270c6e..f881fc7308 100644 --- a/typescript-client/package-lock.json +++ b/typescript-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-client", - "version": "1.999.21", + "version": "1.813.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-client", - "version": "1.999.21", + "version": "1.813.0", "license": "Apache 2.0", "devDependencies": { "@types/node": "^20.17.16", @@ -480,9 +480,9 @@ } }, "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "dev": true, "license": "MIT" }, @@ -603,9 +603,9 @@ "license": "MIT" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { diff --git a/typescript-client/package.json b/typescript-client/package.json index 327a37e3f5..49c63650e7 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.814.0", + "version": "1.815.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "homepage": "https://github.com/windmill-labs/windmill/tree/main/typescript-client#readme", diff --git a/version.txt b/version.txt index 9f38cdf20a..297171ea20 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.814.0 +1.815.0 diff --git a/windmill-yaml-validator/package-lock.json b/windmill-yaml-validator/package-lock.json index 0c9e94cd0a..c65c508fe7 100644 --- a/windmill-yaml-validator/package-lock.json +++ b/windmill-yaml-validator/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-yaml-validator", - "version": "1.814.0", + "version": "1.815.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-yaml-validator", - "version": "1.814.0", + "version": "1.815.0", "license": "Apache 2.0", "dependencies": { "@stoplight/yaml": "^4.3.0", @@ -20,28 +20,14 @@ "typescript": "^5.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -50,9 +36,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -60,22 +46,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -91,14 +77,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -108,14 +94,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -125,9 +111,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -135,29 +121,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -177,9 +163,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -187,9 +173,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -197,9 +183,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -207,27 +193,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -476,33 +462,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -510,14 +496,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -860,6 +846,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1092,9 +1089,9 @@ "license": "MIT" }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1303,10 +1300,23 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", + "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1328,9 +1338,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", "dev": true, "funding": [ { @@ -1348,10 +1358,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -1411,9 +1422,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -1665,9 +1676,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", + "version": "1.5.430", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", + "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", "dev": true, "license": "ISC" }, @@ -1799,9 +1810,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -1835,9 +1846,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1845,9 +1856,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -2857,9 +2868,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { @@ -3044,9 +3055,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3078,11 +3089,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -3252,9 +3266,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3790,9 +3804,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { diff --git a/windmill-yaml-validator/package.json b/windmill-yaml-validator/package.json index 7a15686549..ec013fca2e 100644 --- a/windmill-yaml-validator/package.json +++ b/windmill-yaml-validator/package.json @@ -1,6 +1,6 @@ { "name": "windmill-yaml-validator", - "version": "1.814.0", + "version": "1.815.0", "description": "YAML validator for Windmill flow, schedule, and trigger files", "main": "dist/index.js", "types": "dist/index.d.ts",