@pi-in-go/pigpen-pi-typesafe 0.1.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/CREDITS.md +14 -0
- package/LICENSE +22 -0
- package/README.md +45 -0
- package/extensions/pi-typesafe/branches_test.go +185 -0
- package/extensions/pi-typesafe/command.go +319 -0
- package/extensions/pi-typesafe/export_test.go +9 -0
- package/extensions/pi-typesafe/extension.go +188 -0
- package/extensions/pi-typesafe/extension_test.go +321 -0
- package/extensions/pi-typesafe/fakehost_test.go +548 -0
- package/extensions/pi-typesafe/format.go +191 -0
- package/extensions/pi-typesafe/format_test.go +75 -0
- package/extensions/pi-typesafe/go.mod +10 -0
- package/extensions/pi-typesafe/go.sum +2 -0
- package/extensions/pi-typesafe/go.work +11 -0
- package/extensions/pi-typesafe/harness_test.go +200 -0
- package/extensions/pi-typesafe/ownmodel_test.go +100 -0
- package/extensions/pi-typesafe/review_test.go +134 -0
- package/extensions/pi-typesafe/tool.go +193 -0
- package/extensions/pi-typesafe/twin_test.go +28 -0
- package/libs/pi-typesafe-api/CREDITS.md +14 -0
- package/libs/pi-typesafe-api/LICENSE +22 -0
- package/libs/pi-typesafe-api/README.md +30 -0
- package/libs/pi-typesafe-api/ask.go +62 -0
- package/libs/pi-typesafe-api/ask_test.go +76 -0
- package/libs/pi-typesafe-api/auth.go +249 -0
- package/libs/pi-typesafe-api/auth_test.go +131 -0
- package/libs/pi-typesafe-api/backends.go +336 -0
- package/libs/pi-typesafe-api/backends_test.go +404 -0
- package/libs/pi-typesafe-api/batch.go +202 -0
- package/libs/pi-typesafe-api/batch_test.go +202 -0
- package/libs/pi-typesafe-api/battery_test.go +41 -0
- package/libs/pi-typesafe-api/calibrate.go +354 -0
- package/libs/pi-typesafe-api/calibrate_test.go +186 -0
- package/libs/pi-typesafe-api/client.go +615 -0
- package/libs/pi-typesafe-api/client_test.go +490 -0
- package/libs/pi-typesafe-api/credentials.go +252 -0
- package/libs/pi-typesafe-api/credentials_test.go +216 -0
- package/libs/pi-typesafe-api/doc.go +14 -0
- package/libs/pi-typesafe-api/errors.go +143 -0
- package/libs/pi-typesafe-api/evaluation.go +86 -0
- package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
- package/libs/pi-typesafe-api/gaps_test.go +77 -0
- package/libs/pi-typesafe-api/go.mod +9 -0
- package/libs/pi-typesafe-api/go.sum +2 -0
- package/libs/pi-typesafe-api/helpers_test.go +169 -0
- package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
- package/libs/pi-typesafe-api/json.go +299 -0
- package/libs/pi-typesafe-api/json_test.go +92 -0
- package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
- package/libs/pi-typesafe-api/package.json +40 -0
- package/libs/pi-typesafe-api/provenance.json +18 -0
- package/libs/pi-typesafe-api/review_test.go +23 -0
- package/libs/pi-typesafe-api/schema.go +473 -0
- package/libs/pi-typesafe-api/schema_test.go +262 -0
- package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
- package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
- package/libs/pi-typesafe-api/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
- package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
- package/libs/pi-typesafe-api/ui/login.go +106 -0
- package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
- package/libs/pi-typesafe-api/usage.go +366 -0
- package/libs/pi-typesafe-api/usage_test.go +139 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +98 -0
- package/port/accepted-gaps.json +3 -0
- package/port/golden/enable-confirm.jsonl +11 -0
- package/port/golden/enable-decline.jsonl +20 -0
- package/port/golden/enable-missing-key.jsonl +4 -0
- package/port/golden/login-shadow.jsonl +4 -0
- package/port/golden/logout-env-key.jsonl +6 -0
- package/port/golden/playground-cancel.jsonl +4 -0
- package/port/golden/playground-invalid-json.jsonl +5 -0
- package/port/golden/playground-invalid-questions.jsonl +5 -0
- package/port/golden/status-env-key.jsonl +6 -0
- package/port/golden/status-no-key.jsonl +6 -0
- package/port/golden/tool-disabled.jsonl +18 -0
- package/port/golden/trailing-words.jsonl +10 -0
- package/port/library-mutations.py +44 -0
- package/port/mutations.json +302 -0
- package/port/oracle/.env.example +4 -0
- package/port/oracle/CHANGELOG.md +91 -0
- package/port/oracle/CONTRIBUTING.md +35 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +159 -0
- package/port/oracle/docs/api.md +143 -0
- package/port/oracle/docs/ci-cd.md +97 -0
- package/port/oracle/examples/decision-extension.ts +41 -0
- package/port/oracle/extensions/index.js +2 -0
- package/port/oracle/package.json +89 -0
- package/port/oracle/scripts/dev-pi.mjs +23 -0
- package/port/oracle/scripts/live-smoke.mjs +35 -0
- package/port/oracle/src/ask.ts +42 -0
- package/port/oracle/src/auth.ts +171 -0
- package/port/oracle/src/backends.ts +196 -0
- package/port/oracle/src/batch.ts +170 -0
- package/port/oracle/src/calibrate.ts +237 -0
- package/port/oracle/src/client.ts +310 -0
- package/port/oracle/src/credentials.ts +136 -0
- package/port/oracle/src/errors.ts +53 -0
- package/port/oracle/src/extension.ts +204 -0
- package/port/oracle/src/index.ts +31 -0
- package/port/oracle/src/key-prompt.ts +51 -0
- package/port/oracle/src/login.ts +60 -0
- package/port/oracle/src/schema.ts +158 -0
- package/port/oracle/src/ui.ts +4 -0
- package/port/oracle/src/usage.ts +258 -0
- package/port/oracle/tests/ask.test.ts +63 -0
- package/port/oracle/tests/auth.test.ts +141 -0
- package/port/oracle/tests/backends.test.ts +380 -0
- package/port/oracle/tests/batch.test.ts +156 -0
- package/port/oracle/tests/calibrate.test.ts +144 -0
- package/port/oracle/tests/client.test.ts +499 -0
- package/port/oracle/tests/credentials.test.ts +144 -0
- package/port/oracle/tests/extension.test.ts +276 -0
- package/port/oracle/tests/key-prompt.test.ts +47 -0
- package/port/oracle/tests/login.test.ts +101 -0
- package/port/oracle/tests/schema.test.ts +85 -0
- package/port/oracle/tests/usage.test.ts +106 -0
- package/port/oracle/tsconfig.build.json +10 -0
- package/port/oracle/tsconfig.json +14 -0
- package/port/scenarios/enable-confirm.json +5 -0
- package/port/scenarios/enable-decline.json +3 -0
- package/port/scenarios/enable-missing-key.json +2 -0
- package/port/scenarios/login-shadow.json +2 -0
- package/port/scenarios/logout-env-key.json +3 -0
- package/port/scenarios/playground-cancel.json +2 -0
- package/port/scenarios/playground-invalid-json.json +2 -0
- package/port/scenarios/playground-invalid-questions.json +2 -0
- package/port/scenarios/status-env-key.json +3 -0
- package/port/scenarios/status-no-key.json +3 -0
- package/port/scenarios/tool-disabled.json +2 -0
- package/port/scenarios/trailing-words.json +5 -0
- package/port/upstream-tests.json +160 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"time"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const authVersion = 1
|
|
11
|
+
|
|
12
|
+
// AuthFailure is the last request that degraded TypeSafe, with no upstream body, header, key, or submitted state.
|
|
13
|
+
type AuthFailure struct {
|
|
14
|
+
Code ErrorCode `json:"code"`
|
|
15
|
+
Status int `json:"status,omitempty"`
|
|
16
|
+
Message string `json:"message"`
|
|
17
|
+
At string `json:"at"`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// AuthState is the whole answer to "is Jev actually available right now": which key is in effect, whether it
|
|
21
|
+
// has been accepted, and the last failure that degraded it. Consumers must consult it instead of treating their
|
|
22
|
+
// own consent flag as proof that judgments will happen.
|
|
23
|
+
type AuthState struct {
|
|
24
|
+
// Backend is the value the caller passed (nil means the default).
|
|
25
|
+
Backend any
|
|
26
|
+
Kind KeyKind
|
|
27
|
+
Source KeySource
|
|
28
|
+
// Path is where the key would be read from.
|
|
29
|
+
Path string
|
|
30
|
+
// Reason says why a stored key cannot be used.
|
|
31
|
+
Reason string
|
|
32
|
+
// KeyName is a short label for the key source: TYPESAFE_API_KEY, /typesafe login, no key, unusable key.
|
|
33
|
+
KeyName string
|
|
34
|
+
// Verified is true when the key in effect was accepted by the backend.
|
|
35
|
+
Verified bool
|
|
36
|
+
VerifiedAt string
|
|
37
|
+
// LastFailure is cleared by the next successful request.
|
|
38
|
+
LastFailure *AuthFailure
|
|
39
|
+
// Usable means a key is present and the last authentication outcome was not a rejection. False means judgments are skipped.
|
|
40
|
+
Usable bool
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// AuthOptions selects the state file and backend.
|
|
44
|
+
type AuthOptions struct {
|
|
45
|
+
Path string
|
|
46
|
+
Backend any
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// AuthStatePath is the auth-state file: one small, owner-only record that outlives the process that wrote it.
|
|
50
|
+
func AuthStatePath() string { return filepath.Join(TypeSafeDir(), "auth-state.json") }
|
|
51
|
+
|
|
52
|
+
type storedAuth struct {
|
|
53
|
+
Version int `json:"version"`
|
|
54
|
+
VerifiedAt string `json:"verifiedAt,omitempty"`
|
|
55
|
+
LastFailure *AuthFailure `json:"lastFailure,omitempty"`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var errorCodes = map[ErrorCode]bool{CodeConfiguration: true, CodeValidation: true, CodeBudget: true, CodeAborted: true, CodeTimeout: true, CodeHTTP: true, CodeConnection: true, CodeResponse: true}
|
|
59
|
+
|
|
60
|
+
func readAuthState(path string) storedAuth {
|
|
61
|
+
data, err := os.ReadFile(path)
|
|
62
|
+
if err != nil {
|
|
63
|
+
return storedAuth{}
|
|
64
|
+
}
|
|
65
|
+
var raw struct {
|
|
66
|
+
VerifiedAt any `json:"verifiedAt"`
|
|
67
|
+
LastFailure *struct {
|
|
68
|
+
Code any `json:"code"`
|
|
69
|
+
Status any `json:"status"`
|
|
70
|
+
Message any `json:"message"`
|
|
71
|
+
At any `json:"at"`
|
|
72
|
+
} `json:"lastFailure"`
|
|
73
|
+
}
|
|
74
|
+
if json.Unmarshal(data, &raw) != nil {
|
|
75
|
+
return storedAuth{}
|
|
76
|
+
}
|
|
77
|
+
var out storedAuth
|
|
78
|
+
if v, ok := raw.VerifiedAt.(string); ok && utf16Len(v) <= 40 {
|
|
79
|
+
out.VerifiedAt = v
|
|
80
|
+
}
|
|
81
|
+
if f := raw.LastFailure; f != nil {
|
|
82
|
+
code, _ := f.Code.(string)
|
|
83
|
+
message, _ := f.Message.(string)
|
|
84
|
+
at, _ := f.At.(string)
|
|
85
|
+
if errorCodes[ErrorCode(code)] && message != "" && at != "" && utf16Len(at) <= 40 {
|
|
86
|
+
failure := &AuthFailure{Code: ErrorCode(code), Message: truncateUTF16(message, 300), At: at}
|
|
87
|
+
if s, ok := f.Status.(float64); ok && s == float64(int(s)) {
|
|
88
|
+
failure.Status = int(s)
|
|
89
|
+
}
|
|
90
|
+
out.LastFailure = failure
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func truncateUTF16(s string, n int) string {
|
|
97
|
+
if utf16Len(s) <= n {
|
|
98
|
+
return s
|
|
99
|
+
}
|
|
100
|
+
count := 0
|
|
101
|
+
for i, r := range s {
|
|
102
|
+
w := 1
|
|
103
|
+
if r >= 0x10000 {
|
|
104
|
+
w = 2
|
|
105
|
+
}
|
|
106
|
+
if count+w > n {
|
|
107
|
+
return s[:i]
|
|
108
|
+
}
|
|
109
|
+
count += w
|
|
110
|
+
}
|
|
111
|
+
return s
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// writeAuthState is owner-only, atomic, and best-effort: an unwritable auth record never changes how a request behaves.
|
|
115
|
+
func writeAuthState(path string, state storedAuth) {
|
|
116
|
+
state.Version = authVersion
|
|
117
|
+
body, err := json.MarshalIndent(state, "", " ")
|
|
118
|
+
if err != nil {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
_ = writeOwnerOnly(path, append(body, '\n'))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func rejected(f *AuthFailure) bool {
|
|
125
|
+
return f != nil && f.Code == CodeHTTP && (f.Status == 401 || f.Status == 403)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// GetAuthState adds the key situation, the last outcome, and the clock up for a valid backend. An invalid
|
|
129
|
+
// backend returns the same configuration error as ResolveBackend, so validate a user-supplied endpoint with
|
|
130
|
+
// ResolveBackend first.
|
|
131
|
+
func GetAuthState(opts AuthOptions) (AuthState, error) {
|
|
132
|
+
path := opts.Path
|
|
133
|
+
if path == "" {
|
|
134
|
+
path = AuthStatePath()
|
|
135
|
+
}
|
|
136
|
+
backend := opts.Backend
|
|
137
|
+
if backend == nil {
|
|
138
|
+
backend = DefaultBackend
|
|
139
|
+
}
|
|
140
|
+
situation, err := KeySituationFor(backend)
|
|
141
|
+
if err != nil {
|
|
142
|
+
return AuthState{}, err
|
|
143
|
+
}
|
|
144
|
+
stored := readAuthState(path)
|
|
145
|
+
var source KeySource
|
|
146
|
+
switch situation.Kind {
|
|
147
|
+
case KeyEnvironment:
|
|
148
|
+
source = SourceEnvironment
|
|
149
|
+
case KeyStored:
|
|
150
|
+
source = SourceStored
|
|
151
|
+
}
|
|
152
|
+
isRejected := rejected(stored.LastFailure)
|
|
153
|
+
present := source != "" || situation.Kind == KeyNotRequired
|
|
154
|
+
state := AuthState{
|
|
155
|
+
Backend: backend,
|
|
156
|
+
Kind: situation.Kind,
|
|
157
|
+
Source: source,
|
|
158
|
+
Path: CredentialsPath(),
|
|
159
|
+
KeyName: KeySourceLabel(situation),
|
|
160
|
+
Verified: stored.VerifiedAt != "" && !isRejected,
|
|
161
|
+
VerifiedAt: stored.VerifiedAt,
|
|
162
|
+
LastFailure: stored.LastFailure,
|
|
163
|
+
Usable: present && !isRejected,
|
|
164
|
+
}
|
|
165
|
+
if situation.Kind == KeyUnusable {
|
|
166
|
+
state.Path = situation.Path
|
|
167
|
+
state.Reason = situation.Reason
|
|
168
|
+
}
|
|
169
|
+
return state, nil
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// RecordAuthVerified records that the key was accepted: login verification, or any successful request. It clears the last failure.
|
|
173
|
+
func RecordAuthVerified(at time.Time) {
|
|
174
|
+
writeAuthState(AuthStatePath(), storedAuth{VerifiedAt: isoTime(at)})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// RecordAuthFailure records the failure that degraded TypeSafe. The verification timestamp is kept so a recovered key stays known.
|
|
178
|
+
func RecordAuthFailure(err *IntegrationError, at time.Time) {
|
|
179
|
+
current := readAuthState(AuthStatePath())
|
|
180
|
+
writeAuthState(AuthStatePath(), storedAuth{
|
|
181
|
+
VerifiedAt: current.VerifiedAt,
|
|
182
|
+
LastFailure: &AuthFailure{Code: err.Code, Status: err.Status, Message: err.Message, At: isoTime(at)},
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ClearAuthState forgets verification and degradation: used when the key itself changes (login or logout).
|
|
187
|
+
func ClearAuthState() { _ = os.Remove(AuthStatePath()) }
|
|
188
|
+
|
|
189
|
+
func isoTime(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }
|
|
190
|
+
|
|
191
|
+
// AuthLevel is the severity of an AuthReport.
|
|
192
|
+
type AuthLevel string
|
|
193
|
+
|
|
194
|
+
// The levels: error when judgments are skipped or were rejected, warning when the key is unverified, else ok.
|
|
195
|
+
const (
|
|
196
|
+
LevelOK AuthLevel = "ok"
|
|
197
|
+
LevelWarning AuthLevel = "warning"
|
|
198
|
+
LevelError AuthLevel = "error"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// AuthReport is one line plus a level, so a status command, a headless log, or a consumer's own status line
|
|
202
|
+
// can call out a degraded state instead of reporting "enabled".
|
|
203
|
+
type AuthReport struct {
|
|
204
|
+
Level AuthLevel
|
|
205
|
+
Text string
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// DescribeAuth turns a state into a level and one safe line.
|
|
209
|
+
func DescribeAuth(state AuthState) (AuthReport, error) {
|
|
210
|
+
config, err := ResolveBackend(state.Backend)
|
|
211
|
+
if err != nil {
|
|
212
|
+
return AuthReport{}, err
|
|
213
|
+
}
|
|
214
|
+
label := config.Label + " key"
|
|
215
|
+
since := ""
|
|
216
|
+
if f := state.LastFailure; f != nil {
|
|
217
|
+
since = " Last failure: " + f.Message
|
|
218
|
+
if f.At != "" {
|
|
219
|
+
since += " (" + f.At + ")"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
switch state.Kind {
|
|
223
|
+
case KeyNotRequired:
|
|
224
|
+
return AuthReport{LevelOK, config.Label + ": no key needed; judgments use the model PiG is configured with." + since}, nil
|
|
225
|
+
case KeyMissing:
|
|
226
|
+
how := config.KeyEnv + " is set in the environment"
|
|
227
|
+
if UsesTypeSafeKey(config.BackendConfig) {
|
|
228
|
+
how = "a key is configured (/typesafe login or " + typesafeKeyEnv + ")"
|
|
229
|
+
}
|
|
230
|
+
return AuthReport{LevelError, label + ": missing — every Jev judgment is skipped until " + how + "." + since}, nil
|
|
231
|
+
case KeyUnusable:
|
|
232
|
+
reason := state.Reason
|
|
233
|
+
if reason == "" {
|
|
234
|
+
reason = "unknown reason"
|
|
235
|
+
}
|
|
236
|
+
return AuthReport{LevelError, label + ": unusable (" + reason + ") — judgments are skipped until the key is fixed." + since}, nil
|
|
237
|
+
}
|
|
238
|
+
if rejected(state.LastFailure) {
|
|
239
|
+
return AuthReport{LevelError, label + ": " + state.KeyName + " was rejected." + since}, nil
|
|
240
|
+
}
|
|
241
|
+
if !state.Verified {
|
|
242
|
+
return AuthReport{LevelWarning, label + ": " + state.KeyName + " (not verified yet — the first request proves it)." + since}, nil
|
|
243
|
+
}
|
|
244
|
+
verified := "verified"
|
|
245
|
+
if state.VerifiedAt != "" {
|
|
246
|
+
verified += " " + state.VerifiedAt
|
|
247
|
+
}
|
|
248
|
+
return AuthReport{LevelOK, label + ": " + state.KeyName + " (" + verified + ")." + since}, nil
|
|
249
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"runtime"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
"time"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func mustAuth(t *testing.T, backend any) AuthState {
|
|
12
|
+
t.Helper()
|
|
13
|
+
s, err := GetAuthState(AuthOptions{Backend: backend})
|
|
14
|
+
if err != nil {
|
|
15
|
+
t.Fatal(err)
|
|
16
|
+
}
|
|
17
|
+
return s
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func mustDescribe(t *testing.T, s AuthState) AuthReport {
|
|
21
|
+
t.Helper()
|
|
22
|
+
r, err := DescribeAuth(s)
|
|
23
|
+
if err != nil {
|
|
24
|
+
t.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
return r
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestAuth(t *testing.T) {
|
|
30
|
+
tw(t, "auth", "no key at all is an error the consumer cannot mistake for a working setup", func(t *testing.T) {
|
|
31
|
+
isolate(t)
|
|
32
|
+
s := mustAuth(t, nil)
|
|
33
|
+
if s.Kind != KeyMissing || s.KeyName != "no key" || s.Usable || s.Verified {
|
|
34
|
+
t.Fatalf("state = %+v", s)
|
|
35
|
+
}
|
|
36
|
+
r := mustDescribe(t, s)
|
|
37
|
+
if r.Level != LevelError || !strings.Contains(r.Text, "every Jev judgment is skipped") {
|
|
38
|
+
t.Fatalf("report = %+v", r)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
tw(t, "auth", "another backend reports its own key and never the TypeSafe login hint", func(t *testing.T) {
|
|
42
|
+
isolate(t)
|
|
43
|
+
t.Setenv("TYPESAFE_API_KEY", "env-key-0123456789abcdef")
|
|
44
|
+
if mustAuth(t, nil).Backend != "typesafe" {
|
|
45
|
+
t.Error("the default backend is typesafe")
|
|
46
|
+
}
|
|
47
|
+
missing := mustAuth(t, "openrouter")
|
|
48
|
+
if missing.Backend != "openrouter" || missing.Kind != KeyMissing || missing.Usable {
|
|
49
|
+
t.Fatalf("missing = %+v", missing)
|
|
50
|
+
}
|
|
51
|
+
r := mustDescribe(t, missing)
|
|
52
|
+
if r.Level != LevelError || !strings.HasPrefix(r.Text, "OpenRouter key: missing") || !strings.Contains(r.Text, "OPENROUTER_API_KEY is set") || strings.Contains(r.Text, "/typesafe login") {
|
|
53
|
+
t.Fatalf("report = %+v", r)
|
|
54
|
+
}
|
|
55
|
+
t.Setenv("OPENROUTER_API_KEY", "sk-or-0123456789abcdef")
|
|
56
|
+
present := mustAuth(t, "openrouter")
|
|
57
|
+
if present.Kind != KeyEnvironment || present.KeyName != "OPENROUTER_API_KEY" || !present.Usable || !strings.HasPrefix(mustDescribe(t, present).Text, "OpenRouter key: OPENROUTER_API_KEY") {
|
|
58
|
+
t.Fatalf("present = %+v", present)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
tw(t, "auth", "an environment key is usable but unverified until something proves it", func(t *testing.T) {
|
|
62
|
+
isolate(t)
|
|
63
|
+
t.Setenv("TYPESAFE_API_KEY", "env-key-0123456789abcdef")
|
|
64
|
+
before := mustDescribe(t, mustAuth(t, nil))
|
|
65
|
+
if before.Level != LevelWarning || !strings.Contains(before.Text, "TYPESAFE_API_KEY") || !strings.Contains(before.Text, "not verified yet") {
|
|
66
|
+
t.Fatalf("before = %+v", before)
|
|
67
|
+
}
|
|
68
|
+
RecordAuthVerified(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
69
|
+
s := mustAuth(t, nil)
|
|
70
|
+
if !s.Usable || !s.Verified || s.VerifiedAt != "2026-01-01T00:00:00.000Z" || mustDescribe(t, s).Level != LevelOK {
|
|
71
|
+
t.Fatalf("state = %+v", s)
|
|
72
|
+
}
|
|
73
|
+
if info, _ := os.Stat(AuthStatePath()); runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
|
|
74
|
+
t.Errorf("mode = %v", info.Mode().Perm())
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
tw(t, "auth", "a rejected key degrades loudly and a later success clears it", func(t *testing.T) {
|
|
78
|
+
isolate(t)
|
|
79
|
+
_, _ = StoreAPIKey("stored-key-0123456789abcdef")
|
|
80
|
+
RecordAuthVerified(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC))
|
|
81
|
+
msg := "TypeSafe returned HTTP 401. Check TYPESAFE_API_KEY. No automatic retry was made."
|
|
82
|
+
RecordAuthFailure(&IntegrationError{Code: CodeHTTP, Message: msg, Status: 401}, time.Date(2026, 1, 2, 1, 0, 0, 0, time.UTC))
|
|
83
|
+
d := mustAuth(t, nil)
|
|
84
|
+
want := AuthFailure{Code: CodeHTTP, Status: 401, Message: msg, At: "2026-01-02T01:00:00.000Z"}
|
|
85
|
+
if d.Kind != KeyStored || d.Usable || d.Verified || d.LastFailure == nil || *d.LastFailure != want {
|
|
86
|
+
t.Fatalf("degraded = %+v", d)
|
|
87
|
+
}
|
|
88
|
+
if r := mustDescribe(t, d); r.Level != LevelError || !strings.Contains(r.Text, "was rejected") {
|
|
89
|
+
t.Fatalf("report = %+v", r)
|
|
90
|
+
}
|
|
91
|
+
// A successful request proves the key again and drops the failure.
|
|
92
|
+
RecordAuthVerified(time.Date(2026, 1, 2, 2, 0, 0, 0, time.UTC))
|
|
93
|
+
if r := mustAuth(t, nil); !r.Usable || !r.Verified || r.LastFailure != nil {
|
|
94
|
+
t.Fatalf("recovered = %+v", r)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
tw(t, "auth", "a non-authentication failure degrades the signal without pretending the key is gone", func(t *testing.T) {
|
|
98
|
+
isolate(t)
|
|
99
|
+
_, _ = StoreAPIKey("stored-key-0123456789abcdef")
|
|
100
|
+
RecordAuthVerified(time.Date(2026, 1, 3, 0, 0, 0, 0, time.UTC))
|
|
101
|
+
RecordAuthFailure(&IntegrationError{Code: CodeTimeout, Message: "TypeSafe request timed out; it was not retried and may still be billed."}, time.Date(2026, 1, 3, 0, 5, 0, 0, time.UTC))
|
|
102
|
+
s := mustAuth(t, nil)
|
|
103
|
+
r := mustDescribe(t, s)
|
|
104
|
+
if !s.Usable || !s.Verified || r.Level != LevelOK || !strings.Contains(r.Text, "Last failure") {
|
|
105
|
+
t.Fatalf("state = %+v report = %+v", s, r)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
tw(t, "auth", "a stored key readable by other users is reported as unusable, never as enabled", func(t *testing.T) {
|
|
109
|
+
if runtime.GOOS == "windows" {
|
|
110
|
+
t.Skip("POSIX permission bits")
|
|
111
|
+
}
|
|
112
|
+
isolate(t)
|
|
113
|
+
writeFile(t, CredentialsPath(), `{"apiKey":"stored-key-0123456789abcdef"}`, 0o644)
|
|
114
|
+
s := mustAuth(t, nil)
|
|
115
|
+
if s.Kind != KeyUnusable || s.Usable || s.Reason == "" || mustDescribe(t, s).Level != LevelError {
|
|
116
|
+
t.Fatalf("state = %+v", s)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
tw(t, "auth", "a corrupt auth record is ignored and clearing forgets both facts", func(t *testing.T) {
|
|
120
|
+
isolate(t)
|
|
121
|
+
writeFile(t, AuthStatePath(), "{ not json", 0o600)
|
|
122
|
+
if s := mustAuth(t, nil); s.Verified || s.LastFailure != nil {
|
|
123
|
+
t.Fatalf("corrupt = %+v", s)
|
|
124
|
+
}
|
|
125
|
+
RecordAuthVerified(time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC))
|
|
126
|
+
ClearAuthState()
|
|
127
|
+
if s := mustAuth(t, nil); s.Verified || s.LastFailure != nil {
|
|
128
|
+
t.Fatalf("cleared = %+v", s)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|