@cartanova/qgrid-cli 2.10.10 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/dist/_virtual/_rolldown/runtime.js +1 -10
- package/bundle/dist/application/monit/log-buffer.js +12 -6
- package/bundle/dist/application/monit/monit.frame.js +11 -3
- package/bundle/dist/application/monit/monit.types.js +1 -1
- package/bundle/dist/application/qgrid/conv-routing.js +9 -6
- package/bundle/dist/application/qgrid/expired-token-reminder.js +2 -2
- package/bundle/dist/application/qgrid/oauth.js +31 -4
- package/bundle/dist/application/qgrid/qgrid-image-generation.js +1 -1
- package/bundle/dist/application/qgrid/qgrid-response-format.js +21 -19
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +40 -33
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +128 -93
- package/bundle/dist/application/qgrid/qgrid.frame.js +930 -49
- package/bundle/dist/application/qgrid/qgrid.types.js +212 -5
- package/bundle/dist/application/qgrid/schema-prompt.js +75 -48
- package/bundle/dist/application/qgrid/slack-user-map.js +1 -1
- package/bundle/dist/application/qgrid/token-death.js +1 -1
- package/bundle/dist/application/qgrid/token-subscriber.js +83 -30
- package/bundle/dist/application/qgrid/token-trigger-setup.js +1 -1
- package/bundle/dist/application/qgrid/token-window-keepalive.js +8 -6
- package/bundle/dist/application/qgrid/tool-emulation-schema.js +13 -13
- package/bundle/dist/application/qgrid/tool-emulation.js +19 -15
- package/bundle/dist/application/request-log/json-schema-type-text.js +29 -12
- package/bundle/dist/application/request-log/request-log.model.js +83 -70
- package/bundle/dist/application/request-log/request-log.types.js +1 -1
- package/bundle/dist/application/request-log/zod-code-format.js +1 -1
- package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
- package/bundle/dist/application/request-log-step/request-log-step.types.js +1 -1
- package/bundle/dist/application/setting/setting.constant.js +1 -1
- package/bundle/dist/application/setting/setting.model.js +2 -2
- package/bundle/dist/application/setting/setting.store.js +2 -2
- package/bundle/dist/application/setting/setting.types.js +1 -1
- package/bundle/dist/application/sonamu.generated.js +32 -16
- package/bundle/dist/application/sonamu.generated.sso.js +1 -1
- package/bundle/dist/application/token/token.model.js +26 -9
- package/bundle/dist/application/token/token.types.js +20 -3
- package/bundle/dist/i18n/en.js +15 -1
- package/bundle/dist/i18n/ko.js +15 -1
- package/bundle/dist/i18n/sd.generated.js +1 -1
- package/bundle/dist/migrations/20260807133553_manual__backfill_request_log_cost.js +8 -6
- package/bundle/dist/server-bootstrap.js +4 -2
- package/bundle/dist/server-error-handler.js +1 -1
- package/bundle/dist/sonamu.config.js +37 -8
- package/bundle/dist/startup-migrations.js +17 -7
- package/bundle/dist/testing/global.js +1 -1
- package/bundle/dist/testing/schema-bench.js +0 -0
- package/bundle/dist/testing/test-database-name.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +11 -5
- package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +4 -4
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
- package/bundle/dist/utils/providers/anthropic/claude-session.js +20 -19
- package/bundle/dist/utils/providers/anthropic/fence-strip.js +1 -1
- package/bundle/dist/utils/providers/anthropic/stream-json-adapter.js +49 -23
- package/bundle/dist/utils/providers/common/credentials.js +5 -1
- package/bundle/dist/utils/providers/common/effort.js +3 -3
- package/bundle/dist/utils/providers/common/model-cost.js +69 -14
- package/bundle/dist/utils/providers/common/provider-types.js +29 -1
- package/bundle/dist/utils/providers/common/schema-validation.js +78 -40
- package/bundle/dist/utils/providers/common/smooth-weighted-round-robin.js +1 -1
- package/bundle/dist/utils/providers/common/strictifier.js +62 -43
- package/bundle/dist/utils/providers/common/thinking.js +3 -2
- package/bundle/dist/utils/providers/common/ttft.js +1 -1
- package/bundle/dist/utils/providers/grok/grok-backend-protocol.js +427 -0
- package/bundle/dist/utils/providers/grok/grok-constants.js +55 -0
- package/bundle/dist/utils/providers/grok/grok-direct-client.js +196 -0
- package/bundle/dist/utils/providers/grok/grok-dispatcher.js +1313 -0
- package/bundle/dist/utils/providers/grok/grok-headers.js +118 -0
- package/bundle/dist/utils/providers/grok/grok-media-client.js +420 -0
- package/bundle/dist/utils/providers/grok/grok-models.js +114 -0
- package/bundle/dist/utils/providers/grok/grok-oauth.js +455 -0
- package/bundle/dist/utils/providers/grok/grok-quota.js +119 -0
- package/bundle/dist/utils/providers/grok/grok-refresh.js +192 -0
- package/bundle/dist/utils/providers/grok/grok-types.js +186 -0
- package/bundle/dist/utils/providers/grok/grok-voice-client.js +199 -0
- package/bundle/dist/utils/providers/grok/index.js +11 -0
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +111 -83
- package/bundle/dist/utils/providers/openai/openai-callback-relay.js +3 -2
- package/bundle/dist/utils/providers/openai/openai-constants.js +1 -1
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +23 -7
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +143 -106
- package/bundle/dist/utils/providers/openai/openai-image-generation.js +48 -33
- package/bundle/dist/utils/providers/openai/openai-oauth.js +28 -13
- package/bundle/dist/utils/providers/openai/openai-quota.js +38 -27
- package/bundle/dist/utils/providers/openai/openai-refresh.js +17 -9
- package/bundle/dist/utils/providers/openai/openai-sse.js +1 -1
- package/bundle/dist/utils/providers/openai/openai-transport-config.js +1 -1
- package/bundle/dist/utils/quiet-hours.js +1 -1
- package/bundle/dist/utils/slack-notify.js +14 -8
- package/bundle/dist/utils/subset-loaders.js +4 -5
- package/bundle/src/application/monit/log-buffer.ts +23 -9
- package/bundle/src/application/monit/monit.frame.ts +22 -5
- package/bundle/src/application/monit/monit.types.ts +6 -0
- package/bundle/src/application/qgrid/conv-routing.test.ts +76 -4
- package/bundle/src/application/qgrid/conv-routing.ts +35 -9
- package/bundle/src/application/qgrid/expired-token-reminder.ts +9 -1
- package/bundle/src/application/qgrid/oauth.ts +51 -15
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +9 -0
- package/bundle/src/application/qgrid/qgrid-response-format.ts +50 -25
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +19 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +84 -40
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +97 -0
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +274 -126
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +1013 -3
- package/bundle/src/application/qgrid/qgrid.frame.ts +1415 -60
- package/bundle/src/application/qgrid/qgrid.types.ts +335 -0
- package/bundle/src/application/qgrid/schema-prompt.ts +105 -41
- package/bundle/src/application/qgrid/slack-user-map.ts +5 -0
- package/bundle/src/application/qgrid/token-death.ts +4 -0
- package/bundle/src/application/qgrid/token-subscriber.test.ts +21 -19
- package/bundle/src/application/qgrid/token-subscriber.ts +186 -53
- package/bundle/src/application/qgrid/token-trigger-setup.ts +2 -0
- package/bundle/src/application/qgrid/token-window-keepalive.ts +49 -6
- package/bundle/src/application/qgrid/tool-emulation-schema.ts +33 -29
- package/bundle/src/application/qgrid/tool-emulation.ts +52 -27
- package/bundle/src/application/request-log/json-schema-type-text.test.ts +5 -5
- package/bundle/src/application/request-log/json-schema-type-text.ts +81 -26
- package/bundle/src/application/request-log/request-log.model.test.ts +111 -4
- package/bundle/src/application/request-log/request-log.model.ts +277 -146
- package/bundle/src/application/request-log/request-log.types.ts +6 -0
- package/bundle/src/application/request-log/zod-code-format.ts +11 -0
- package/bundle/src/application/request-log-step/request-log-step.model.ts +1 -0
- package/bundle/src/application/request-log-step/request-log-step.types.ts +2 -0
- package/bundle/src/application/setting/setting.constant.ts +9 -0
- package/bundle/src/application/setting/setting.model.ts +12 -2
- package/bundle/src/application/setting/setting.store.ts +5 -1
- package/bundle/src/application/setting/setting.types.ts +8 -0
- package/bundle/src/application/sonamu.generated.http +133 -0
- package/bundle/src/application/sonamu.generated.sso.ts +2 -0
- package/bundle/src/application/sonamu.generated.ts +15 -0
- package/bundle/src/application/token/token.identity.test.ts +20 -0
- package/bundle/src/application/token/token.model.ts +66 -7
- package/bundle/src/application/token/token.types.ts +24 -1
- package/bundle/src/i18n/en.ts +19 -0
- package/bundle/src/i18n/ko.ts +21 -0
- package/bundle/src/i18n/sd.generated.ts +23 -2
- package/bundle/src/migrations/20260807133553_manual__backfill_request_log_cost.ts +16 -8
- package/bundle/src/server-bootstrap.ts +4 -2
- package/bundle/src/server-error-handler.ts +1 -0
- package/bundle/src/sonamu.config.ts +69 -7
- package/bundle/src/startup-migrations.ts +27 -9
- package/bundle/src/testing/global.ts +20 -0
- package/bundle/src/testing/schema-bench.ts +105 -30
- package/bundle/src/testing/test-database-name.ts +2 -0
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +8 -0
- package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +41 -6
- package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +7 -3
- package/bundle/src/utils/providers/anthropic/anthropic-thinking.ts +6 -0
- package/bundle/src/utils/providers/anthropic/claude-session.ts +58 -24
- package/bundle/src/utils/providers/anthropic/fence-strip.ts +11 -0
- package/bundle/src/utils/providers/anthropic/stream-json-adapter.ts +110 -45
- package/bundle/src/utils/providers/common/credentials.ts +8 -0
- package/bundle/src/utils/providers/common/effort.ts +9 -2
- package/bundle/src/utils/providers/common/model-cost.test.ts +48 -1
- package/bundle/src/utils/providers/common/model-cost.ts +259 -158
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +98 -2
- package/bundle/src/utils/providers/common/provider-types.ts +46 -3
- package/bundle/src/utils/providers/common/schema-validation.test.ts +5 -1
- package/bundle/src/utils/providers/common/schema-validation.ts +124 -56
- package/bundle/src/utils/providers/common/smooth-weighted-round-robin.ts +4 -0
- package/bundle/src/utils/providers/common/strictifier.ts +185 -59
- package/bundle/src/utils/providers/common/thinking.ts +2 -2
- package/bundle/src/utils/providers/common/ttft.ts +4 -2
- package/bundle/src/utils/providers/grok/__fixtures__/golden-responses-1.0.60.json +164 -0
- package/bundle/src/utils/providers/grok/grok-backend-protocol.test.ts +325 -0
- package/bundle/src/utils/providers/grok/grok-backend-protocol.ts +721 -0
- package/bundle/src/utils/providers/grok/grok-constants.ts +105 -0
- package/bundle/src/utils/providers/grok/grok-direct-client.test.ts +179 -0
- package/bundle/src/utils/providers/grok/grok-direct-client.ts +313 -0
- package/bundle/src/utils/providers/grok/grok-dispatcher.test.ts +1395 -0
- package/bundle/src/utils/providers/grok/grok-dispatcher.ts +2105 -0
- package/bundle/src/utils/providers/grok/grok-headers.test.ts +149 -0
- package/bundle/src/utils/providers/grok/grok-headers.ts +199 -0
- package/bundle/src/utils/providers/grok/grok-media-client.test.ts +446 -0
- package/bundle/src/utils/providers/grok/grok-media-client.ts +703 -0
- package/bundle/src/utils/providers/grok/grok-models.test.ts +96 -0
- package/bundle/src/utils/providers/grok/grok-models.ts +161 -0
- package/bundle/src/utils/providers/grok/grok-oauth.test.ts +406 -0
- package/bundle/src/utils/providers/grok/grok-oauth.ts +771 -0
- package/bundle/src/utils/providers/grok/grok-quota.test.ts +142 -0
- package/bundle/src/utils/providers/grok/grok-quota.ts +198 -0
- package/bundle/src/utils/providers/grok/grok-refresh.test.ts +239 -0
- package/bundle/src/utils/providers/grok/grok-refresh.ts +325 -0
- package/bundle/src/utils/providers/grok/grok-types.test.ts +252 -0
- package/bundle/src/utils/providers/grok/grok-types.ts +374 -0
- package/bundle/src/utils/providers/grok/grok-voice-client.test.ts +321 -0
- package/bundle/src/utils/providers/grok/grok-voice-client.ts +284 -0
- package/bundle/src/utils/providers/grok/index.ts +20 -0
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +221 -137
- package/bundle/src/utils/providers/openai/openai-callback-relay.ts +12 -1
- package/bundle/src/utils/providers/openai/openai-constants.ts +2 -0
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +111 -21
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +322 -169
- package/bundle/src/utils/providers/openai/openai-image-generation.ts +116 -36
- package/bundle/src/utils/providers/openai/openai-oauth.ts +48 -20
- package/bundle/src/utils/providers/openai/openai-quota.ts +77 -46
- package/bundle/src/utils/providers/openai/openai-refresh.ts +42 -18
- package/bundle/src/utils/providers/openai/openai-sse.ts +27 -3
- package/bundle/src/utils/providers/openai/openai-transport-config.ts +2 -0
- package/bundle/src/utils/quiet-hours.ts +12 -1
- package/bundle/src/utils/slack-notify.ts +44 -13
- package/bundle/src/utils/subset-loaders.ts +7 -7
- package/bundle/web-dist/client/assets/clsx-5XM_pHvN.js +1 -0
- package/bundle/web-dist/client/assets/index-Be2qCZmI.css +1 -0
- package/bundle/web-dist/client/assets/index-C96h3feE.js +32 -0
- package/bundle/web-dist/client/assets/logs-DBM6U0QD.js +1 -0
- package/bundle/web-dist/client/assets/monit-BBjXa7vB.js +1 -0
- package/bundle/web-dist/client/assets/routes-Crz_PQzi.js +5 -0
- package/bundle/web-dist/client/assets/{services.generated-NP-ValHd.js → services.generated-B5lc3aIC.js} +5 -5
- package/bundle/web-dist/client/assets/settings-Cdwm3OBB.js +1 -0
- package/bundle/web-dist/client/assets/show-DBUhALT1.js +2 -0
- package/bundle/web-dist/client/assets/tokens-CVQAVJTo.js +1 -0
- package/bundle/web-dist/client/assets/total-cost-MnfKU8IN.js +1 -0
- package/bundle/web-dist/client/assets/use-oauth-login-flow-BfxYI4Oh.js +1 -0
- package/bundle/web-dist/client/assets/use-update-token-mutation-qKBC2sn7.js +1 -0
- package/bundle/web-dist/client/assets/{vendor-tanstack-DJRQAbrY.js → vendor-tanstack-DRPs09ai.js} +1 -1
- package/bundle/web-dist/client/index.html +5 -5
- package/bundle/web-dist/server/assets/clsx-D_N5PN_2.js +16 -0
- package/bundle/web-dist/server/assets/{lazyRouteComponent-CgBdabgd.js → lazyRouteComponent-AKiHfZWp.js} +129 -2
- package/bundle/web-dist/server/assets/{logs-BMyR98rU.js → logs-B7QmGJq3.js} +18 -14
- package/bundle/web-dist/server/assets/{monit-ZPt66W5M.js → monit-DFt7x3bh.js} +13 -12
- package/bundle/web-dist/server/assets/{plus-BZ9I0VgC.js → plus-DaYQRpBt.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-Ds75e4Jl.js → react-dom-Cv23mm1K.js} +2 -129
- package/bundle/web-dist/server/assets/{refresh-cw-CztCFis-.js → refresh-cw-BpXNsAy-.js} +1 -1
- package/bundle/web-dist/server/assets/{request-model-DjGldb-h.js → request-model-eHE6YbaF.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-Ct-k7nBA.js → routes-_Y-PJfgO.js} +110 -39
- package/bundle/web-dist/server/assets/{services.generated-BA4AcAf5.js → services.generated-CMJ8AHbO.js} +582 -172
- package/bundle/web-dist/server/assets/{settings-CBUZ47Xx.js → settings-JwJKxn2e.js} +14 -14
- package/bundle/web-dist/server/assets/{show-LSFIzvO2.js → show-BEv6z1v_.js} +90 -60
- package/bundle/web-dist/server/assets/{tokens-DxrBW512.js → tokens-CNT4mI-f.js} +162 -50
- package/bundle/web-dist/server/assets/total-cost-fTE9Xy82.js +18 -0
- package/bundle/web-dist/server/assets/use-oauth-login-flow-jkAkT3X6.js +2345 -0
- package/bundle/web-dist/server/assets/{use-update-token-mutation-pkArIzCP.js → use-update-token-mutation-Sm6kh02m.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +1644 -21655
- package/dist/cli.js +9 -9
- package/package.json +4 -11
- package/postinstall.mjs +12 -0
- package/skills/qgrid/SKILL.md +1 -0
- package/skills/qgrid/references/cli-env-and-server-boot.md +4 -1
- package/skills/qgrid/references/decision-rationale.md +23 -0
- package/skills/qgrid/references/grok-runtime.md +89 -0
- package/skills/qgrid/references/provider-runtime-differences.md +30 -29
- package/skills/qgrid/references/repo-map.md +10 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +56 -3
- package/bundle/dist/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +0 -406
- package/bundle/dist/node_modules/.pnpm/@vitest_expect@4.1.10/node_modules/@vitest/expect/dist/index.js +0 -1507
- package/bundle/dist/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.js +0 -887
- package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/chunk-artifact.js +0 -2756
- package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/index.js +0 -8
- package/bundle/dist/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/utils.js +0 -8
- package/bundle/dist/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.js +0 -922
- package/bundle/dist/node_modules/.pnpm/@vitest_spy@4.1.10/node_modules/@vitest/spy/dist/index.js +0 -386
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.js +0 -121
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/constants.js +0 -51
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.js +0 -1317
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.js +0 -561
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/error.js +0 -40
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/helpers.js +0 -272
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/offset.js +0 -27
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/serialize.js +0 -77
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/source-map.js +0 -381
- package/bundle/dist/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/timers.js +0 -51
- package/bundle/dist/node_modules/.pnpm/chai@6.2.2/node_modules/chai/index.js +0 -2986
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.js +0 -10
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.js +0 -109
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.js +0 -10
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.js +0 -10
- package/bundle/dist/node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.js +0 -10
- package/bundle/dist/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.js +0 -1126
- package/bundle/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/index.js +0 -21
- package/bundle/dist/node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.js +0 -432
- package/bundle/dist/node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.js +0 -517
- package/bundle/dist/node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.js +0 -101
- package/bundle/dist/node_modules/.pnpm/vite@8.0.3_@emnapi_core@1.11.2_@emnapi_runtime@1.11.2_@types_node@25.0.7_esbuild@0.27.4_5b35fc1fb85d2af4d105e0d74f940f50/node_modules/vite/dist/node/module-runner.js +0 -1265
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js +0 -9
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/benchmark.CX_oY03V.js +0 -41
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/evaluatedModules.Dg1zASAC.js +0 -19
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/index.Chj8NDwU.js +0 -202
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/index.DdgEv5B1.js +0 -46
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js +0 -119
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js +0 -3044
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js +0 -59
- package/bundle/dist/node_modules/.pnpm/vitest@4.1.10_@opentelemetry_api@1.9.0_@types_node@25.0.7_@vitest_coverage-v8@4.1.10_vi_a1977b93af7bb69308bcb1dbab39b618/node_modules/vitest/dist/index.js +0 -28
- package/bundle/dist/testing/setup-mocks.js +0 -16
- package/bundle/src/testing/setup-mocks.ts +0 -44
- package/bundle/web-dist/client/assets/index-Bw2s88Ot.css +0 -1
- package/bundle/web-dist/client/assets/index-DZwzUfI-.js +0 -106
- package/bundle/web-dist/client/assets/logs-Cog79DU5.js +0 -1
- package/bundle/web-dist/client/assets/monit-2auznBys.js +0 -1
- package/bundle/web-dist/client/assets/routes-B0lTeLK-.js +0 -5
- package/bundle/web-dist/client/assets/settings-D8ZxZGSJ.js +0 -1
- package/bundle/web-dist/client/assets/show-CFSERlnM.js +0 -2
- package/bundle/web-dist/client/assets/tokens-CZXUyI7O.js +0 -1
- package/bundle/web-dist/client/assets/use-oauth-login-flow-hboOs3wG.js +0 -1
- package/bundle/web-dist/client/assets/use-update-token-mutation-BQHS0byG.js +0 -1
- package/bundle/web-dist/server/assets/use-oauth-login-flow-Gn8flMK5.js +0 -178
- package/dist/cli.d.ts +0 -10
- /package/bundle/web-dist/client/assets/{cost-DH2jZbrw.js → cost-DB3HbU7T.js} +0 -0
- /package/bundle/web-dist/client/assets/{plus-B62CuqT8.js → plus-BsQdY7PS.js} +0 -0
- /package/bundle/web-dist/client/assets/{preload-helper-ca-nBW7U.js → preload-helper-DWTEM3RW.js} +0 -0
- /package/bundle/web-dist/client/assets/{refresh-cw-NNcU0Cmq.js → refresh-cw-D9IUmT23.js} +0 -0
- /package/bundle/web-dist/client/assets/{request-model-CojtinqK.js → request-model-Bpp_rkK8.js} +0 -0
- /package/bundle/web-dist/server/assets/{cost-eEHeTRT5.js → cost-BC9yzyA9.js} +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { GrokModelCatalog } from "./grok-models";
|
|
4
|
+
|
|
5
|
+
const credentials = {
|
|
6
|
+
accessToken: "header.payload.sig",
|
|
7
|
+
userId: "19bfa1b2-a163-9e0c-8de1-832a73e2695e",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("GrokModelCatalog", () => {
|
|
11
|
+
it("resolves seeded models with or without the grok/ prefix", () => {
|
|
12
|
+
const catalog = new GrokModelCatalog();
|
|
13
|
+
expect(catalog.resolve("grok/grok-4.7")).toEqual({
|
|
14
|
+
id: "grok-4.7",
|
|
15
|
+
contextWindow: 256000,
|
|
16
|
+
efforts: ["xhigh", "high", "medium", "low"],
|
|
17
|
+
defaultEffort: "high",
|
|
18
|
+
known: true,
|
|
19
|
+
});
|
|
20
|
+
expect(catalog.resolve("grok-4.5").id).toBe("grok-4.5");
|
|
21
|
+
expect(catalog.resolve(undefined).id).toBe("grok-4.7");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("forwards an unlisted id with every effort instead of rejecting it", () => {
|
|
25
|
+
const catalog = new GrokModelCatalog();
|
|
26
|
+
expect(catalog.resolve("grok/grok-composer-2.5")).toEqual({
|
|
27
|
+
id: "grok-composer-2.5",
|
|
28
|
+
contextWindow: undefined,
|
|
29
|
+
efforts: ["xhigh", "high", "medium", "low"],
|
|
30
|
+
defaultEffort: "high",
|
|
31
|
+
known: false,
|
|
32
|
+
});
|
|
33
|
+
expect(catalog.validateEffort("grok-composer-2.5", "low")).toBe("low");
|
|
34
|
+
expect(catalog.validateEffort("grok-composer-2.5", "max")).toBeUndefined();
|
|
35
|
+
expect(catalog.models.has("grok-composer-2.5")).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("validates efforts per model", () => {
|
|
39
|
+
const catalog = new GrokModelCatalog();
|
|
40
|
+
expect(catalog.validateEffort("grok-4.5", "xhigh")).toBe("xhigh");
|
|
41
|
+
expect(catalog.validateEffort("grok-4.5", "max")).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("merges GET /models and stores the etag", async () => {
|
|
45
|
+
const catalog = new GrokModelCatalog();
|
|
46
|
+
let url = "";
|
|
47
|
+
const result = await catalog.refresh(credentials, {
|
|
48
|
+
fetch: async (input) => {
|
|
49
|
+
url = String(input);
|
|
50
|
+
return new Response(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
object: "list",
|
|
53
|
+
data: [
|
|
54
|
+
{
|
|
55
|
+
id: "grok-4.8",
|
|
56
|
+
api_backend: "responses",
|
|
57
|
+
context_window: 500000,
|
|
58
|
+
reasoning_efforts: [
|
|
59
|
+
{ id: "h", value: "high" },
|
|
60
|
+
{ id: "m", value: "medium", default: true },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{ id: "legacy", api_backend: "chat_completions", context_window: 1000 },
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
{ headers: { "x-models-etag": '"42"' } },
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
expect(url).toBe("https://cli-chat-proxy.grok.com/v1/models");
|
|
71
|
+
expect(result).toEqual({ kind: "ok", models: ["grok-4.8"], etag: '"42"' });
|
|
72
|
+
expect(catalog.resolve("grok/grok-4.8")).toEqual({
|
|
73
|
+
id: "grok-4.8",
|
|
74
|
+
contextWindow: 500000,
|
|
75
|
+
efforts: ["high", "medium"],
|
|
76
|
+
defaultEffort: "medium",
|
|
77
|
+
known: true,
|
|
78
|
+
});
|
|
79
|
+
expect(catalog.validateEffort("grok-4.8", "xhigh")).toBeUndefined();
|
|
80
|
+
// A non-responses backend is not merged; the id is forwarded unlisted.
|
|
81
|
+
expect(catalog.resolve("legacy").known).toBe(false);
|
|
82
|
+
// Seeds absent from the live list remain.
|
|
83
|
+
expect(catalog.resolve("grok-4.6").id).toBe("grok-4.6");
|
|
84
|
+
expect(catalog.isStale('"42"')).toBe(false);
|
|
85
|
+
expect(catalog.isStale('"43"')).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reports lookup_failed without changing the catalog", async () => {
|
|
89
|
+
const catalog = new GrokModelCatalog();
|
|
90
|
+
const result = await catalog.refresh(credentials, {
|
|
91
|
+
fetch: async () => new Response("{}", { status: 500 }),
|
|
92
|
+
});
|
|
93
|
+
expect(result.kind).toBe("lookup_failed");
|
|
94
|
+
expect(catalog.models.size).toBe(4);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GROK_DEFAULT_MODEL,
|
|
3
|
+
GROK_REASONING_EFFORTS,
|
|
4
|
+
GROK_SEED_MODELS,
|
|
5
|
+
type GrokReasoningEffort,
|
|
6
|
+
} from "./grok-constants";
|
|
7
|
+
import { grokProxyGet } from "./grok-direct-client";
|
|
8
|
+
import { buildGrokProxyGetHeaders } from "./grok-headers";
|
|
9
|
+
import { GrokModelsResponse, type GrokCredentials, type GrokProxyModel } from "./grok-types";
|
|
10
|
+
|
|
11
|
+
export interface GrokResolvedModel {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Undefined for an unlisted id; the response `x-grok-context-window` header may fill it. */
|
|
14
|
+
contextWindow: number | undefined;
|
|
15
|
+
efforts: readonly GrokReasoningEffort[];
|
|
16
|
+
defaultEffort: GrokReasoningEffort;
|
|
17
|
+
/** False when the id is in neither the seed nor the live catalog and is forwarded as-is. */
|
|
18
|
+
known: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isGrokEffort(value: string): value is GrokReasoningEffort {
|
|
22
|
+
return GROK_REASONING_EFFORTS.some((effort) => effort === value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Strips the qgrid route prefix; an empty id means the default model. */
|
|
26
|
+
export function canonicalGrokModelId(modelId: string | undefined): string {
|
|
27
|
+
const stripped = (modelId ?? "").startsWith("grok/") ? (modelId ?? "").slice(5) : (modelId ?? "");
|
|
28
|
+
|
|
29
|
+
return stripped || GROK_DEFAULT_MODEL;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fromProxyModel(model: GrokProxyModel): GrokResolvedModel | undefined {
|
|
33
|
+
if (model.api_backend !== "responses") return undefined;
|
|
34
|
+
const listed = (model.reasoning_efforts ?? []).map((entry) => entry.value).filter(isGrokEffort);
|
|
35
|
+
const efforts: readonly GrokReasoningEffort[] = listed.length ? listed : GROK_REASONING_EFFORTS;
|
|
36
|
+
const flagged = model.reasoning_efforts?.find((entry) => entry.default)?.value;
|
|
37
|
+
const candidates = [flagged, model.reasoning_effort ?? undefined, "high"];
|
|
38
|
+
|
|
39
|
+
const defaultEffort =
|
|
40
|
+
candidates.find(
|
|
41
|
+
(value): value is GrokReasoningEffort =>
|
|
42
|
+
value !== undefined && isGrokEffort(value) && efforts.includes(value),
|
|
43
|
+
) ?? efforts[0]!;
|
|
44
|
+
|
|
45
|
+
return { id: model.id, contextWindow: model.context_window, efforts, defaultEffort, known: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type GrokModelRefreshResult =
|
|
49
|
+
| { kind: "ok"; models: string[]; etag: string | undefined }
|
|
50
|
+
| { kind: "lookup_failed"; reason: string };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Model catalog seeded from the U0 capture and merged with live `GET {proxy}/models`. Like the
|
|
54
|
+
* OpenAI runtime, it never rejects an id: an unlisted id is forwarded and the upstream decides.
|
|
55
|
+
*/
|
|
56
|
+
export class GrokModelCatalog {
|
|
57
|
+
readonly models = new Map<string, GrokResolvedModel>();
|
|
58
|
+
etag: string | undefined;
|
|
59
|
+
refreshedAt: number | undefined;
|
|
60
|
+
pendingRefresh: Promise<GrokModelRefreshResult> | undefined;
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
for (const seed of GROK_SEED_MODELS) {
|
|
64
|
+
this.models.set(seed.id, {
|
|
65
|
+
id: seed.id,
|
|
66
|
+
contextWindow: seed.contextWindow,
|
|
67
|
+
efforts: seed.reasoningEfforts,
|
|
68
|
+
defaultEffort: seed.defaultEffort,
|
|
69
|
+
known: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when a response advertised a catalog etag different from the one last merged. */
|
|
75
|
+
isStale(observedEtag: string | undefined): boolean {
|
|
76
|
+
return observedEtag !== undefined && observedEtag !== this.etag;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Single-flight refresh. Live entries replace seeds; seeds absent from the live list stay. */
|
|
80
|
+
async refresh(
|
|
81
|
+
credentials: Pick<GrokCredentials, "accessToken" | "userId">,
|
|
82
|
+
options: { fetch?: typeof fetch; signal?: AbortSignal; baseUrl?: string; now?: number } = {},
|
|
83
|
+
): Promise<GrokModelRefreshResult> {
|
|
84
|
+
this.pendingRefresh ??= this.refreshNow(credentials, options).finally(() => {
|
|
85
|
+
this.pendingRefresh = undefined;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return this.pendingRefresh;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async refreshNow(
|
|
92
|
+
credentials: Pick<GrokCredentials, "accessToken" | "userId">,
|
|
93
|
+
options: { fetch?: typeof fetch; signal?: AbortSignal; baseUrl?: string; now?: number },
|
|
94
|
+
): Promise<GrokModelRefreshResult> {
|
|
95
|
+
try {
|
|
96
|
+
const getOptions: Parameters<typeof grokProxyGet>[1] = {
|
|
97
|
+
headers: buildGrokProxyGetHeaders({
|
|
98
|
+
bearer: credentials.accessToken,
|
|
99
|
+
userId: credentials.userId,
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (options.fetch) getOptions.fetch = options.fetch;
|
|
104
|
+
|
|
105
|
+
if (options.signal) getOptions.signal = options.signal;
|
|
106
|
+
|
|
107
|
+
if (options.baseUrl) getOptions.baseUrl = options.baseUrl;
|
|
108
|
+
const { json, headers } = await grokProxyGet("/models", getOptions);
|
|
109
|
+
|
|
110
|
+
const parsed = GrokModelsResponse.safeParse(json);
|
|
111
|
+
|
|
112
|
+
if (!parsed.success) {
|
|
113
|
+
return {
|
|
114
|
+
kind: "lookup_failed",
|
|
115
|
+
reason: `unexpected /models shape: ${parsed.error.message}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const merged: string[] = [];
|
|
120
|
+
|
|
121
|
+
for (const entry of parsed.data.data) {
|
|
122
|
+
const model = fromProxyModel(entry);
|
|
123
|
+
|
|
124
|
+
if (!model) continue;
|
|
125
|
+
this.models.set(model.id, model);
|
|
126
|
+
merged.push(model.id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.etag = headers.modelsEtag;
|
|
130
|
+
this.refreshedAt = options.now ?? Date.now();
|
|
131
|
+
|
|
132
|
+
return { kind: "ok", models: merged, etag: this.etag };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
kind: "lookup_failed",
|
|
136
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resolve(modelId: string | undefined): GrokResolvedModel {
|
|
142
|
+
const id = canonicalGrokModelId(modelId);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
this.models.get(id) ?? {
|
|
146
|
+
id,
|
|
147
|
+
contextWindow: undefined,
|
|
148
|
+
efforts: GROK_REASONING_EFFORTS,
|
|
149
|
+
defaultEffort: "high",
|
|
150
|
+
known: false,
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The effort if the model accepts it, otherwise undefined. Unlisted ids accept every effort. */
|
|
156
|
+
validateEffort(modelId: string | undefined, effort: string): GrokReasoningEffort | undefined {
|
|
157
|
+
const model = this.resolve(modelId);
|
|
158
|
+
|
|
159
|
+
return isGrokEffort(effort) && model.efforts.includes(effort) ? effort : undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { GROK_BUILD_VERSION, GROK_OAUTH_CLIENT_ID, GROK_OAUTH_SCOPES } from "./grok-constants";
|
|
4
|
+
import {
|
|
5
|
+
GROK_FALLBACK_AUTHORIZATION_ENDPOINT,
|
|
6
|
+
GROK_FALLBACK_TOKEN_ENDPOINT,
|
|
7
|
+
GrokOAuthError,
|
|
8
|
+
buildGrokAuthorizeUrl,
|
|
9
|
+
completeGrokOAuth,
|
|
10
|
+
getGrokOAuthEndpoints,
|
|
11
|
+
parseGrokCallbackInput,
|
|
12
|
+
pollGrokDeviceCode,
|
|
13
|
+
resetGrokDiscoveryCache,
|
|
14
|
+
startGrokDeviceCode,
|
|
15
|
+
startGrokOAuth,
|
|
16
|
+
} from "./grok-oauth";
|
|
17
|
+
|
|
18
|
+
const USER_ID = "11111111-2222-3333-4444-555555555555";
|
|
19
|
+
|
|
20
|
+
function segment(value: unknown): string {
|
|
21
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function accessToken(
|
|
25
|
+
payload: Record<string, unknown> = {},
|
|
26
|
+
header: Record<string, unknown> = { typ: "at+jwt", alg: "ES256" },
|
|
27
|
+
): string {
|
|
28
|
+
return `${segment(header)}.${segment({ iss: "https://auth.x.ai", sub: USER_ID, ...payload })}.c2ln`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Call {
|
|
32
|
+
url: string;
|
|
33
|
+
method: string;
|
|
34
|
+
headers: Record<string, string>;
|
|
35
|
+
body: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Responder = (call: Call) => Response | Promise<Response>;
|
|
39
|
+
|
|
40
|
+
function fakeFetch(routes: Record<string, Responder>): { fetch: typeof fetch; calls: Call[] } {
|
|
41
|
+
const calls: Call[] = [];
|
|
42
|
+
const impl = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
43
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
44
|
+
const headers = Object.fromEntries(new Headers(init?.headers).entries());
|
|
45
|
+
const body =
|
|
46
|
+
init?.body instanceof URLSearchParams ? init.body.toString() : String(init?.body ?? "");
|
|
47
|
+
const call = { url, method: init?.method ?? "GET", headers, body };
|
|
48
|
+
calls.push(call);
|
|
49
|
+
const key = Object.keys(routes).find((route) => url.startsWith(route));
|
|
50
|
+
if (!key) throw new TypeError(`unexpected fetch ${url}`);
|
|
51
|
+
const responder = routes[key];
|
|
52
|
+
if (!responder) throw new TypeError(`unexpected fetch ${url}`);
|
|
53
|
+
return responder(call);
|
|
54
|
+
};
|
|
55
|
+
return { fetch: impl, calls };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const json = (status: number, body: unknown): Response =>
|
|
59
|
+
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
60
|
+
|
|
61
|
+
const DISCOVERY = "https://auth.x.ai/.well-known/openid-configuration";
|
|
62
|
+
const USER = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
|
|
63
|
+
|
|
64
|
+
const discoveryOk = (): Response =>
|
|
65
|
+
json(200, {
|
|
66
|
+
authorization_endpoint: "https://auth.x.ai/oauth2/auth",
|
|
67
|
+
token_endpoint: "https://auth.x.ai/oauth2/token",
|
|
68
|
+
});
|
|
69
|
+
const userOk = (): Response =>
|
|
70
|
+
json(200, { userId: USER_ID, subscriptionTier: "SuperGrokPlus", hasGrokCodeAccess: true });
|
|
71
|
+
const tokensOk = (overrides: Record<string, unknown> = {}): Response =>
|
|
72
|
+
json(200, {
|
|
73
|
+
access_token: accessToken(),
|
|
74
|
+
refresh_token: "rt-1",
|
|
75
|
+
expires_in: 21_600,
|
|
76
|
+
token_type: "Bearer",
|
|
77
|
+
...overrides,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
beforeEach(() => resetGrokDiscoveryCache());
|
|
81
|
+
afterEach(() => resetGrokDiscoveryCache());
|
|
82
|
+
|
|
83
|
+
describe("authorize URL", () => {
|
|
84
|
+
it("emits grok-build's exact query", () => {
|
|
85
|
+
const url = buildGrokAuthorizeUrl({
|
|
86
|
+
authorizationEndpoint: "https://auth.x.ai/oauth2/auth",
|
|
87
|
+
redirectUri: "http://127.0.0.1:54321/callback",
|
|
88
|
+
codeChallenge: "challenge-abc",
|
|
89
|
+
state: "state-1",
|
|
90
|
+
nonce: "nonce-1",
|
|
91
|
+
});
|
|
92
|
+
expect(url).toBe(
|
|
93
|
+
"https://auth.x.ai/oauth2/auth?response_type=code" +
|
|
94
|
+
`&client_id=${GROK_OAUTH_CLIENT_ID}` +
|
|
95
|
+
"&redirect_uri=http%3A%2F%2F127.0.0.1%3A54321%2Fcallback" +
|
|
96
|
+
"&scope=openid%20profile%20email%20offline_access%20grok-cli%3Aaccess%20api%3Aaccess" +
|
|
97
|
+
"%20conversations%3Aread%20conversations%3Awrite%20workspaces%3Aread%20workspaces%3Awrite" +
|
|
98
|
+
"&code_challenge=challenge-abc&code_challenge_method=S256&state=state-1&nonce=nonce-1" +
|
|
99
|
+
"&referrer=grok-build",
|
|
100
|
+
);
|
|
101
|
+
expect(new URL(url).searchParams.get("scope")).toBe(GROK_OAUTH_SCOPES.join(" "));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("startGrokOAuth binds a loopback redirect, state, and an S256 challenge of the verifier", async () => {
|
|
105
|
+
const { fetch } = fakeFetch({ [DISCOVERY]: discoveryOk });
|
|
106
|
+
const started = await startGrokOAuth({ state: "s-xyz", redirectPort: 40_123, fetch });
|
|
107
|
+
const url = new URL(started.authorizeUrl);
|
|
108
|
+
expect(started.redirectUri).toBe("http://127.0.0.1:40123/callback");
|
|
109
|
+
expect(url.origin + url.pathname).toBe("https://auth.x.ai/oauth2/auth");
|
|
110
|
+
expect(url.searchParams.get("redirect_uri")).toBe(started.redirectUri);
|
|
111
|
+
expect(url.searchParams.get("state")).toBe("s-xyz");
|
|
112
|
+
expect(url.searchParams.get("code_challenge_method")).toBe("S256");
|
|
113
|
+
expect(Buffer.from(started.verifier, "base64url")).toHaveLength(32);
|
|
114
|
+
const { createHash } = await import("node:crypto");
|
|
115
|
+
expect(url.searchParams.get("code_challenge")).toBe(
|
|
116
|
+
createHash("sha256").update(started.verifier).digest("base64url"),
|
|
117
|
+
);
|
|
118
|
+
expect(url.searchParams.get("nonce")).toMatch(/^[0-9a-f-]{36}$/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("discovery", () => {
|
|
123
|
+
it("uses the fallback endpoints when the well-known fetch fails, without caching them", async () => {
|
|
124
|
+
let attempts = 0;
|
|
125
|
+
const { fetch } = fakeFetch({
|
|
126
|
+
[DISCOVERY]: () => {
|
|
127
|
+
attempts += 1;
|
|
128
|
+
throw new TypeError("fetch failed");
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const endpoints = await getGrokOAuthEndpoints({ fetch });
|
|
132
|
+
expect(endpoints).toEqual({
|
|
133
|
+
authorizationEndpoint: GROK_FALLBACK_AUTHORIZATION_ENDPOINT,
|
|
134
|
+
tokenEndpoint: GROK_FALLBACK_TOKEN_ENDPOINT,
|
|
135
|
+
source: "fallback",
|
|
136
|
+
});
|
|
137
|
+
await getGrokOAuthEndpoints({ fetch });
|
|
138
|
+
expect(attempts).toBe(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("falls back on HTTP errors and on endpoints outside x.ai", async () => {
|
|
142
|
+
const http = fakeFetch({ [DISCOVERY]: () => json(503, {}) });
|
|
143
|
+
expect((await getGrokOAuthEndpoints({ fetch: http.fetch })).source).toBe("fallback");
|
|
144
|
+
const foreign = fakeFetch({
|
|
145
|
+
[DISCOVERY]: () =>
|
|
146
|
+
json(200, {
|
|
147
|
+
authorization_endpoint: "https://auth.x.ai/oauth2/auth",
|
|
148
|
+
token_endpoint: "https://evil.example/token",
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
expect((await getGrokOAuthEndpoints({ fetch: foreign.fetch })).source).toBe("fallback");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("caches a discovered document for 24 h", async () => {
|
|
155
|
+
let now = 1_000;
|
|
156
|
+
const { fetch, calls } = fakeFetch({ [DISCOVERY]: discoveryOk });
|
|
157
|
+
await getGrokOAuthEndpoints({ fetch, now: () => now });
|
|
158
|
+
now += 24 * 60 * 60 * 1000 - 1;
|
|
159
|
+
expect((await getGrokOAuthEndpoints({ fetch, now: () => now })).source).toBe("discovery");
|
|
160
|
+
expect(calls).toHaveLength(1);
|
|
161
|
+
now += 1;
|
|
162
|
+
await getGrokOAuthEndpoints({ fetch, now: () => now });
|
|
163
|
+
expect(calls).toHaveLength(2);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("callback input", () => {
|
|
168
|
+
it("accepts a full callback URL or a bare code", () => {
|
|
169
|
+
expect(parseGrokCallbackInput(" http://127.0.0.1:5/callback?code=abc&state=s1 ")).toEqual({
|
|
170
|
+
code: "abc",
|
|
171
|
+
state: "s1",
|
|
172
|
+
});
|
|
173
|
+
expect(parseGrokCallbackInput("bare-code_123")).toEqual({ code: "bare-code_123", state: "" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("surfaces IdP errors and rejects empty input", () => {
|
|
177
|
+
expect(() =>
|
|
178
|
+
parseGrokCallbackInput("http://127.0.0.1:5/callback?error=access_denied&state=s"),
|
|
179
|
+
).toThrow(expect.objectContaining({ code: "authorization_failed", oauthError: "access_denied" }));
|
|
180
|
+
expect(() => parseGrokCallbackInput(" ")).toThrow(GrokOAuthError);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("completeGrokOAuth", () => {
|
|
185
|
+
const base = {
|
|
186
|
+
verifier: "verifier-1",
|
|
187
|
+
redirectUri: "http://127.0.0.1:40123/callback",
|
|
188
|
+
state: "s1",
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
it("exchanges a pasted callback URL and resolves the account through /user", async () => {
|
|
192
|
+
const { fetch, calls } = fakeFetch({
|
|
193
|
+
[DISCOVERY]: discoveryOk,
|
|
194
|
+
"https://auth.x.ai/oauth2/token": () => tokensOk({ scope: "openid grok-cli:access" }),
|
|
195
|
+
[USER]: userOk,
|
|
196
|
+
});
|
|
197
|
+
const credentials = await completeGrokOAuth({
|
|
198
|
+
...base,
|
|
199
|
+
codeOrUrl: "http://127.0.0.1:40123/callback?code=the-code&state=s1",
|
|
200
|
+
fetch,
|
|
201
|
+
now: () => Date.parse("2026-10-16T00:00:00Z"),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const exchange = calls.find((call) => call.url === "https://auth.x.ai/oauth2/token");
|
|
205
|
+
expect(exchange?.method).toBe("POST");
|
|
206
|
+
expect(exchange?.headers["content-type"]).toBe("application/x-www-form-urlencoded");
|
|
207
|
+
expect(exchange?.headers["x-grok-client-version"]).toBe(GROK_BUILD_VERSION);
|
|
208
|
+
expect(exchange?.headers["user-agent"]).toMatch(/^grok-shell\/1\.0\.60 \(/);
|
|
209
|
+
expect(Object.fromEntries(new URLSearchParams(exchange?.body))).toEqual({
|
|
210
|
+
grant_type: "authorization_code",
|
|
211
|
+
code: "the-code",
|
|
212
|
+
redirect_uri: base.redirectUri,
|
|
213
|
+
client_id: GROK_OAUTH_CLIENT_ID,
|
|
214
|
+
code_verifier: "verifier-1",
|
|
215
|
+
});
|
|
216
|
+
const user = calls.find((call) => call.url === USER);
|
|
217
|
+
expect(user?.headers["x-userid"]).toBe(USER_ID);
|
|
218
|
+
expect(user?.headers["x-xai-token-auth"]).toBe("xai-grok-cli");
|
|
219
|
+
|
|
220
|
+
expect(credentials).toMatchObject({
|
|
221
|
+
authMode: "oauth",
|
|
222
|
+
refreshToken: "rt-1",
|
|
223
|
+
expiresAt: "2026-10-16T06:00:00.000Z",
|
|
224
|
+
issuer: "https://auth.x.ai",
|
|
225
|
+
clientId: GROK_OAUTH_CLIENT_ID,
|
|
226
|
+
scopes: ["openid", "grok-cli:access"],
|
|
227
|
+
userId: USER_ID,
|
|
228
|
+
subscriptionTier: "SuperGrokPlus",
|
|
229
|
+
});
|
|
230
|
+
expect(credentials.sessionId).not.toBe(credentials.agentId);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("accepts a bare code and defaults scopes to the requested set", async () => {
|
|
234
|
+
const { fetch, calls } = fakeFetch({
|
|
235
|
+
[DISCOVERY]: discoveryOk,
|
|
236
|
+
"https://auth.x.ai/oauth2/token": () => tokensOk(),
|
|
237
|
+
[USER]: userOk,
|
|
238
|
+
});
|
|
239
|
+
const credentials = await completeGrokOAuth({ ...base, codeOrUrl: "bare-code", fetch });
|
|
240
|
+
expect(new URLSearchParams(calls[1]?.body).get("code")).toBe("bare-code");
|
|
241
|
+
expect(credentials.scopes).toEqual([...GROK_OAUTH_SCOPES]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("rejects a state mismatch before any token request", async () => {
|
|
245
|
+
const { fetch, calls } = fakeFetch({});
|
|
246
|
+
await expect(
|
|
247
|
+
completeGrokOAuth({
|
|
248
|
+
...base,
|
|
249
|
+
codeOrUrl: "http://127.0.0.1:40123/callback?code=c&state=other",
|
|
250
|
+
fetch,
|
|
251
|
+
}),
|
|
252
|
+
).rejects.toMatchObject({ code: "state_mismatch" });
|
|
253
|
+
expect(calls).toHaveLength(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("fails registration when the user id cannot be resolved", async () => {
|
|
257
|
+
const noSub = fakeFetch({
|
|
258
|
+
[DISCOVERY]: discoveryOk,
|
|
259
|
+
"https://auth.x.ai/oauth2/token": () =>
|
|
260
|
+
tokensOk({ access_token: accessToken({ sub: undefined }) }),
|
|
261
|
+
});
|
|
262
|
+
await expect(
|
|
263
|
+
completeGrokOAuth({ ...base, codeOrUrl: "c", fetch: noSub.fetch }),
|
|
264
|
+
).rejects.toMatchObject({ code: "user_id_unresolved" });
|
|
265
|
+
expect(noSub.calls.some((call) => call.url === USER)).toBe(false);
|
|
266
|
+
|
|
267
|
+
const badUser = fakeFetch({
|
|
268
|
+
[DISCOVERY]: discoveryOk,
|
|
269
|
+
"https://auth.x.ai/oauth2/token": () => tokensOk(),
|
|
270
|
+
[USER]: () => json(200, { subscriptionTier: "Super" }),
|
|
271
|
+
});
|
|
272
|
+
await expect(
|
|
273
|
+
completeGrokOAuth({ ...base, codeOrUrl: "c", fetch: badUser.fetch }),
|
|
274
|
+
).rejects.toMatchObject({ code: "user_id_unresolved" });
|
|
275
|
+
|
|
276
|
+
const deniedUser = fakeFetch({
|
|
277
|
+
[DISCOVERY]: discoveryOk,
|
|
278
|
+
"https://auth.x.ai/oauth2/token": () => tokensOk(),
|
|
279
|
+
[USER]: () => json(401, {}),
|
|
280
|
+
});
|
|
281
|
+
await expect(
|
|
282
|
+
completeGrokOAuth({ ...base, codeOrUrl: "c", fetch: deniedUser.fetch }),
|
|
283
|
+
).rejects.toMatchObject({ code: "user_lookup_failed", status: 401 });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("rejects tokens that are not an auth.x.ai at+jwt", async () => {
|
|
287
|
+
for (const token of [
|
|
288
|
+
"xai-api-key-123",
|
|
289
|
+
accessToken({}, { typ: "JWT", alg: "ES256" }),
|
|
290
|
+
accessToken({ iss: "https://evil.example" }),
|
|
291
|
+
]) {
|
|
292
|
+
const { fetch, calls } = fakeFetch({
|
|
293
|
+
[DISCOVERY]: discoveryOk,
|
|
294
|
+
"https://auth.x.ai/oauth2/token": () => tokensOk({ access_token: token }),
|
|
295
|
+
[USER]: userOk,
|
|
296
|
+
});
|
|
297
|
+
await expect(completeGrokOAuth({ ...base, codeOrUrl: "c", fetch })).rejects.toMatchObject({
|
|
298
|
+
code: "invalid_access_token",
|
|
299
|
+
});
|
|
300
|
+
expect(calls.some((call) => call.url === USER)).toBe(false);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("does not echo token endpoint bodies in errors", async () => {
|
|
305
|
+
const { fetch } = fakeFetch({
|
|
306
|
+
[DISCOVERY]: discoveryOk,
|
|
307
|
+
"https://auth.x.ai/oauth2/token": () =>
|
|
308
|
+
json(400, { error: "invalid_grant", error_description: "secret-echo" }),
|
|
309
|
+
});
|
|
310
|
+
const error = await completeGrokOAuth({ ...base, codeOrUrl: "c", fetch }).catch(
|
|
311
|
+
(caught: unknown) => caught,
|
|
312
|
+
);
|
|
313
|
+
expect(error).toMatchObject({ code: "token_exchange_failed", oauthError: "invalid_grant" });
|
|
314
|
+
expect(error).toBeInstanceOf(GrokOAuthError);
|
|
315
|
+
expect(String(error)).toContain("invalid_grant");
|
|
316
|
+
expect(String(error)).not.toContain("secret-echo");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("device code", () => {
|
|
321
|
+
it("starts with the frozen scope and validates the response", async () => {
|
|
322
|
+
const { fetch, calls } = fakeFetch({
|
|
323
|
+
"https://auth.x.ai/oauth2/device/code": () =>
|
|
324
|
+
json(200, {
|
|
325
|
+
device_code: "dc",
|
|
326
|
+
user_code: "ABCD-1234",
|
|
327
|
+
verification_uri: "https://accounts.x.ai/device",
|
|
328
|
+
verification_uri_complete: "https://accounts.x.ai/device?user_code=ABCD-1234",
|
|
329
|
+
expires_in: 900,
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
expect(await startGrokDeviceCode({ fetch })).toEqual({
|
|
333
|
+
deviceCode: "dc",
|
|
334
|
+
userCode: "ABCD-1234",
|
|
335
|
+
verificationUri: "https://accounts.x.ai/device",
|
|
336
|
+
verificationUriComplete: "https://accounts.x.ai/device?user_code=ABCD-1234",
|
|
337
|
+
expiresIn: 900,
|
|
338
|
+
interval: 5,
|
|
339
|
+
});
|
|
340
|
+
expect(Object.fromEntries(new URLSearchParams(calls[0]?.body))).toEqual({
|
|
341
|
+
client_id: GROK_OAUTH_CLIENT_ID,
|
|
342
|
+
scope: GROK_OAUTH_SCOPES.join(" "),
|
|
343
|
+
referrer: "grok-build",
|
|
344
|
+
});
|
|
345
|
+
expect(calls[0]?.headers["x-grok-client-version"]).toBe(GROK_BUILD_VERSION);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("rejects unsafe verification URIs", async () => {
|
|
349
|
+
const { fetch } = fakeFetch({
|
|
350
|
+
"https://auth.x.ai/oauth2/device/code": () =>
|
|
351
|
+
json(200, {
|
|
352
|
+
device_code: "dc",
|
|
353
|
+
user_code: "ABCD",
|
|
354
|
+
verification_uri: "javascript:alert(1)",
|
|
355
|
+
expires_in: 900,
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
358
|
+
await expect(startGrokDeviceCode({ fetch })).rejects.toMatchObject({
|
|
359
|
+
code: "invalid_device_code_response",
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("polls once per call and honors authorization_pending and slow_down", async () => {
|
|
364
|
+
const replies = [
|
|
365
|
+
json(400, { error: "authorization_pending" }),
|
|
366
|
+
json(400, { error: "slow_down" }),
|
|
367
|
+
tokensOk(),
|
|
368
|
+
];
|
|
369
|
+
const { fetch, calls } = fakeFetch({
|
|
370
|
+
"https://auth.x.ai/oauth2/token": () => {
|
|
371
|
+
const reply = replies.shift();
|
|
372
|
+
if (!reply) throw new TypeError("no more replies");
|
|
373
|
+
return reply;
|
|
374
|
+
},
|
|
375
|
+
[USER]: userOk,
|
|
376
|
+
});
|
|
377
|
+
expect(await pollGrokDeviceCode({ deviceCode: "dc", interval: 5, fetch })).toEqual({
|
|
378
|
+
status: "pending",
|
|
379
|
+
interval: 5,
|
|
380
|
+
});
|
|
381
|
+
expect(await pollGrokDeviceCode({ deviceCode: "dc", interval: 5, fetch })).toEqual({
|
|
382
|
+
status: "pending",
|
|
383
|
+
interval: 10,
|
|
384
|
+
});
|
|
385
|
+
const done = await pollGrokDeviceCode({ deviceCode: "dc", interval: 10, fetch });
|
|
386
|
+
expect(done.status).toBe("complete");
|
|
387
|
+
expect(done.status === "complete" ? done.credentials.userId : undefined).toBe(USER_ID);
|
|
388
|
+
expect(Object.fromEntries(new URLSearchParams(calls[0]?.body))).toEqual({
|
|
389
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
390
|
+
device_code: "dc",
|
|
391
|
+
client_id: GROK_OAUTH_CLIENT_ID,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("treats expired_token and access_denied as terminal", async () => {
|
|
396
|
+
for (const [error, code] of [
|
|
397
|
+
["expired_token", "expired_token"],
|
|
398
|
+
["access_denied", "access_denied"],
|
|
399
|
+
] as const) {
|
|
400
|
+
const { fetch } = fakeFetch({ "https://auth.x.ai/oauth2/token": () => json(400, { error }) });
|
|
401
|
+
await expect(pollGrokDeviceCode({ deviceCode: "dc", interval: 5, fetch })).rejects.toMatchObject(
|
|
402
|
+
{ code, retryable: false },
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
});
|