@alumbwe/anvil 1.0.41 → 1.0.49
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/bin/anvil.js +18 -0
- package/package.json +13 -6
- package/src/agents/bundled-agents.generated.ts +1 -1
- package/src/entry.ts +5 -0
- package/src/index.tsx +27 -4
- package/src/login/plain-login.ts +60 -0
- package/src/login/supabase-auth.ts +346 -0
- package/src/pre-init/client-env.ts +17 -0
- package/src/utils/anvil-client.ts +4 -0
- package/src/utils/auth.ts +16 -4
- package/src/utils/client-env-defaults.ts +28 -0
- package/src/utils/config-dir.ts +1 -1
- package/vendor/@anvil/common/src/constants/deepseek-direct.ts +30 -1
- package/vendor/@anvil/common/src/util/project-ignore.ts +4 -3
- package/vendor/@anvil/sdk/dist/index.cjs +46 -10
- package/vendor/@anvil/sdk/dist/index.mjs +46 -10
- package/src/commands/__tests__/anvil-command-aliases.test.ts +0 -56
- package/src/commands/__tests__/bash-command.test.ts +0 -445
- package/src/commands/__tests__/command-args.test.ts +0 -349
- package/src/commands/__tests__/copy-conversation-clipboard.test.ts +0 -108
- package/src/commands/__tests__/copy-conversation.test.ts +0 -226
- package/src/commands/__tests__/diagnostics-command.test.ts +0 -35
- package/src/commands/__tests__/image.test.ts +0 -99
- package/src/commands/__tests__/init.test.ts +0 -456
- package/src/commands/__tests__/new-command-rotates-chat.test.ts +0 -91
- package/src/commands/__tests__/process-diagnostics.test.ts +0 -54
- package/src/commands/__tests__/prompt-builders.test.ts +0 -28
- package/src/commands/__tests__/router-input.test.ts +0 -298
- package/src/components/__tests__/ad-banner.test.tsx +0 -83
- package/src/components/__tests__/anvil-landing-heading-row.test.tsx +0 -91
- package/src/components/__tests__/anvil-model-selector.test.tsx +0 -519
- package/src/components/__tests__/anvil-offer-invariants.test.ts +0 -54
- package/src/components/__tests__/anvil-takeover-prompt.test.tsx +0 -212
- package/src/components/__tests__/chat-input-bar.test.tsx +0 -130
- package/src/components/__tests__/grid-layout.integration.test.tsx +0 -304
- package/src/components/__tests__/grid-layout.test.tsx +0 -1051
- package/src/components/__tests__/message-block.completion.test.tsx +0 -104
- package/src/components/__tests__/message-block.streaming.test.tsx +0 -78
- package/src/components/__tests__/message-with-agents.test.tsx +0 -579
- package/src/components/__tests__/multiline-input.test.tsx +0 -1132
- package/src/components/__tests__/queue-panel.test.tsx +0 -398
- package/src/components/__tests__/selectable-list.test.ts +0 -232
- package/src/components/__tests__/status-bar.test.tsx +0 -44
- package/src/components/__tests__/status-indicator.test.tsx +0 -204
- package/src/components/__tests__/terminal-thinking-orb.test.tsx +0 -111
- package/src/components/__tests__/user-error-banner.test.tsx +0 -102
- package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +0 -432
- package/src/components/ask-user/__tests__/validation.test.ts +0 -213
- package/src/components/tools/__tests__/apply-patch.test.tsx +0 -84
- package/src/components/tools/__tests__/code-search.test.tsx +0 -45
- package/src/components/tools/__tests__/gravity-index.test.ts +0 -76
- package/src/components/tools/__tests__/render-ui.test.tsx +0 -92
- package/src/components/tools/__tests__/run-terminal-command.test.ts +0 -233
- package/src/contexts/__tests__/chat-runtime-context.test.tsx +0 -149
- package/src/data/slash-commands.test.ts +0 -24
- package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +0 -49
- package/src/hooks/__tests__/session-fetch-signal.test.ts +0 -63
- package/src/hooks/__tests__/use-activity-query.test.ts +0 -1000
- package/src/hooks/__tests__/use-ask-user-bridge.test.ts +0 -176
- package/src/hooks/__tests__/use-auth-query.test.ts +0 -21
- package/src/hooks/__tests__/use-chat-input-remount.test.tsx +0 -66
- package/src/hooks/__tests__/use-clipboard.test.tsx +0 -73
- package/src/hooks/__tests__/use-connection-status.test.ts +0 -112
- package/src/hooks/__tests__/use-directory-browser.test.ts +0 -203
- package/src/hooks/__tests__/use-gravity-ad.test.ts +0 -73
- package/src/hooks/__tests__/use-grid-layout.test.ts +0 -349
- package/src/hooks/__tests__/use-input-history.test.ts +0 -699
- package/src/hooks/__tests__/use-message-queue-editing.test.tsx +0 -209
- package/src/hooks/__tests__/use-message-queue.test.tsx +0 -71
- package/src/hooks/__tests__/use-path-tab-completion.test.ts +0 -315
- package/src/hooks/__tests__/use-queue-controls.test.ts +0 -68
- package/src/hooks/__tests__/use-queue-ui.test.tsx +0 -83
- package/src/hooks/__tests__/use-searchable-list.test.ts +0 -359
- package/src/hooks/__tests__/use-send-message-timer.test.ts +0 -146
- package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +0 -361
- package/src/hooks/__tests__/use-suggestion-engine.test.ts +0 -485
- package/src/hooks/__tests__/use-terminal-focus.test.ts +0 -66
- package/src/hooks/__tests__/use-terminal-layout.test.ts +0 -675
- package/src/hooks/__tests__/use-timeout.test.ts +0 -330
- package/src/hooks/__tests__/use-usage-query.test.ts +0 -502
- package/src/hooks/__tests__/use-user-details-query.test.ts +0 -195
- package/src/hooks/helpers/__tests__/send-message.test.ts +0 -1918
- package/src/init/__tests__/init-direnv.test.ts +0 -526
- package/src/state/__tests__/chat-store-focus.test.ts +0 -22
- package/src/state/__tests__/feedback-store.test.ts +0 -257
- package/src/utils/__tests__/active-run.test.ts +0 -140
- package/src/utils/__tests__/activity-tracker.test.ts +0 -440
- package/src/utils/__tests__/agent-display.test.ts +0 -139
- package/src/utils/__tests__/analytics-client.test.ts +0 -262
- package/src/utils/__tests__/anonymous-id.test.ts +0 -74
- package/src/utils/__tests__/anvil-api.test.ts +0 -578
- package/src/utils/__tests__/anvil-instance-owner.test.ts +0 -69
- package/src/utils/__tests__/anvil-model-navigation.test.ts +0 -102
- package/src/utils/__tests__/anvil-premium-reset.test.ts +0 -79
- package/src/utils/__tests__/anvil-referral-cache.test.ts +0 -116
- package/src/utils/__tests__/anvil-session-api.test.ts +0 -215
- package/src/utils/__tests__/anvil-session-display.test.ts +0 -23
- package/src/utils/__tests__/anvil-streak-line.test.ts +0 -217
- package/src/utils/__tests__/arrays.test.ts +0 -264
- package/src/utils/__tests__/bash-context-processor.test.ts +0 -38
- package/src/utils/__tests__/block-processor.test.ts +0 -810
- package/src/utils/__tests__/chat-history.test.ts +0 -203
- package/src/utils/__tests__/chat-input-key-intercept.test.ts +0 -40
- package/src/utils/__tests__/chat-meta.test.ts +0 -130
- package/src/utils/__tests__/clipboard-linux.test.ts +0 -359
- package/src/utils/__tests__/clipboard.test.ts +0 -910
- package/src/utils/__tests__/code-search-summary.test.ts +0 -84
- package/src/utils/__tests__/collapse-helpers.test.ts +0 -1109
- package/src/utils/__tests__/error-handling.test.ts +0 -658
- package/src/utils/__tests__/exit-cleanly.test.ts +0 -110
- package/src/utils/__tests__/feedback-helpers.test.ts +0 -444
- package/src/utils/__tests__/feedback-submission.test.ts +0 -26
- package/src/utils/__tests__/fetch-usage.test.ts +0 -355
- package/src/utils/__tests__/fingerprint.test.ts +0 -144
- package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +0 -7
- package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +0 -3
- package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +0 -33
- package/src/utils/__tests__/format-elapsed-time.test.ts +0 -103
- package/src/utils/__tests__/format-timeout.test.ts +0 -87
- package/src/utils/__tests__/image-dimensions.test.ts +0 -249
- package/src/utils/__tests__/image-processor.test.ts +0 -209
- package/src/utils/__tests__/implementor-helpers.test.ts +0 -1458
- package/src/utils/__tests__/keyboard-actions.test.ts +0 -660
- package/src/utils/__tests__/layout-helpers.test.ts +0 -36
- package/src/utils/__tests__/lazy-response-ads.test.ts +0 -167
- package/src/utils/__tests__/log-shipper.test.ts +0 -53
- package/src/utils/__tests__/markdown-renderer.test.tsx +0 -421
- package/src/utils/__tests__/message-block-helpers.test.ts +0 -1263
- package/src/utils/__tests__/message-updater.test.ts +0 -645
- package/src/utils/__tests__/osc-timeout-scenarios.test.ts +0 -172
- package/src/utils/__tests__/pending-attachments.test.ts +0 -277
- package/src/utils/__tests__/polling-backoff.test.ts +0 -51
- package/src/utils/__tests__/queue-panel-actions.test.ts +0 -148
- package/src/utils/__tests__/run-state-storage.test.ts +0 -919
- package/src/utils/__tests__/safe-json.test.ts +0 -77
- package/src/utils/__tests__/sdk-event-handlers.test.ts +0 -685
- package/src/utils/__tests__/send-message-helpers.test.ts +0 -1898
- package/src/utils/__tests__/send-message-timer.test.ts +0 -64
- package/src/utils/__tests__/settings.test.ts +0 -99
- package/src/utils/__tests__/strings.test.ts +0 -192
- package/src/utils/__tests__/terminal-color-detection.test.ts +0 -425
- package/src/utils/__tests__/terminal-command-broker.test.ts +0 -528
- package/src/utils/__tests__/terminal-enter-detection.test.ts +0 -77
- package/src/utils/__tests__/terminal-io.test.ts +0 -137
- package/src/utils/__tests__/terminal-protocol-controller.test.ts +0 -119
- package/src/utils/__tests__/text-layout.test.ts +0 -82
- package/src/utils/__tests__/theme-platform-detection.test.ts +0 -125
- package/src/utils/__tests__/think-tag-parser.test.ts +0 -159
- package/src/utils/__tests__/trace-writer.test.ts +0 -223
- package/src/utils/__tests__/trim-chat-logs.test.ts +0 -78
- package/src/utils/__tests__/usage-banner-state.test.ts +0 -298
- package/src/utils/__tests__/validation-error-formatting.test.ts +0 -126
- package/src/utils/__tests__/windows-terminal-health.test.ts +0 -101
- package/src/utils/__tests__/write-file-atomic.test.ts +0 -127
- package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +0 -846
- package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +0 -101
- package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +0 -1408
- package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +0 -258
- package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +0 -48
- package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +0 -282
- package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +0 -526
- package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +0 -68
- package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +0 -132
- package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +0 -422
- package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +0 -167
- package/vendor/@anvil/common/src/__tests__/env-process.test.ts +0 -145
- package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +0 -234
- package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +0 -379
- package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +0 -807
- package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +0 -154
- package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +0 -246
- package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +0 -77
- package/vendor/@anvil/common/src/__tests__/model-config.test.ts +0 -89
- package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +0 -298
- package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +0 -116
- package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +0 -167
- package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +0 -132
- package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +0 -72
- package/vendor/@anvil/common/src/__tests__/user-state.test.ts +0 -30
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +0 -63
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +0 -246
- package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +0 -106
- package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +0 -351
- package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +0 -169
- package/vendor/@anvil/common/src/testing/errors.ts +0 -33
- package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +0 -334
- package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +0 -6
- package/vendor/@anvil/common/src/testing/index.ts +0 -84
- package/vendor/@anvil/common/src/testing/mock-modules.ts +0 -53
- package/vendor/@anvil/common/src/testing/mock-types.ts +0 -123
- package/vendor/@anvil/common/src/testing/mocks/analytics.ts +0 -261
- package/vendor/@anvil/common/src/testing/mocks/child-process.ts +0 -93
- package/vendor/@anvil/common/src/testing/mocks/crypto.ts +0 -218
- package/vendor/@anvil/common/src/testing/mocks/database.ts +0 -337
- package/vendor/@anvil/common/src/testing/mocks/fetch.ts +0 -219
- package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +0 -166
- package/vendor/@anvil/common/src/testing/mocks/index.ts +0 -101
- package/vendor/@anvil/common/src/testing/mocks/logger.ts +0 -135
- package/vendor/@anvil/common/src/testing/mocks/stream.ts +0 -313
- package/vendor/@anvil/common/src/testing/mocks/timers.ts +0 -132
- package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +0 -127
- package/vendor/@anvil/common/src/testing/setup.ts +0 -282
- package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +0 -34
- package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +0 -213
- package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +0 -47
- package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +0 -65
- package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +0 -49
- package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +0 -20
- package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +0 -26
- package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +0 -122
- package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +0 -102
- package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +0 -143
- package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +0 -202
- package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +0 -193
- package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +0 -140
- package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +0 -148
- package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +0 -115
- package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +0 -57
- package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +0 -774
- package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +0 -206
- package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +0 -173
- package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +0 -60
- package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +0 -31
- package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +0 -1252
- package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +0 -505
- package/vendor/@anvil/common/src/util/__tests__/path.test.ts +0 -22
- package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +0 -90
- package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +0 -325
- package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +0 -28
- package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +0 -130
- package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +0 -1008
- package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +0 -289
- package/vendor/@anvil/common/src/util/__tests__/string.test.ts +0 -239
- package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +0 -70
- package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +0 -108
- package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +0 -180
- package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +0 -24
- package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +0 -88
- package/vendor/@anvil/common/src/util/skills.test.ts +0 -44
- package/vendor/@anvil/sdk/dist/index.cjs.map +0 -440
- package/vendor/@anvil/sdk/dist/index.mjs.map +0 -440
package/bin/anvil.js
CHANGED
|
@@ -26,6 +26,24 @@ const PKG_ROOT = path.resolve(HERE, '..')
|
|
|
26
26
|
const ENTRY = path.join(PKG_ROOT, 'src', 'entry.ts')
|
|
27
27
|
const IS_WIN = process.platform === 'win32'
|
|
28
28
|
const BUN_EXE = IS_WIN ? 'bun.exe' : 'bun'
|
|
29
|
+
const COMPILED_BINARY = IS_WIN
|
|
30
|
+
? path.join(HERE, 'anvil.exe')
|
|
31
|
+
: path.join(HERE, 'anvil')
|
|
32
|
+
|
|
33
|
+
// Release installs include a compiled binary. Prefer it so the global npm
|
|
34
|
+
// shim runs the same build as `cli/bin/anvil.exe` and does not require Bun.
|
|
35
|
+
if (!process.versions.bun && fs.existsSync(COMPILED_BINARY)) {
|
|
36
|
+
const result = spawnSync(COMPILED_BINARY, process.argv.slice(2), {
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
env: process.env,
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
})
|
|
41
|
+
if (result.error) {
|
|
42
|
+
console.error(`Failed to start compiled Anvil (${COMPILED_BINARY}):`, result.error.message)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
process.exit(result.status ?? (result.signal ? 1 : 0))
|
|
46
|
+
}
|
|
29
47
|
|
|
30
48
|
/** Check that a candidate command is a working bun executable. */
|
|
31
49
|
function isBun(cmd) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alumbwe/anvil",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.49",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"anvil": "
|
|
7
|
+
"anvil": "bin/anvil.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/anvil.js",
|
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
"vendor/",
|
|
14
14
|
"tsconfig.json",
|
|
15
15
|
"bunfig.toml",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
16
|
+
"!**/__tests__/**",
|
|
17
|
+
"!**/__mocks__/**",
|
|
18
|
+
"!**/__fixtures__/**",
|
|
19
|
+
"!**/*.test.*",
|
|
20
|
+
"!**/*.spec.*",
|
|
21
|
+
"!**/testing/**",
|
|
22
|
+
"!**/smoke/**",
|
|
23
|
+
"!src/dist-private/**",
|
|
24
|
+
"!**/*.map",
|
|
25
|
+
"!**/*.tsbuildinfo"
|
|
20
26
|
],
|
|
21
27
|
"repository": {
|
|
22
28
|
"type": "git",
|
|
@@ -63,6 +69,7 @@
|
|
|
63
69
|
"node-machine-id": "^1.1.12",
|
|
64
70
|
"open": "^10.1.0",
|
|
65
71
|
"partial-json": "^0.1.7",
|
|
72
|
+
"picocolors": "^1.1.1",
|
|
66
73
|
"pino": "9.4.0",
|
|
67
74
|
"posthog-node": "^5.8.0",
|
|
68
75
|
"react": "^19.0.0",
|
package/src/entry.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
// MUST be first: applies production defaults for NEXT_PUBLIC_* before any
|
|
4
|
+
// module in the import graph evaluates `@anvil/common/env`, which validates
|
|
5
|
+
// the client env at import time and throws on machines without an env file.
|
|
6
|
+
import './pre-init/client-env'
|
|
7
|
+
|
|
3
8
|
import {
|
|
4
9
|
isTerminalCommandBrokerInvocation,
|
|
5
10
|
serveTerminalCommandBroker,
|
package/src/index.tsx
CHANGED
|
@@ -27,6 +27,8 @@ import { App } from './app'
|
|
|
27
27
|
import { loadPackageVersion, parseArgs } from './cli-args'
|
|
28
28
|
import { handlePublish } from './commands/publish'
|
|
29
29
|
import { runPlainLogin } from './login/plain-login'
|
|
30
|
+
import { ensureSignedIn } from './login/supabase-auth'
|
|
31
|
+
import { isManagedGatewayMode } from '@anvil/common/constants/deepseek-direct'
|
|
30
32
|
import { initializeApp } from './init/init-app'
|
|
31
33
|
import { loadEnvLocal } from './setup/load-env'
|
|
32
34
|
import { runKeySetup } from './setup/key-setup'
|
|
@@ -248,19 +250,40 @@ async function main(): Promise<void> {
|
|
|
248
250
|
process.exit(0)
|
|
249
251
|
}
|
|
250
252
|
|
|
251
|
-
// First-run:
|
|
252
|
-
//
|
|
253
|
-
|
|
253
|
+
// First-run: required local API keys. In managed gateway mode the DeepSeek
|
|
254
|
+
// (model) key lives on the SERVER — the gateway injects it — so the CLI must
|
|
255
|
+
// Managed mode does not require local provider keys. In direct development
|
|
256
|
+
// mode both provider keys are required locally.
|
|
257
|
+
const gatewayMode = isManagedGatewayMode()
|
|
258
|
+
// Managed mode keeps both model keys in Supabase; an installed CLI only
|
|
259
|
+
// needs its stored Supabase access token. Local provider keys are required
|
|
260
|
+
// only for direct development mode.
|
|
261
|
+
if (!gatewayMode && (!process.env.DEEPSEEK_API_KEY || !process.env.GEMINI_API_KEY)) {
|
|
254
262
|
const result = await runKeySetup(process.cwd())
|
|
255
263
|
if (!result.allRequired) {
|
|
256
264
|
console.error(
|
|
257
|
-
'\n
|
|
265
|
+
'\n Required API keys are missing.\n' +
|
|
258
266
|
' Run with `setup` to reconfigure, or set them in .env.local manually.\n',
|
|
259
267
|
)
|
|
260
268
|
process.exit(1)
|
|
261
269
|
}
|
|
262
270
|
}
|
|
263
271
|
|
|
272
|
+
// Managed gateway mode: `anvil` recognizes whether you're signed in and, if
|
|
273
|
+
// not, launches the browser sign-in before starting. (Direct mode and the
|
|
274
|
+
// login/setup commands are handled above and need no gate here.)
|
|
275
|
+
if (gatewayMode && !isPublishCommand) {
|
|
276
|
+
try {
|
|
277
|
+
await ensureSignedIn()
|
|
278
|
+
setApiClientAuthToken(getAuthToken())
|
|
279
|
+
} catch {
|
|
280
|
+
console.error(
|
|
281
|
+
'\n Sign-in required to use Anvil. Run `anvil login` and try again.\n',
|
|
282
|
+
)
|
|
283
|
+
process.exit(1)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
264
287
|
// Show project picker only when user starts at the home directory or an ancestor
|
|
265
288
|
const projectRoot = getProjectRoot()
|
|
266
289
|
const homeDir = os.homedir()
|
package/src/login/plain-login.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { AnalyticsEvent } from '@anvil/common/constants/analytics-events'
|
|
2
|
+
import { isManagedGatewayMode } from '@anvil/common/constants/deepseek-direct'
|
|
2
3
|
import { cyan, green, red, yellow, bold } from 'picocolors'
|
|
3
4
|
|
|
4
5
|
import { LOGIN_WEBSITE_URL } from './constants'
|
|
5
6
|
import { generateLoginUrl, pollLoginStatus } from './login-flow'
|
|
7
|
+
import {
|
|
8
|
+
isSupabaseAuthConfigured,
|
|
9
|
+
loginViaBrowser,
|
|
10
|
+
signInWithPassword,
|
|
11
|
+
} from './supabase-auth'
|
|
6
12
|
import {
|
|
7
13
|
flushAnalytics,
|
|
8
14
|
identifyUser,
|
|
@@ -23,7 +29,61 @@ import type { User } from '../utils/auth'
|
|
|
23
29
|
* This is the escape hatch for remote/SSH environments where the TUI's
|
|
24
30
|
* clipboard and browser integration don't work.
|
|
25
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* Managed gateway mode: authenticate with Supabase (the cli-agent gateway
|
|
34
|
+
* verifies a Supabase JWT). Opens the browser to the hosted sign-in page and
|
|
35
|
+
* captures the session via a local loopback callback. Non-interactive when
|
|
36
|
+
* ANVIL_LOGIN_EMAIL + ANVIL_LOGIN_PASSWORD are set.
|
|
37
|
+
*/
|
|
38
|
+
export async function runSupabaseLogin(): Promise<void> {
|
|
39
|
+
console.log()
|
|
40
|
+
console.log(bold('Anvil Login'))
|
|
41
|
+
console.log()
|
|
42
|
+
|
|
43
|
+
const envEmail = process.env.ANVIL_LOGIN_EMAIL
|
|
44
|
+
const envPassword = process.env.ANVIL_LOGIN_PASSWORD
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
let user
|
|
48
|
+
if (envEmail && envPassword) {
|
|
49
|
+
console.log(`Signing in as ${envEmail}...`)
|
|
50
|
+
user = await signInWithPassword(envEmail, envPassword)
|
|
51
|
+
} else {
|
|
52
|
+
console.log('Waiting for you to finish signing in in your browser…')
|
|
53
|
+
user = await loginViaBrowser()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (user.id) {
|
|
57
|
+
identifyUser(user.id, { email: user.email, anvil: IS_ANVIL })
|
|
58
|
+
trackEvent(AnalyticsEvent.LOGIN, {
|
|
59
|
+
userId: user.id,
|
|
60
|
+
via: 'plain_command',
|
|
61
|
+
hasEmail: Boolean(user.email),
|
|
62
|
+
hasName: Boolean(user.name),
|
|
63
|
+
})
|
|
64
|
+
await flushAnalytics()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log()
|
|
68
|
+
console.log(green(`✓ Logged in as ${user.name} (${user.email})`))
|
|
69
|
+
console.log()
|
|
70
|
+
console.log('You can now run ' + cyan('anvil') + ' to start.')
|
|
71
|
+
process.exit(0)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(
|
|
74
|
+
red(`Login failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
75
|
+
)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
26
80
|
export async function runPlainLogin(): Promise<void> {
|
|
81
|
+
// Managed gateway mode uses Supabase auth (the gateway verifies a Supabase
|
|
82
|
+
// JWT), not the Anvil device-code flow.
|
|
83
|
+
if (isManagedGatewayMode() && isSupabaseAuthConfigured()) {
|
|
84
|
+
return runSupabaseLogin()
|
|
85
|
+
}
|
|
86
|
+
|
|
27
87
|
const fingerprintId = await getFingerprintId()
|
|
28
88
|
|
|
29
89
|
console.log()
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supabase-auth.ts — CLI authentication for managed gateway mode.
|
|
3
|
+
*
|
|
4
|
+
* The cli-agent gateway authenticates each request with a Supabase JWT
|
|
5
|
+
* (`sb.auth.getUser(token)`). So in managed mode the CLI must hold a Supabase
|
|
6
|
+
* session, not an Anvil API key. This module talks to Supabase's GoTrue REST
|
|
7
|
+
* API directly (no extra dependency), stores the session in the normal
|
|
8
|
+
* credentials file as `authToken` (= access token) plus the refresh token and
|
|
9
|
+
* expiry, and refreshes the access token before it expires.
|
|
10
|
+
*
|
|
11
|
+
* Supported sign-in methods:
|
|
12
|
+
* - email OTP: requestEmailOtp() → verifyEmailOtp()
|
|
13
|
+
* - email + password: signInWithPassword()
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import http from 'node:http'
|
|
17
|
+
|
|
18
|
+
import open from 'open'
|
|
19
|
+
|
|
20
|
+
import { logger } from '../utils/logger'
|
|
21
|
+
import { getUserCredentials, saveUserCredentials, type User } from '../utils/auth'
|
|
22
|
+
|
|
23
|
+
/** GoTrue session as returned by /token and /verify. */
|
|
24
|
+
interface SupabaseSession {
|
|
25
|
+
access_token: string
|
|
26
|
+
refresh_token: string
|
|
27
|
+
expires_at?: number
|
|
28
|
+
expires_in?: number
|
|
29
|
+
user?: {
|
|
30
|
+
id?: string
|
|
31
|
+
email?: string
|
|
32
|
+
user_metadata?: { name?: string; full_name?: string }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function supabaseUrl(): string | undefined {
|
|
37
|
+
const url = process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
38
|
+
return url ? url.replace(/\/+$/, '') : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The anon/publishable key — GoTrue requires it as the `apikey` header. */
|
|
42
|
+
function supabaseAnonKey(): string | undefined {
|
|
43
|
+
return (
|
|
44
|
+
process.env.SUPABASE_PUBLISHABLE_KEY ??
|
|
45
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ??
|
|
46
|
+
process.env.SUPABASE_ANON_KEY ??
|
|
47
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when Supabase auth can run (URL + anon key configured). */
|
|
52
|
+
export function isSupabaseAuthConfigured(): boolean {
|
|
53
|
+
return Boolean(supabaseUrl() && supabaseAnonKey())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function authConfig(): { url: string; anonKey: string } {
|
|
57
|
+
const url = supabaseUrl()
|
|
58
|
+
const anonKey = supabaseAnonKey()
|
|
59
|
+
if (!url || !anonKey) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'Supabase is not configured. Set SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY.',
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
return { url, anonKey }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function gotrue(
|
|
68
|
+
path: string,
|
|
69
|
+
body: Record<string, unknown>,
|
|
70
|
+
): Promise<SupabaseSession> {
|
|
71
|
+
const { url, anonKey } = authConfig()
|
|
72
|
+
const res = await fetch(`${url}/auth/v1${path}`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
apikey: anonKey,
|
|
77
|
+
Authorization: `Bearer ${anonKey}`,
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
})
|
|
81
|
+
const text = await res.text()
|
|
82
|
+
let json: unknown
|
|
83
|
+
try {
|
|
84
|
+
json = text ? JSON.parse(text) : {}
|
|
85
|
+
} catch {
|
|
86
|
+
json = {}
|
|
87
|
+
}
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const message =
|
|
90
|
+
(json as { error_description?: string; msg?: string; error?: string })
|
|
91
|
+
?.error_description ??
|
|
92
|
+
(json as { msg?: string }).msg ??
|
|
93
|
+
(json as { error?: string }).error ??
|
|
94
|
+
`Supabase auth failed (${res.status})`
|
|
95
|
+
throw new Error(message)
|
|
96
|
+
}
|
|
97
|
+
return json as SupabaseSession
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Normalize a session's expiry to epoch seconds. */
|
|
101
|
+
function expiresAtOf(session: SupabaseSession): number {
|
|
102
|
+
if (typeof session.expires_at === 'number') return session.expires_at
|
|
103
|
+
const ttl = typeof session.expires_in === 'number' ? session.expires_in : 3600
|
|
104
|
+
return Math.floor(Date.now() / 1000) + ttl
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Map a Supabase session to the credentials `User` shape and persist it. */
|
|
108
|
+
export function saveSupabaseSession(session: SupabaseSession): User {
|
|
109
|
+
const meta = session.user?.user_metadata
|
|
110
|
+
const user: User = {
|
|
111
|
+
id: session.user?.id,
|
|
112
|
+
email: session.user?.email ?? '',
|
|
113
|
+
name: meta?.name ?? meta?.full_name ?? session.user?.email ?? 'Anvil user',
|
|
114
|
+
authToken: session.access_token,
|
|
115
|
+
refreshToken: session.refresh_token,
|
|
116
|
+
expiresAt: expiresAtOf(session),
|
|
117
|
+
provider: 'supabase',
|
|
118
|
+
}
|
|
119
|
+
saveUserCredentials(user)
|
|
120
|
+
return user
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Send an email OTP (6-digit code / magic link) for sign-in or signup. */
|
|
124
|
+
export async function requestEmailOtp(email: string): Promise<void> {
|
|
125
|
+
await gotrue('/otp', { email, create_user: true })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Verify an email OTP code and persist the resulting session. */
|
|
129
|
+
export async function verifyEmailOtp(email: string, token: string): Promise<User> {
|
|
130
|
+
const session = await gotrue('/verify', { type: 'email', email, token })
|
|
131
|
+
return saveSupabaseSession(session)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Sign in with email + password and persist the resulting session. */
|
|
135
|
+
export async function signInWithPassword(
|
|
136
|
+
email: string,
|
|
137
|
+
password: string,
|
|
138
|
+
): Promise<User> {
|
|
139
|
+
const session = await gotrue('/token?grant_type=password', { email, password })
|
|
140
|
+
return saveSupabaseSession(session)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Exchange a refresh token for a new session and persist it. */
|
|
144
|
+
async function refreshSupabaseSession(refreshToken: string): Promise<User> {
|
|
145
|
+
const session = await gotrue('/token?grant_type=refresh_token', {
|
|
146
|
+
refresh_token: refreshToken,
|
|
147
|
+
})
|
|
148
|
+
return saveSupabaseSession(session)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Browser sign-in (loopback) ─────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/** Hosted sign-in page that completes auth in the browser, then redirects the
|
|
154
|
+
* session back to the CLI's loopback callback. Override with ANVIL_LOGIN_URL. */
|
|
155
|
+
export function getLoginPageUrl(): string {
|
|
156
|
+
return (
|
|
157
|
+
process.env.ANVIL_LOGIN_URL ?? 'https://cli.tryanvil.dev/login'
|
|
158
|
+
).replace(/\/+$/, '')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Decode a JWT payload (no verification — only for display id/email). */
|
|
162
|
+
function decodeJwt(token: string): { sub?: string; email?: string } {
|
|
163
|
+
try {
|
|
164
|
+
const part = token.split('.')[1] ?? ''
|
|
165
|
+
const json = Buffer.from(
|
|
166
|
+
part.replace(/-/g, '+').replace(/_/g, '/'),
|
|
167
|
+
'base64',
|
|
168
|
+
).toString('utf8')
|
|
169
|
+
return JSON.parse(json)
|
|
170
|
+
} catch {
|
|
171
|
+
return {}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Served on the loopback /callback: reads the URL fragment and posts the
|
|
176
|
+
* tokens back to this same server (same-origin, so no mixed-content issues). */
|
|
177
|
+
function callbackHtml(): string {
|
|
178
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" />
|
|
179
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
180
|
+
<title>Anvil — signing in</title>
|
|
181
|
+
<style>body{margin:0;height:100vh;display:grid;place-items:center;background:#0a0a0a;color:#f5f5f5;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}div{text-align:center}p{color:#a0a0a0}</style>
|
|
182
|
+
</head><body><div><h2>Signing you in…</h2><p id="m">One moment.</p></div>
|
|
183
|
+
<script>
|
|
184
|
+
(function(){
|
|
185
|
+
var p = new URLSearchParams(location.hash.replace(/^#/,''));
|
|
186
|
+
var body = {access_token:p.get('access_token'),refresh_token:p.get('refresh_token'),expires_at:p.get('expires_at')};
|
|
187
|
+
if(!body.access_token){document.getElementById('m').textContent='No token found. Return to the terminal and run anvil login again.';return;}
|
|
188
|
+
fetch('/token',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)})
|
|
189
|
+
.then(function(){document.getElementById('m').textContent='You are signed in. You can close this tab and return to your terminal.';})
|
|
190
|
+
.catch(function(){document.getElementById('m').textContent='Could not reach the CLI. Is "anvil login" still running?';});
|
|
191
|
+
})();
|
|
192
|
+
</script></body></html>`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Open the browser to the hosted sign-in page and capture the session via a
|
|
197
|
+
* local loopback callback (the gh/vercel-style flow). Resolves once the
|
|
198
|
+
* browser posts back the Supabase session, which is then persisted.
|
|
199
|
+
*/
|
|
200
|
+
export async function loginViaBrowser(timeoutMs = 300_000): Promise<User> {
|
|
201
|
+
if (!isSupabaseAuthConfigured()) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
'Supabase is not configured. Set SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY.',
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return new Promise<User>((resolve, reject) => {
|
|
208
|
+
let settled = false
|
|
209
|
+
const server = http.createServer((req, res) => {
|
|
210
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
211
|
+
if (req.method === 'GET' && url.pathname === '/callback') {
|
|
212
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
|
|
213
|
+
res.end(callbackHtml())
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if (req.method === 'POST' && url.pathname === '/token') {
|
|
217
|
+
let data = ''
|
|
218
|
+
req.on('data', (c) => {
|
|
219
|
+
data += c
|
|
220
|
+
if (data.length > 1_000_000) req.destroy()
|
|
221
|
+
})
|
|
222
|
+
req.on('end', () => {
|
|
223
|
+
try {
|
|
224
|
+
const parsed = JSON.parse(data || '{}') as {
|
|
225
|
+
access_token?: string
|
|
226
|
+
refresh_token?: string
|
|
227
|
+
expires_at?: string
|
|
228
|
+
}
|
|
229
|
+
if (!parsed.access_token) {
|
|
230
|
+
res.writeHead(400)
|
|
231
|
+
res.end('{"error":"no token"}')
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
const claims = decodeJwt(parsed.access_token)
|
|
235
|
+
const user = saveSupabaseSession({
|
|
236
|
+
access_token: parsed.access_token,
|
|
237
|
+
refresh_token: parsed.refresh_token ?? '',
|
|
238
|
+
expires_at: parsed.expires_at ? Number(parsed.expires_at) : undefined,
|
|
239
|
+
user: {
|
|
240
|
+
id: claims.sub,
|
|
241
|
+
email: claims.email,
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
245
|
+
res.end('{"ok":true}')
|
|
246
|
+
finish(() => resolve(user))
|
|
247
|
+
} catch (error) {
|
|
248
|
+
res.writeHead(500)
|
|
249
|
+
res.end('{"error":"bad request"}')
|
|
250
|
+
finish(() =>
|
|
251
|
+
reject(error instanceof Error ? error : new Error(String(error))),
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
res.writeHead(404)
|
|
258
|
+
res.end('not found')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const timer = setTimeout(
|
|
262
|
+
() => finish(() => reject(new Error('Login timed out. Please run anvil login again.'))),
|
|
263
|
+
timeoutMs,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
function finish(then: () => void) {
|
|
267
|
+
if (settled) return
|
|
268
|
+
settled = true
|
|
269
|
+
clearTimeout(timer)
|
|
270
|
+
// Give the browser a beat to receive the 200 before the socket closes.
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
try {
|
|
273
|
+
server.close()
|
|
274
|
+
} catch {
|
|
275
|
+
/* already closed */
|
|
276
|
+
}
|
|
277
|
+
then()
|
|
278
|
+
}, 150)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
server.on('error', (error) => finish(() => reject(error)))
|
|
282
|
+
server.listen(0, '127.0.0.1', () => {
|
|
283
|
+
const address = server.address()
|
|
284
|
+
const port = typeof address === 'object' && address ? address.port : 0
|
|
285
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`
|
|
286
|
+
const loginUrl = `${getLoginPageUrl()}?redirect_uri=${encodeURIComponent(redirectUri)}`
|
|
287
|
+
console.log('\nOpening your browser to sign in…')
|
|
288
|
+
console.log(loginUrl, '\n')
|
|
289
|
+
void open(loginUrl).catch(() => {
|
|
290
|
+
console.log('Could not open a browser automatically — open the URL above.')
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** True when a Supabase session is already stored. */
|
|
297
|
+
export function hasSupabaseSession(): boolean {
|
|
298
|
+
const user = getUserCredentials()
|
|
299
|
+
return Boolean(user?.provider === 'supabase' && user.authToken)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Ensure the user is signed in for managed gateway mode. No-op when a Supabase
|
|
304
|
+
* session already exists. Non-interactive with ANVIL_LOGIN_EMAIL/PASSWORD;
|
|
305
|
+
* otherwise opens the browser sign-in. Throws if sign-in doesn't complete.
|
|
306
|
+
*/
|
|
307
|
+
export async function ensureSignedIn(): Promise<void> {
|
|
308
|
+
if (hasSupabaseSession()) return
|
|
309
|
+
const email = process.env.ANVIL_LOGIN_EMAIL
|
|
310
|
+
const password = process.env.ANVIL_LOGIN_PASSWORD
|
|
311
|
+
if (email && password) {
|
|
312
|
+
await signInWithPassword(email, password)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
await loginViaBrowser()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Seconds of headroom before expiry at which we proactively refresh. */
|
|
319
|
+
const REFRESH_SKEW_SECONDS = 60
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Ensure the stored Supabase access token is still valid, refreshing it when
|
|
323
|
+
* it is within {@link REFRESH_SKEW_SECONDS} of expiry. No-op for non-Supabase
|
|
324
|
+
* credentials (direct/anvil). Returns the current valid access token, or
|
|
325
|
+
* undefined when there is nothing to refresh / refresh failed.
|
|
326
|
+
*/
|
|
327
|
+
export async function ensureFreshAuthToken(): Promise<string | undefined> {
|
|
328
|
+
const user = getUserCredentials()
|
|
329
|
+
if (!user || user.provider !== 'supabase') return user?.authToken
|
|
330
|
+
const now = Math.floor(Date.now() / 1000)
|
|
331
|
+
const notExpiring =
|
|
332
|
+
typeof user.expiresAt === 'number' &&
|
|
333
|
+
user.expiresAt - REFRESH_SKEW_SECONDS > now
|
|
334
|
+
if (notExpiring) return user.authToken
|
|
335
|
+
if (!user.refreshToken) return user.authToken
|
|
336
|
+
try {
|
|
337
|
+
const refreshed = await refreshSupabaseSession(user.refreshToken)
|
|
338
|
+
return refreshed.authToken
|
|
339
|
+
} catch (error) {
|
|
340
|
+
logger.warn(
|
|
341
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
342
|
+
'Supabase token refresh failed; using the existing token',
|
|
343
|
+
)
|
|
344
|
+
return user.authToken
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Apply production defaults for NEXT_PUBLIC_* vars BEFORE anything that
|
|
2
|
+
// imports `@anvil/common/env`, which validates the client env at import time
|
|
3
|
+
// and throws on a machine without an env file (any global npm install).
|
|
4
|
+
//
|
|
5
|
+
// Must be imported before `./index` in `entry.ts`, mirroring the tree-sitter
|
|
6
|
+
// pre-init: both prepare runtime state the eagerly-evaluated import graph
|
|
7
|
+
// needs at module-evaluation time.
|
|
8
|
+
//
|
|
9
|
+
// Gap-filling only: real values already in process.env (from ~/.env.local,
|
|
10
|
+
// the shell, or --define flags baked into compiled binaries) always win.
|
|
11
|
+
import { CLIENT_ENV_DEFAULTS } from '../utils/client-env-defaults'
|
|
12
|
+
|
|
13
|
+
for (const [key, value] of Object.entries(CLIENT_ENV_DEFAULTS)) {
|
|
14
|
+
if (process.env[key] === undefined) {
|
|
15
|
+
process.env[key] = value
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -3,6 +3,7 @@ import { AskUserBridge } from '@anvil/common/utils/ask-user-bridge'
|
|
|
3
3
|
import { AnvilClient } from '@anvil/sdk'
|
|
4
4
|
|
|
5
5
|
import { getAuthTokenDetails } from './auth'
|
|
6
|
+
import { ensureFreshAuthToken } from '../login/supabase-auth'
|
|
6
7
|
import { getCliEnv, getSystemProcessEnv } from './env'
|
|
7
8
|
import { loadAgentDefinitions } from './local-agent-registry'
|
|
8
9
|
import { logger } from './logger'
|
|
@@ -48,6 +49,9 @@ export function resetAnvilClient(): void {
|
|
|
48
49
|
|
|
49
50
|
export async function getAnvilClient(): Promise<AnvilClient | null> {
|
|
50
51
|
if (!clientInstance) {
|
|
52
|
+
// Managed gateway mode: refresh the Supabase access token if it is near
|
|
53
|
+
// expiry before reading it (no-op for direct/anvil credentials).
|
|
54
|
+
await ensureFreshAuthToken()
|
|
51
55
|
const { token: apiKey } = getAuthTokenDetails()
|
|
52
56
|
|
|
53
57
|
if (!apiKey) {
|
package/src/utils/auth.ts
CHANGED
|
@@ -2,7 +2,10 @@ import fs from 'fs'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
|
|
4
4
|
import { getCiEnv } from '@anvil/common/env-ci'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getDirectDeepSeekApiKey,
|
|
7
|
+
isManagedGatewayMode,
|
|
8
|
+
} from '@anvil/common/constants/deepseek-direct'
|
|
6
9
|
import { z } from 'zod'
|
|
7
10
|
|
|
8
11
|
|
|
@@ -21,6 +24,13 @@ const userSchema = z.object({
|
|
|
21
24
|
fingerprintId: z.string().optional(),
|
|
22
25
|
fingerprintHash: z.string().optional(),
|
|
23
26
|
credits: z.number().optional(),
|
|
27
|
+
// Managed gateway mode (Supabase): `authToken` holds the Supabase access
|
|
28
|
+
// token (a JWT the cli-agent gateway verifies). These let the CLI refresh it
|
|
29
|
+
// without a re-login. Optional + additive, so existing credentials still parse.
|
|
30
|
+
provider: z.string().optional(),
|
|
31
|
+
refreshToken: z.string().optional(),
|
|
32
|
+
/** Access-token expiry, epoch seconds. */
|
|
33
|
+
expiresAt: z.number().optional(),
|
|
24
34
|
})
|
|
25
35
|
|
|
26
36
|
export type User = z.infer<typeof userSchema>
|
|
@@ -117,10 +127,12 @@ export const getAuthTokenDetails = (
|
|
|
117
127
|
return { token: envToken, source: 'environment' }
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
// Direct-to-DeepSeek mode: the DeepSeek key doubles as the auth token so
|
|
121
|
-
//
|
|
130
|
+
// Direct-to-DeepSeek mode: the DeepSeek key doubles as the auth token so the
|
|
131
|
+
// CLI treats the user as authenticated without an Anvil backend. In managed
|
|
132
|
+
// gateway mode this does NOT apply — the gateway needs a real Supabase JWT,
|
|
133
|
+
// so a local DeepSeek key must never count as being signed in.
|
|
122
134
|
const deepseekToken = getDirectDeepSeekApiKey()
|
|
123
|
-
if (deepseekToken) {
|
|
135
|
+
if (deepseekToken && !isManagedGatewayMode()) {
|
|
124
136
|
return { token: deepseekToken, source: 'environment' }
|
|
125
137
|
}
|
|
126
138
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production defaults for the NEXT_PUBLIC_* client env vars.
|
|
3
|
+
*
|
|
4
|
+
* `@anvil/common/env` validates all NEXT_PUBLIC_* vars at *import time*, so a
|
|
5
|
+
* global npm install (no .env.local anywhere on the machine) dies before any
|
|
6
|
+
* of our code runs. These defaults — all public-by-design values, the same
|
|
7
|
+
* set `cli/scripts/build-binary.ts` bakes into compiled binaries — are applied
|
|
8
|
+
* to `process.env` at pre-init so the CLI boots on any machine, cmd or
|
|
9
|
+
* PowerShell, with or without an env file.
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: the pre-init loader (`cli/src/pre-init/client-env.ts`) imports
|
|
12
|
+
* this table, and `build-binary.ts` reads it at build time. Keep the two in
|
|
13
|
+
* sync through this module — never duplicate the values.
|
|
14
|
+
*
|
|
15
|
+
* User-set values always win: the loader only fills gaps, it never overwrites
|
|
16
|
+
* anything already in process.env (including values Bun loads from
|
|
17
|
+
* .env.local in dev, and the --define flags baked into binaries).
|
|
18
|
+
*/
|
|
19
|
+
export const CLIENT_ENV_DEFAULTS: Readonly<Record<string, string>> = {
|
|
20
|
+
NEXT_PUBLIC_CB_ENVIRONMENT: 'prod',
|
|
21
|
+
NEXT_PUBLIC_ANVIL_APP_URL: 'https://anvil.dev',
|
|
22
|
+
NEXT_PUBLIC_SUPPORT_EMAIL: 'support@anvil.dev',
|
|
23
|
+
NEXT_PUBLIC_POSTHOG_API_KEY: 'phc_placeholder',
|
|
24
|
+
NEXT_PUBLIC_POSTHOG_HOST_URL: 'https://us.i.posthog.com',
|
|
25
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 'pk_placeholder',
|
|
26
|
+
NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL: 'https://billing.stripe.com/portal',
|
|
27
|
+
NEXT_PUBLIC_WEB_PORT: '3000',
|
|
28
|
+
}
|