@cartanova/qgrid-cli 2.10.10 → 2.11.0
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-BA7uQ-6c.js +31 -0
- package/bundle/web-dist/client/assets/index-Be2qCZmI.css +1 -0
- package/bundle/web-dist/client/assets/logs-C47OJGEI.js +1 -0
- package/bundle/web-dist/client/assets/monit-BS9no0dU.js +4 -0
- package/bundle/web-dist/client/assets/plus-sdACTfH0.js +1 -0
- package/bundle/web-dist/client/assets/refresh-cw-CY8o_1cT.js +1 -0
- package/bundle/web-dist/client/assets/request-model-BRtFuZvj.js +1 -0
- package/bundle/web-dist/client/assets/routes-kqgkUxE8.js +5 -0
- package/bundle/web-dist/client/assets/{services.generated-NP-ValHd.js → services.generated-CefiDXEv.js} +5 -5
- package/bundle/web-dist/client/assets/settings-DNDvFGSw.js +1 -0
- package/bundle/web-dist/client/assets/show-u8Np69q3.js +2 -0
- package/bundle/web-dist/client/assets/tokens-QvP410NW.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-Bzdh28Ws.js +1 -0
- package/bundle/web-dist/client/assets/use-update-token-mutation-CfJKaxmR.js +1 -0
- package/bundle/web-dist/client/assets/vendor-react-CUFFrVN9.js +244 -0
- package/bundle/web-dist/client/assets/vendor-tanstack-Bf_jag9G.js +1 -0
- package/bundle/web-dist/client/index.html +6 -6
- package/bundle/web-dist/server/assets/clsx-D_N5PN_2.js +16 -0
- package/bundle/web-dist/server/assets/{lazyRouteComponent-CgBdabgd.js → lazyRouteComponent-D8iTIHyn.js} +129 -2
- package/bundle/web-dist/server/assets/logs-B3OTd9hy.js +825 -0
- package/bundle/web-dist/server/assets/{monit-ZPt66W5M.js → monit-B2hqorPe.js} +385 -126
- package/bundle/web-dist/server/assets/plus-DwG7h9kq.js +28 -0
- package/bundle/web-dist/server/assets/{react-dom-Ds75e4Jl.js → react-dom-BIYQzBqG.js} +2 -129
- package/bundle/web-dist/server/assets/refresh-cw-mMBkwOq0.js +44 -0
- package/bundle/web-dist/server/assets/{request-model-DjGldb-h.js → request-model-I6OdODSH.js} +15 -6
- package/bundle/web-dist/server/assets/{routes-Ct-k7nBA.js → routes-B95YlUv2.js} +760 -213
- package/bundle/web-dist/server/assets/{services.generated-BA4AcAf5.js → services.generated-DQATJ6qf.js} +12820 -12194
- package/bundle/web-dist/server/assets/settings-C3gbGk0m.js +830 -0
- package/bundle/web-dist/server/assets/{show-LSFIzvO2.js → show-Bxaj3rhH.js} +1350 -418
- package/bundle/web-dist/server/assets/tokens-CZjH3cB7.js +1038 -0
- package/bundle/web-dist/server/assets/total-cost-fTE9Xy82.js +18 -0
- package/bundle/web-dist/server/assets/use-oauth-login-flow-CKDHgc3U.js +2379 -0
- package/bundle/web-dist/server/assets/{use-update-token-mutation-pkArIzCP.js → use-update-token-mutation-BEbgOzaa.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +4295 -22810
- 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/plus-B62CuqT8.js +0 -1
- package/bundle/web-dist/client/assets/refresh-cw-NNcU0Cmq.js +0 -1
- package/bundle/web-dist/client/assets/request-model-CojtinqK.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/client/assets/vendor-react-DH4kUfK2.js +0 -9
- package/bundle/web-dist/client/assets/vendor-tanstack-DJRQAbrY.js +0 -1
- package/bundle/web-dist/server/assets/logs-BMyR98rU.js +0 -509
- package/bundle/web-dist/server/assets/plus-BZ9I0VgC.js +0 -19
- package/bundle/web-dist/server/assets/refresh-cw-CztCFis-.js +0 -23
- package/bundle/web-dist/server/assets/settings-CBUZ47Xx.js +0 -461
- package/bundle/web-dist/server/assets/tokens-DxrBW512.js +0 -448
- 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/{preload-helper-ca-nBW7U.js → preload-helper-DWTEM3RW.js} +0 -0
- /package/bundle/web-dist/server/assets/{cost-eEHeTRT5.js → cost-BC9yzyA9.js} +0 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
// Grok (xAI) login: OIDC discovery, PKCE authorize URL, loopback/paste code exchange, RFC 8628
|
|
2
|
+
// device code, and the post-login identity lookup. Mechanism follows grok-build 1.0.45
|
|
3
|
+
// crates/codegen/xai-grok-login/src (oidc/protocol.rs, oidc/login.rs, device_code.rs).
|
|
4
|
+
// Error messages never carry response bodies: token endpoints can echo credentials.
|
|
5
|
+
import { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
GROK_AUTH_ISSUER,
|
|
11
|
+
GROK_BUILD_VERSION,
|
|
12
|
+
GROK_CHAT_PROXY_URL,
|
|
13
|
+
GROK_DEVICE_CODE_URL,
|
|
14
|
+
GROK_DEVICE_TOKEN_URL,
|
|
15
|
+
GROK_OAUTH_CLIENT_ID,
|
|
16
|
+
GROK_OAUTH_REFERRER,
|
|
17
|
+
GROK_OAUTH_SCOPES,
|
|
18
|
+
} from "./grok-constants";
|
|
19
|
+
import { buildGrokProxyGetHeaders, grokShellUserAgent } from "./grok-headers";
|
|
20
|
+
import {
|
|
21
|
+
GrokCredentials,
|
|
22
|
+
GrokUserSubscription,
|
|
23
|
+
decodeGrokJwt,
|
|
24
|
+
isGrokAccessToken,
|
|
25
|
+
} from "./grok-types";
|
|
26
|
+
|
|
27
|
+
export type GrokFetch = typeof globalThis.fetch;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Errors
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export type GrokOAuthErrorCode =
|
|
34
|
+
| "invalid_callback_input"
|
|
35
|
+
| "authorization_failed"
|
|
36
|
+
| "state_mismatch"
|
|
37
|
+
| "token_exchange_failed"
|
|
38
|
+
| "invalid_token_response"
|
|
39
|
+
| "invalid_access_token"
|
|
40
|
+
| "missing_refresh_token"
|
|
41
|
+
| "missing_expiry"
|
|
42
|
+
| "user_id_unresolved"
|
|
43
|
+
| "user_lookup_failed"
|
|
44
|
+
| "invalid_credentials"
|
|
45
|
+
| "device_flow_not_enabled"
|
|
46
|
+
| "device_code_failed"
|
|
47
|
+
| "invalid_device_code_response"
|
|
48
|
+
| "device_token_failed"
|
|
49
|
+
| "access_denied"
|
|
50
|
+
| "expired_token";
|
|
51
|
+
|
|
52
|
+
/** Login failure. `retryable` marks network/5xx failures where the same step may be retried. */
|
|
53
|
+
export class GrokOAuthError extends Error {
|
|
54
|
+
override readonly name = "GrokOAuthError";
|
|
55
|
+
constructor(
|
|
56
|
+
readonly code: GrokOAuthErrorCode,
|
|
57
|
+
message: string,
|
|
58
|
+
readonly options: { status?: number; oauthError?: string; retryable?: boolean } = {},
|
|
59
|
+
) {
|
|
60
|
+
super(message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get status(): number | undefined {
|
|
64
|
+
return this.options.status;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get retryable(): boolean {
|
|
68
|
+
return this.options.retryable ?? false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** OAuth `error` code, only when it is a plain lowercase identifier. */
|
|
72
|
+
get oauthError(): string | undefined {
|
|
73
|
+
return this.options.oauthError;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Discovery
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
export interface GrokOAuthEndpoints {
|
|
82
|
+
authorizationEndpoint: string;
|
|
83
|
+
tokenEndpoint: string;
|
|
84
|
+
source: "discovery" | "fallback";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// grok-build caches 1 h (oidc/protocol.rs:269) and has no fallback (discovery errors abort the
|
|
88
|
+
// login, protocol.rs:279-302). qgrid caches 24 h and falls back to fixed paths: the token path
|
|
89
|
+
// is the one grok-build itself hard-codes for the device grant (device_code.rs:187); the
|
|
90
|
+
// authorize path is not present in the source and is unverified.
|
|
91
|
+
export const GROK_DISCOVERY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
92
|
+
export const GROK_FALLBACK_AUTHORIZATION_ENDPOINT = `${GROK_AUTH_ISSUER}/oauth2/authorize`;
|
|
93
|
+
export const GROK_FALLBACK_TOKEN_ENDPOINT = `${GROK_AUTH_ISSUER}/oauth2/token`;
|
|
94
|
+
|
|
95
|
+
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
96
|
+
const TOKEN_TIMEOUT_MS = 15_000;
|
|
97
|
+
const USER_LOOKUP_TIMEOUT_MS = 15_000;
|
|
98
|
+
|
|
99
|
+
const DiscoveryDocument = z.looseObject({
|
|
100
|
+
authorization_endpoint: z.string(),
|
|
101
|
+
token_endpoint: z.string(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let discoveryCache: { endpoints: GrokOAuthEndpoints; fetchedAt: number } | undefined;
|
|
105
|
+
|
|
106
|
+
/** Clears the module-level discovery cache (tests and operator resets). */
|
|
107
|
+
export function resetGrokDiscoveryCache(): void {
|
|
108
|
+
discoveryCache = undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A discovery document that redirected the token endpoint off xAI would ship refresh tokens to a
|
|
112
|
+
// third party, so only https endpoints on x.ai hosts are accepted; anything else falls back.
|
|
113
|
+
function isTrustedEndpoint(value: string): boolean {
|
|
114
|
+
let url: URL;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
url = new URL(value);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
url.protocol === "https:" &&
|
|
124
|
+
!url.username &&
|
|
125
|
+
!url.password &&
|
|
126
|
+
(url.hostname === "x.ai" || url.hostname.endsWith(".x.ai"))
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface GrokOAuthIo {
|
|
131
|
+
fetch?: GrokFetch;
|
|
132
|
+
now?: () => number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* `GET {issuer}/.well-known/openid-configuration`, cached 24 h. Any failure uses the fallback
|
|
137
|
+
* endpoints without caching them, so the next call retries discovery.
|
|
138
|
+
*/
|
|
139
|
+
export async function getGrokOAuthEndpoints(io: GrokOAuthIo = {}): Promise<GrokOAuthEndpoints> {
|
|
140
|
+
const fetchImpl = io.fetch ?? globalThis.fetch;
|
|
141
|
+
const now = io.now ?? Date.now;
|
|
142
|
+
const cached = discoveryCache;
|
|
143
|
+
|
|
144
|
+
if (cached && now() - cached.fetchedAt < GROK_DISCOVERY_TTL_MS) return cached.endpoints;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetchImpl(`${GROK_AUTH_ISSUER}/.well-known/openid-configuration`, {
|
|
148
|
+
method: "GET",
|
|
149
|
+
headers: { accept: "application/json", "user-agent": grokShellUserAgent() },
|
|
150
|
+
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (response.ok) {
|
|
154
|
+
const document = DiscoveryDocument.safeParse(await response.json());
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
document.success &&
|
|
158
|
+
isTrustedEndpoint(document.data.authorization_endpoint) &&
|
|
159
|
+
isTrustedEndpoint(document.data.token_endpoint)
|
|
160
|
+
) {
|
|
161
|
+
const endpoints: GrokOAuthEndpoints = {
|
|
162
|
+
authorizationEndpoint: document.data.authorization_endpoint,
|
|
163
|
+
tokenEndpoint: document.data.token_endpoint,
|
|
164
|
+
source: "discovery",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
discoveryCache = { endpoints, fetchedAt: now() };
|
|
168
|
+
|
|
169
|
+
return endpoints;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
await response.body?.cancel().catch(() => undefined);
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Network failure, timeout, or a non-JSON body: fall through to the fixed endpoints.
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
authorizationEndpoint: GROK_FALLBACK_AUTHORIZATION_ENDPOINT,
|
|
180
|
+
tokenEndpoint: GROK_FALLBACK_TOKEN_ENDPOINT,
|
|
181
|
+
source: "fallback",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// PKCE and authorize URL
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
export interface GrokPkce {
|
|
190
|
+
codeVerifier: string;
|
|
191
|
+
codeChallenge: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** S256 over a base64url verifier of 32 random bytes (oidc/protocol.rs:314-320). */
|
|
195
|
+
export function generateGrokPkce(): GrokPkce {
|
|
196
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
codeVerifier,
|
|
200
|
+
codeChallenge: createHash("sha256").update(codeVerifier).digest("base64url"),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** RFC 3986 unreserved-only encoding, matching Rust `urlencoding::encode` (space → %20). */
|
|
205
|
+
function encodeQueryValue(value: string): string {
|
|
206
|
+
return encodeURIComponent(value).replace(
|
|
207
|
+
/[!'()*]/g,
|
|
208
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const GROK_OAUTH_SCOPE = GROK_OAUTH_SCOPES.join(" ");
|
|
213
|
+
|
|
214
|
+
export interface GrokAuthorizeUrlInput {
|
|
215
|
+
authorizationEndpoint: string;
|
|
216
|
+
redirectUri: string;
|
|
217
|
+
codeChallenge: string;
|
|
218
|
+
state: string;
|
|
219
|
+
nonce: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Authorize URL with grok-build's query order (oidc/protocol.rs:321-366). */
|
|
223
|
+
export function buildGrokAuthorizeUrl(input: GrokAuthorizeUrlInput): string {
|
|
224
|
+
const query: [string, string][] = [
|
|
225
|
+
["response_type", "code"],
|
|
226
|
+
["client_id", GROK_OAUTH_CLIENT_ID],
|
|
227
|
+
["redirect_uri", input.redirectUri],
|
|
228
|
+
["scope", GROK_OAUTH_SCOPE],
|
|
229
|
+
["code_challenge", input.codeChallenge],
|
|
230
|
+
["code_challenge_method", "S256"],
|
|
231
|
+
["state", input.state],
|
|
232
|
+
["nonce", input.nonce],
|
|
233
|
+
["referrer", GROK_OAUTH_REFERRER],
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const search = query.map(([key, value]) => `${key}=${encodeQueryValue(value)}`).join("&");
|
|
237
|
+
|
|
238
|
+
return `${input.authorizationEndpoint}?${search}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function grokLoopbackRedirectUri(port: number): string {
|
|
242
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
243
|
+
throw new RangeError("redirect port must be an integer in 1..65535");
|
|
244
|
+
|
|
245
|
+
return `http://127.0.0.1:${port}/callback`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface StartGrokOAuthInput extends GrokOAuthIo {
|
|
249
|
+
state: string;
|
|
250
|
+
/** Loopback port; defaults to a random port in the ephemeral range. */
|
|
251
|
+
redirectPort?: number;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface StartGrokOAuthResult {
|
|
255
|
+
authorizeUrl: string;
|
|
256
|
+
verifier: string;
|
|
257
|
+
redirectUri: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function startGrokOAuth(input: StartGrokOAuthInput): Promise<StartGrokOAuthResult> {
|
|
261
|
+
if (!input.state) throw new TypeError("state must be non-empty");
|
|
262
|
+
const endpoints = await getGrokOAuthEndpoints(input);
|
|
263
|
+
const pkce = generateGrokPkce();
|
|
264
|
+
const redirectUri = grokLoopbackRedirectUri(input.redirectPort ?? randomInt(49_152, 65_536));
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
authorizeUrl: buildGrokAuthorizeUrl({
|
|
268
|
+
authorizationEndpoint: endpoints.authorizationEndpoint,
|
|
269
|
+
redirectUri,
|
|
270
|
+
codeChallenge: pkce.codeChallenge,
|
|
271
|
+
state: input.state,
|
|
272
|
+
nonce: randomUUID(),
|
|
273
|
+
}),
|
|
274
|
+
verifier: pkce.codeVerifier,
|
|
275
|
+
redirectUri,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Callback / paste parsing
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
export interface GrokCallbackInput {
|
|
284
|
+
code: string;
|
|
285
|
+
/** Empty for a bare pasted code, which carries no state (oidc/login.rs:33-60). */
|
|
286
|
+
state: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const OAUTH_ERROR_CODE = /^[a-z_]{1,80}$/;
|
|
290
|
+
|
|
291
|
+
function safeOAuthErrorCode(value: string): string | undefined {
|
|
292
|
+
return OAUTH_ERROR_CODE.test(value) ? value : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Accepts the full callback URL or a bare authorization code (oidc/login.rs:30-60). */
|
|
296
|
+
export function parseGrokCallbackInput(input: string): GrokCallbackInput {
|
|
297
|
+
const trimmed = input.trim();
|
|
298
|
+
|
|
299
|
+
if (!trimmed) throw new GrokOAuthError("invalid_callback_input", "empty callback input");
|
|
300
|
+
|
|
301
|
+
let url: URL | undefined;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
url = new URL(trimmed);
|
|
305
|
+
} catch {
|
|
306
|
+
url = undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (url) {
|
|
310
|
+
const code = url.searchParams.get("code");
|
|
311
|
+
|
|
312
|
+
if (code) return { code, state: url.searchParams.get("state") ?? "" };
|
|
313
|
+
const error = url.searchParams.get("error");
|
|
314
|
+
|
|
315
|
+
if (error) {
|
|
316
|
+
const oauthError = safeOAuthErrorCode(error);
|
|
317
|
+
throw new GrokOAuthError(
|
|
318
|
+
"authorization_failed",
|
|
319
|
+
`authorization failed: ${oauthError ?? "unknown_error"}`,
|
|
320
|
+
oauthError === undefined ? {} : { oauthError },
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new GrokOAuthError("invalid_callback_input", "callback URL has no code parameter");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (/\s/.test(trimmed))
|
|
328
|
+
throw new GrokOAuthError("invalid_callback_input", "authorization code contains whitespace");
|
|
329
|
+
|
|
330
|
+
return { code: trimmed, state: "" };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function statesEqual(expected: string, received: string): boolean {
|
|
334
|
+
const a = Buffer.from(expected);
|
|
335
|
+
const b = Buffer.from(received);
|
|
336
|
+
|
|
337
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Token exchange and finishing
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
export const GrokTokenResponse = z.looseObject({
|
|
345
|
+
access_token: z.string().min(1),
|
|
346
|
+
refresh_token: z.string().min(1).nullish(),
|
|
347
|
+
id_token: z.string().nullish(),
|
|
348
|
+
expires_in: z.number().nonnegative().nullish(),
|
|
349
|
+
scope: z.string().nullish(),
|
|
350
|
+
token_type: z.string().nullish(),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
export type GrokTokenResponse = z.infer<typeof GrokTokenResponse>;
|
|
354
|
+
|
|
355
|
+
const OAuthErrorBody = z.looseObject({ error: z.string() });
|
|
356
|
+
|
|
357
|
+
/** Reads the OAuth `error` code from a failed response without retaining the body. */
|
|
358
|
+
export async function readGrokOAuthErrorCode(response: Response): Promise<string | undefined> {
|
|
359
|
+
const text = await response.text().catch(() => "");
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const parsed = OAuthErrorBody.safeParse(JSON.parse(text));
|
|
363
|
+
|
|
364
|
+
return parsed.success ? safeOAuthErrorCode(parsed.data.error) : undefined;
|
|
365
|
+
} catch {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function formHeaders(extra: Record<string, string> = {}) {
|
|
371
|
+
return {
|
|
372
|
+
"user-agent": grokShellUserAgent(),
|
|
373
|
+
...extra,
|
|
374
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
375
|
+
accept: "application/json",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function parseTokenResponse(response: Response): Promise<GrokTokenResponse> {
|
|
380
|
+
let body: unknown;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
body = await response.json();
|
|
384
|
+
} catch {
|
|
385
|
+
throw new GrokOAuthError("invalid_token_response", "token response is not JSON");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const parsed = GrokTokenResponse.safeParse(body);
|
|
389
|
+
|
|
390
|
+
if (!parsed.success)
|
|
391
|
+
throw new GrokOAuthError("invalid_token_response", "token response failed validation");
|
|
392
|
+
|
|
393
|
+
return parsed.data;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function networkError(code: GrokOAuthErrorCode, what: string, cause: unknown): GrokOAuthError {
|
|
397
|
+
const reason = cause instanceof Error ? cause.name : "unknown";
|
|
398
|
+
|
|
399
|
+
return new GrokOAuthError(code, `${what} request failed (${reason})`, { retryable: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface FinishGrokLoginInput extends GrokOAuthIo {
|
|
403
|
+
tokens: GrokTokenResponse;
|
|
404
|
+
requestedScopes?: readonly string[];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Validates the token response, resolves the account id (JWT `sub`, confirmed by
|
|
409
|
+
* `GET /user?include=subscription`), and builds stored credentials.
|
|
410
|
+
*/
|
|
411
|
+
export async function finishGrokLogin(input: FinishGrokLoginInput): Promise<GrokCredentials> {
|
|
412
|
+
const fetchImpl = input.fetch ?? globalThis.fetch;
|
|
413
|
+
const now = input.now ?? Date.now;
|
|
414
|
+
const { tokens } = input;
|
|
415
|
+
|
|
416
|
+
if (!isGrokAccessToken(tokens.access_token))
|
|
417
|
+
throw new GrokOAuthError(
|
|
418
|
+
"invalid_access_token",
|
|
419
|
+
`access token is not an at+jwt issued by ${GROK_AUTH_ISSUER}`,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
if (!tokens.refresh_token)
|
|
423
|
+
throw new GrokOAuthError("missing_refresh_token", "token response has no refresh token");
|
|
424
|
+
|
|
425
|
+
const decoded = decodeGrokJwt(tokens.access_token);
|
|
426
|
+
const subject = decoded?.payload.sub;
|
|
427
|
+
|
|
428
|
+
if (!subject)
|
|
429
|
+
throw new GrokOAuthError("user_id_unresolved", "access token has no subject (sub) claim");
|
|
430
|
+
|
|
431
|
+
let response: Response;
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
response = await fetchImpl(`${GROK_CHAT_PROXY_URL}/user?include=subscription`, {
|
|
435
|
+
method: "GET",
|
|
436
|
+
headers: buildGrokProxyGetHeaders({ bearer: tokens.access_token, userId: subject }),
|
|
437
|
+
signal: AbortSignal.timeout(USER_LOOKUP_TIMEOUT_MS),
|
|
438
|
+
});
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw networkError("user_lookup_failed", "user lookup", error);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!response.ok) {
|
|
444
|
+
await response.body?.cancel().catch(() => undefined);
|
|
445
|
+
throw new GrokOAuthError("user_lookup_failed", `user lookup failed: HTTP ${response.status}`, {
|
|
446
|
+
status: response.status,
|
|
447
|
+
retryable: response.status >= 500 || response.status === 429,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let userBody: unknown;
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
userBody = await response.json();
|
|
455
|
+
} catch {
|
|
456
|
+
throw new GrokOAuthError("user_id_unresolved", "user lookup returned non-JSON");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const user = GrokUserSubscription.safeParse(userBody);
|
|
460
|
+
|
|
461
|
+
if (!user.success)
|
|
462
|
+
throw new GrokOAuthError("user_id_unresolved", "user lookup did not return a valid userId");
|
|
463
|
+
|
|
464
|
+
const issuedAt = now();
|
|
465
|
+
const expiresIn = tokens.expires_in;
|
|
466
|
+
const jwtExpiry = decoded?.payload.exp;
|
|
467
|
+
|
|
468
|
+
const expiresAtMs =
|
|
469
|
+
expiresIn !== undefined && expiresIn !== null
|
|
470
|
+
? issuedAt + expiresIn * 1000
|
|
471
|
+
: jwtExpiry !== undefined
|
|
472
|
+
? jwtExpiry * 1000
|
|
473
|
+
: undefined;
|
|
474
|
+
|
|
475
|
+
if (expiresAtMs === undefined || !Number.isFinite(expiresAtMs))
|
|
476
|
+
throw new GrokOAuthError("missing_expiry", "token response has no expiry");
|
|
477
|
+
|
|
478
|
+
const grantedScopes = tokens.scope?.split(" ").filter(Boolean);
|
|
479
|
+
|
|
480
|
+
const candidate: GrokCredentials = {
|
|
481
|
+
authMode: "oauth" as const,
|
|
482
|
+
accessToken: tokens.access_token,
|
|
483
|
+
refreshToken: tokens.refresh_token,
|
|
484
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
485
|
+
issuer: GROK_AUTH_ISSUER,
|
|
486
|
+
clientId: GROK_OAUTH_CLIENT_ID,
|
|
487
|
+
scopes:
|
|
488
|
+
grantedScopes && grantedScopes.length > 0
|
|
489
|
+
? grantedScopes
|
|
490
|
+
: [...(input.requestedScopes ?? GROK_OAUTH_SCOPES)],
|
|
491
|
+
userId: user.data.userId,
|
|
492
|
+
sessionId: randomUUID(),
|
|
493
|
+
agentId: randomUUID(),
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
if (user.data.subscriptionTier) candidate.subscriptionTier = user.data.subscriptionTier;
|
|
497
|
+
|
|
498
|
+
const credentials = GrokCredentials.safeParse(candidate);
|
|
499
|
+
|
|
500
|
+
if (!credentials.success)
|
|
501
|
+
throw new GrokOAuthError("invalid_credentials", "credentials failed validation");
|
|
502
|
+
|
|
503
|
+
return credentials.data;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export interface CompleteGrokOAuthInput extends GrokOAuthIo {
|
|
507
|
+
codeOrUrl: string;
|
|
508
|
+
verifier: string;
|
|
509
|
+
redirectUri: string;
|
|
510
|
+
/** The state issued with the authorize URL; a pasted URL must echo it. */
|
|
511
|
+
state: string;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function completeGrokOAuth(input: CompleteGrokOAuthInput): Promise<GrokCredentials> {
|
|
515
|
+
const fetchImpl = input.fetch ?? globalThis.fetch;
|
|
516
|
+
const callback = parseGrokCallbackInput(input.codeOrUrl);
|
|
517
|
+
|
|
518
|
+
// A bare code carries no state; the caller binds it to the pending login (oidc/login.rs:452-455).
|
|
519
|
+
if (callback.state && !statesEqual(input.state, callback.state))
|
|
520
|
+
throw new GrokOAuthError("state_mismatch", "OAuth state mismatch");
|
|
521
|
+
|
|
522
|
+
const endpoints = await getGrokOAuthEndpoints(input);
|
|
523
|
+
let response: Response;
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
response = await fetchImpl(endpoints.tokenEndpoint, {
|
|
527
|
+
method: "POST",
|
|
528
|
+
// oidc/protocol.rs:385-399: form body, x-grok-client-version, shared-client UA.
|
|
529
|
+
headers: formHeaders({ "x-grok-client-version": GROK_BUILD_VERSION }),
|
|
530
|
+
body: new URLSearchParams([
|
|
531
|
+
["grant_type", "authorization_code"],
|
|
532
|
+
["code", callback.code],
|
|
533
|
+
["redirect_uri", input.redirectUri],
|
|
534
|
+
["client_id", GROK_OAUTH_CLIENT_ID],
|
|
535
|
+
["code_verifier", input.verifier],
|
|
536
|
+
]),
|
|
537
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
538
|
+
});
|
|
539
|
+
} catch (error) {
|
|
540
|
+
throw networkError("token_exchange_failed", "token exchange", error);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (!response.ok) {
|
|
544
|
+
const oauthError = await readGrokOAuthErrorCode(response);
|
|
545
|
+
const errorOptions: GrokOAuthError["options"] = { status: response.status };
|
|
546
|
+
|
|
547
|
+
if (oauthError !== undefined) errorOptions.oauthError = oauthError;
|
|
548
|
+
errorOptions.retryable = response.status >= 500;
|
|
549
|
+
throw new GrokOAuthError(
|
|
550
|
+
"token_exchange_failed",
|
|
551
|
+
`token exchange failed: HTTP ${response.status} ${oauthError ?? "unknown"}`,
|
|
552
|
+
errorOptions,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return finishGrokLogin({
|
|
557
|
+
...input,
|
|
558
|
+
fetch: fetchImpl,
|
|
559
|
+
tokens: await parseTokenResponse(response),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
// Device code (RFC 8628)
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
// device_code.rs:19-20
|
|
568
|
+
export const GROK_DEVICE_DEFAULT_INTERVAL_S = 5;
|
|
569
|
+
export const GROK_DEVICE_SLOW_DOWN_INCREMENT_S = 5;
|
|
570
|
+
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
571
|
+
|
|
572
|
+
// The dashboard renders the URL and code to a human (device_code.rs:266-271 → `ui`).
|
|
573
|
+
const DEVICE_CLIENT_SURFACE = "ui";
|
|
574
|
+
|
|
575
|
+
function isAllowedVerificationUri(value: string): boolean {
|
|
576
|
+
// device_code.rs:475-488: reject control characters, require https (http only on loopback).
|
|
577
|
+
// Control characters are single UTF-16 code units, so scanning code units is exact.
|
|
578
|
+
for (let index = 0; index < value.length; index++) {
|
|
579
|
+
const code = value.charCodeAt(index);
|
|
580
|
+
|
|
581
|
+
if (code < 0x20 || code === 0x7f) return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let url: URL;
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
url = new URL(value);
|
|
588
|
+
} catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (url.protocol === "https:") return true;
|
|
593
|
+
|
|
594
|
+
return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const DeviceCodeResponse = z.looseObject({
|
|
598
|
+
device_code: z.string().min(1),
|
|
599
|
+
user_code: z.string().regex(/^[A-Za-z0-9-]+$/),
|
|
600
|
+
verification_uri: z.string().refine(isAllowedVerificationUri),
|
|
601
|
+
verification_uri_complete: z.string().refine(isAllowedVerificationUri).nullish(),
|
|
602
|
+
expires_in: z.number().positive(),
|
|
603
|
+
interval: z.number().int().positive().nullish(),
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
export interface GrokDeviceCode {
|
|
607
|
+
deviceCode: string;
|
|
608
|
+
userCode: string;
|
|
609
|
+
verificationUri: string;
|
|
610
|
+
verificationUriComplete?: string;
|
|
611
|
+
/** Seconds until the device code expires. */
|
|
612
|
+
expiresIn: number;
|
|
613
|
+
/** Seconds to wait before the next poll. */
|
|
614
|
+
interval: number;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function deviceHeaders(): Record<string, string> {
|
|
618
|
+
return formHeaders({
|
|
619
|
+
"x-grok-client-version": GROK_BUILD_VERSION,
|
|
620
|
+
"x-grok-client-surface": DEVICE_CLIENT_SURFACE,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export async function startGrokDeviceCode(io: GrokOAuthIo = {}): Promise<GrokDeviceCode> {
|
|
625
|
+
const fetchImpl = io.fetch ?? globalThis.fetch;
|
|
626
|
+
let response: Response;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
response = await fetchImpl(GROK_DEVICE_CODE_URL, {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: deviceHeaders(),
|
|
632
|
+
body: new URLSearchParams([
|
|
633
|
+
["client_id", GROK_OAUTH_CLIENT_ID],
|
|
634
|
+
["scope", GROK_OAUTH_SCOPE],
|
|
635
|
+
["referrer", GROK_OAUTH_REFERRER],
|
|
636
|
+
]),
|
|
637
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
638
|
+
});
|
|
639
|
+
} catch (error) {
|
|
640
|
+
throw networkError("device_code_failed", "device code", error);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (!response.ok) {
|
|
644
|
+
await response.body?.cancel().catch(() => undefined);
|
|
645
|
+
|
|
646
|
+
if (response.status === 404)
|
|
647
|
+
throw new GrokOAuthError("device_flow_not_enabled", "device-code login is not available", {
|
|
648
|
+
status: 404,
|
|
649
|
+
});
|
|
650
|
+
throw new GrokOAuthError(
|
|
651
|
+
"device_code_failed",
|
|
652
|
+
`device code request failed: HTTP ${response.status}`,
|
|
653
|
+
{ status: response.status, retryable: response.status >= 500 || response.status === 429 },
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
let body: unknown;
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
body = await response.json();
|
|
661
|
+
} catch {
|
|
662
|
+
throw new GrokOAuthError("invalid_device_code_response", "device code response is not JSON");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const parsed = DeviceCodeResponse.safeParse(body);
|
|
666
|
+
|
|
667
|
+
if (!parsed.success)
|
|
668
|
+
throw new GrokOAuthError(
|
|
669
|
+
"invalid_device_code_response",
|
|
670
|
+
"device code response failed validation",
|
|
671
|
+
);
|
|
672
|
+
const data = parsed.data;
|
|
673
|
+
|
|
674
|
+
const deviceCode: GrokDeviceCode = {
|
|
675
|
+
deviceCode: data.device_code,
|
|
676
|
+
userCode: data.user_code,
|
|
677
|
+
verificationUri: data.verification_uri,
|
|
678
|
+
expiresIn: data.expires_in,
|
|
679
|
+
interval: data.interval ?? GROK_DEVICE_DEFAULT_INTERVAL_S,
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
if (data.verification_uri_complete) {
|
|
683
|
+
deviceCode.verificationUriComplete = data.verification_uri_complete;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return deviceCode;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export type GrokDevicePollResult =
|
|
690
|
+
| { status: "pending"; interval: number }
|
|
691
|
+
| { status: "complete"; credentials: GrokCredentials };
|
|
692
|
+
|
|
693
|
+
export interface PollGrokDeviceCodeInput extends GrokOAuthIo {
|
|
694
|
+
deviceCode: string;
|
|
695
|
+
/** Current poll interval in seconds; the result carries the interval for the next poll. */
|
|
696
|
+
interval: number;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* One poll of the device token endpoint. The caller sleeps `interval` seconds between polls and
|
|
701
|
+
* enforces the device-code expiry; this function never loops.
|
|
702
|
+
*/
|
|
703
|
+
export async function pollGrokDeviceCode(
|
|
704
|
+
input: PollGrokDeviceCodeInput,
|
|
705
|
+
): Promise<GrokDevicePollResult> {
|
|
706
|
+
const fetchImpl = input.fetch ?? globalThis.fetch;
|
|
707
|
+
const interval = Math.max(1, Math.ceil(input.interval));
|
|
708
|
+
let response: Response;
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
response = await fetchImpl(GROK_DEVICE_TOKEN_URL, {
|
|
712
|
+
method: "POST",
|
|
713
|
+
headers: deviceHeaders(),
|
|
714
|
+
body: new URLSearchParams([
|
|
715
|
+
["grant_type", DEVICE_GRANT_TYPE],
|
|
716
|
+
["device_code", input.deviceCode],
|
|
717
|
+
["client_id", GROK_OAUTH_CLIENT_ID],
|
|
718
|
+
]),
|
|
719
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
720
|
+
});
|
|
721
|
+
} catch (error) {
|
|
722
|
+
throw networkError("device_token_failed", "device token", error);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (response.ok)
|
|
726
|
+
return {
|
|
727
|
+
status: "complete",
|
|
728
|
+
credentials: await finishGrokLogin({
|
|
729
|
+
...input,
|
|
730
|
+
fetch: fetchImpl,
|
|
731
|
+
tokens: await parseTokenResponse(response),
|
|
732
|
+
}),
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const oauthError = await readGrokOAuthErrorCode(response);
|
|
736
|
+
|
|
737
|
+
switch (oauthError) {
|
|
738
|
+
case "authorization_pending":
|
|
739
|
+
return { status: "pending", interval };
|
|
740
|
+
case "slow_down":
|
|
741
|
+
return { status: "pending", interval: interval + GROK_DEVICE_SLOW_DOWN_INCREMENT_S };
|
|
742
|
+
case "access_denied":
|
|
743
|
+
throw new GrokOAuthError("access_denied", "authorization denied by the user", {
|
|
744
|
+
status: response.status,
|
|
745
|
+
oauthError,
|
|
746
|
+
});
|
|
747
|
+
case "expired_token":
|
|
748
|
+
throw new GrokOAuthError("expired_token", "device code expired", {
|
|
749
|
+
status: response.status,
|
|
750
|
+
oauthError,
|
|
751
|
+
});
|
|
752
|
+
default:
|
|
753
|
+
throw new GrokOAuthError(
|
|
754
|
+
"device_token_failed",
|
|
755
|
+
`device token request failed: HTTP ${response.status} ${oauthError ?? "unknown"}`,
|
|
756
|
+
deviceTokenErrorOptions(response.status, oauthError),
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function deviceTokenErrorOptions(
|
|
762
|
+
status: number,
|
|
763
|
+
oauthError: string | undefined,
|
|
764
|
+
): GrokOAuthError["options"] {
|
|
765
|
+
const errorOptions: GrokOAuthError["options"] = { status };
|
|
766
|
+
|
|
767
|
+
if (oauthError !== undefined) errorOptions.oauthError = oauthError;
|
|
768
|
+
errorOptions.retryable = oauthError === undefined && status >= 500;
|
|
769
|
+
|
|
770
|
+
return errorOptions;
|
|
771
|
+
}
|