dsh-coding-subscription-oauth 0.5.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/CHANGELOG.md +136 -0
- package/CONTRIBUTING.md +109 -0
- package/INSTALL.md +220 -0
- package/LICENSE +19 -0
- package/NOTICE +11 -0
- package/README.de.md +254 -0
- package/README.es.md +254 -0
- package/README.fr.md +254 -0
- package/README.ja.md +254 -0
- package/README.ko.md +254 -0
- package/README.md +286 -0
- package/README.pt-BR.md +254 -0
- package/README.ru.md +254 -0
- package/README.zh-CN.md +271 -0
- package/cordis.patch.yml +13 -0
- package/docs/00-project-rules.md +195 -0
- package/docs/02-architecture.md +132 -0
- package/docs/02-architecture.zh-CN.md +128 -0
- package/lib/adapter.d.ts +24 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/alias-adapter.d.ts +43 -0
- package/lib/alias-adapter.d.ts.map +1 -0
- package/lib/auth-routes.d.ts +155 -0
- package/lib/auth-routes.d.ts.map +1 -0
- package/lib/auth.d.ts +29 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/bin.d.ts +6 -0
- package/lib/bin.d.ts.map +1 -0
- package/lib/bin.js +27591 -0
- package/lib/bin.js.map +7 -0
- package/lib/capability-routes.d.ts +43 -0
- package/lib/capability-routes.d.ts.map +1 -0
- package/lib/capability-runtime.d.ts +56 -0
- package/lib/capability-runtime.d.ts.map +1 -0
- package/lib/capability-settings.d.ts +263 -0
- package/lib/capability-settings.d.ts.map +1 -0
- package/lib/capability-tools.d.ts +50 -0
- package/lib/capability-tools.d.ts.map +1 -0
- package/lib/catalog.d.ts +53 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/client.js +3 -0
- package/lib/client.js.map +7 -0
- package/lib/codex-http.d.ts +77 -0
- package/lib/codex-http.d.ts.map +1 -0
- package/lib/codex-images.d.ts +109 -0
- package/lib/codex-images.d.ts.map +1 -0
- package/lib/codex-model-capabilities.d.ts +112 -0
- package/lib/codex-model-capabilities.d.ts.map +1 -0
- package/lib/codex-search.d.ts +96 -0
- package/lib/codex-search.d.ts.map +1 -0
- package/lib/codex-usage.d.ts +79 -0
- package/lib/codex-usage.d.ts.map +1 -0
- package/lib/gateway-anthropic-messages.d.ts +8 -0
- package/lib/gateway-anthropic-messages.d.ts.map +1 -0
- package/lib/gateway-auth.d.ts +22 -0
- package/lib/gateway-auth.d.ts.map +1 -0
- package/lib/gateway-backend.d.ts +62 -0
- package/lib/gateway-backend.d.ts.map +1 -0
- package/lib/gateway-body.d.ts +9 -0
- package/lib/gateway-body.d.ts.map +1 -0
- package/lib/gateway-config.d.ts +24 -0
- package/lib/gateway-config.d.ts.map +1 -0
- package/lib/gateway-http.d.ts +16 -0
- package/lib/gateway-http.d.ts.map +1 -0
- package/lib/gateway-openai-chat.d.ts +8 -0
- package/lib/gateway-openai-chat.d.ts.map +1 -0
- package/lib/gateway-openai-responses.d.ts +8 -0
- package/lib/gateway-openai-responses.d.ts.map +1 -0
- package/lib/gateway-parse.d.ts +10 -0
- package/lib/gateway-parse.d.ts.map +1 -0
- package/lib/gateway-protocol.d.ts +47 -0
- package/lib/gateway-protocol.d.ts.map +1 -0
- package/lib/gateway-routes.d.ts +21 -0
- package/lib/gateway-routes.d.ts.map +1 -0
- package/lib/gateway.d.ts +48 -0
- package/lib/gateway.d.ts.map +1 -0
- package/lib/grok-imagine.d.ts +271 -0
- package/lib/grok-imagine.d.ts.map +1 -0
- package/lib/grok-import.d.ts +21 -0
- package/lib/grok-import.d.ts.map +1 -0
- package/lib/http-json.d.ts +10 -0
- package/lib/http-json.d.ts.map +1 -0
- package/lib/ids.d.ts +33 -0
- package/lib/ids.d.ts.map +1 -0
- package/lib/imagine-routes.d.ts +59 -0
- package/lib/imagine-routes.d.ts.map +1 -0
- package/lib/index.d.ts +69 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +35355 -0
- package/lib/index.js.map +7 -0
- package/lib/invariant.d.ts +9 -0
- package/lib/invariant.d.ts.map +1 -0
- package/lib/invariant.js +14 -0
- package/lib/invariant.js.map +7 -0
- package/lib/kimi-errors.d.ts +13 -0
- package/lib/kimi-errors.d.ts.map +1 -0
- package/lib/media-store.d.ts +130 -0
- package/lib/media-store.d.ts.map +1 -0
- package/lib/oauth-import-routes.d.ts +52 -0
- package/lib/oauth-import-routes.d.ts.map +1 -0
- package/lib/oauth-providers.d.ts +26 -0
- package/lib/oauth-providers.d.ts.map +1 -0
- package/lib/oauth-session.d.ts +40 -0
- package/lib/oauth-session.d.ts.map +1 -0
- package/lib/oauth-sources.d.ts +205 -0
- package/lib/oauth-sources.d.ts.map +1 -0
- package/lib/oauth.d.ts +79 -0
- package/lib/oauth.d.ts.map +1 -0
- package/lib/provider.d.ts +38 -0
- package/lib/provider.d.ts.map +1 -0
- package/lib/proxy.d.ts +17 -0
- package/lib/proxy.d.ts.map +1 -0
- package/lib/redact.d.ts +5 -0
- package/lib/redact.d.ts.map +1 -0
- package/lib/session.d.ts +40 -0
- package/lib/session.d.ts.map +1 -0
- package/lib/store.d.ts +46 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/web-origin.d.ts +10 -0
- package/lib/web-origin.d.ts.map +1 -0
- package/lib/web-routes.d.ts +20 -0
- package/lib/web-routes.d.ts.map +1 -0
- package/package.json +185 -0
- package/patches/dsh-agy@0.1.2.patch +25 -0
- package/scripts/release.mjs +166 -0
- package/scripts/smoke-deployed-routes.mjs +146 -0
- package/scripts/verify-deployed-catalog.mjs +87 -0
- package/src/adapter.ts +282 -0
- package/src/alias-adapter.ts +152 -0
- package/src/auth-routes.ts +871 -0
- package/src/auth.ts +67 -0
- package/src/bin.ts +350 -0
- package/src/capability-routes.ts +275 -0
- package/src/capability-runtime.ts +313 -0
- package/src/capability-settings.ts +657 -0
- package/src/capability-tools.ts +666 -0
- package/src/catalog.ts +271 -0
- package/src/client/GrokBuildSettings.tsx +2221 -0
- package/src/client/index.tsx +37 -0
- package/src/client/locales.ts +421 -0
- package/src/codex-http.ts +447 -0
- package/src/codex-images.ts +485 -0
- package/src/codex-model-capabilities.ts +320 -0
- package/src/codex-search.ts +245 -0
- package/src/codex-usage.ts +263 -0
- package/src/gateway-anthropic-messages.ts +84 -0
- package/src/gateway-auth.ts +100 -0
- package/src/gateway-backend.ts +274 -0
- package/src/gateway-body.ts +49 -0
- package/src/gateway-config.ts +76 -0
- package/src/gateway-http.ts +104 -0
- package/src/gateway-openai-chat.ts +124 -0
- package/src/gateway-openai-responses.ts +53 -0
- package/src/gateway-parse.ts +224 -0
- package/src/gateway-protocol.ts +52 -0
- package/src/gateway-routes.ts +152 -0
- package/src/gateway.ts +242 -0
- package/src/grok-imagine.ts +1627 -0
- package/src/grok-import.ts +151 -0
- package/src/http-json.ts +82 -0
- package/src/ids.ts +45 -0
- package/src/imagine-routes.ts +461 -0
- package/src/index.ts +598 -0
- package/src/invariant.ts +17 -0
- package/src/kimi-errors.ts +26 -0
- package/src/media-store.ts +927 -0
- package/src/oauth-import-routes.ts +314 -0
- package/src/oauth-providers.ts +152 -0
- package/src/oauth-session.ts +183 -0
- package/src/oauth-sources.ts +1104 -0
- package/src/oauth.ts +620 -0
- package/src/provider.ts +128 -0
- package/src/proxy.ts +99 -0
- package/src/redact.ts +72 -0
- package/src/session.ts +218 -0
- package/src/store.ts +217 -0
- package/src/web-origin.ts +60 -0
- package/src/web-routes.ts +75 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration-ready capability settings controller for the
|
|
5
|
+
* `coding-subscription-oauth` namespace. Schema defaults sit under the
|
|
6
|
+
* composition/YAML `base`, and the user section layers on top. The controller
|
|
7
|
+
* talks to an injected structural settings service while registering a real
|
|
8
|
+
* Schemastery section that the Host settings service can render and validate.
|
|
9
|
+
* @module dsh-coding-subscription-oauth/capability-settings
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Settings namespace owned by this plugin. */
|
|
13
|
+
export const CAPABILITY_SETTINGS_NAMESPACE = "coding-subscription-oauth";
|
|
14
|
+
|
|
15
|
+
/** Default-off capability flags. Presence in the user section marks an override. */
|
|
16
|
+
export const CAPABILITY_FLAG_KEYS = [
|
|
17
|
+
"codexSearch",
|
|
18
|
+
"codexImages",
|
|
19
|
+
"codexImageEdits",
|
|
20
|
+
"codexUsage",
|
|
21
|
+
"codexFast",
|
|
22
|
+
"grokImagineImage",
|
|
23
|
+
"grokImagineVideo",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
/** Conservative numeric limits persisted beside the flags. */
|
|
27
|
+
export const CAPABILITY_LIMIT_KEYS = ["searchResults", "imageCount", "videoArtifactTtlMs"] as const;
|
|
28
|
+
|
|
29
|
+
/** Every key the controller admits into secret-free state. */
|
|
30
|
+
export const CAPABILITY_SETTINGS_KEYS = [...CAPABILITY_FLAG_KEYS, ...CAPABILITY_LIMIT_KEYS] as const;
|
|
31
|
+
|
|
32
|
+
export type CapabilityFlagKey = (typeof CAPABILITY_FLAG_KEYS)[number];
|
|
33
|
+
export type CapabilityLimitKey = (typeof CAPABILITY_LIMIT_KEYS)[number];
|
|
34
|
+
export type CapabilitySettingsKey = (typeof CAPABILITY_SETTINGS_KEYS)[number];
|
|
35
|
+
|
|
36
|
+
/** Resolved, secret-free capability section. */
|
|
37
|
+
export interface CapabilitySettings {
|
|
38
|
+
readonly codexSearch: boolean;
|
|
39
|
+
readonly codexImages: boolean;
|
|
40
|
+
readonly codexImageEdits: boolean;
|
|
41
|
+
readonly codexUsage: boolean;
|
|
42
|
+
readonly codexFast: boolean;
|
|
43
|
+
readonly grokImagineImage: boolean;
|
|
44
|
+
readonly grokImagineVideo: boolean;
|
|
45
|
+
readonly searchResults: number;
|
|
46
|
+
readonly imageCount: number;
|
|
47
|
+
readonly videoArtifactTtlMs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Sparse overlay used for YAML/composition `base` and the user section. */
|
|
51
|
+
export type CapabilitySettingsPatch = Partial<CapabilitySettings>;
|
|
52
|
+
|
|
53
|
+
/** Inclusive bounds and schema defaults for each numeric limit. */
|
|
54
|
+
export const CAPABILITY_SETTINGS_BOUNDS = {
|
|
55
|
+
searchResults: { min: 1, max: 20, default: 5 },
|
|
56
|
+
imageCount: { min: 1, max: 4, default: 1 },
|
|
57
|
+
videoArtifactTtlMs: {
|
|
58
|
+
min: 60 * 60 * 1000,
|
|
59
|
+
max: 7 * 24 * 60 * 60 * 1000,
|
|
60
|
+
default: 7 * 24 * 60 * 60 * 1000,
|
|
61
|
+
},
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
/** Schema defaults: every flag off, every limit at its conservative default. */
|
|
65
|
+
export const DEFAULT_CAPABILITY_SETTINGS: CapabilitySettings = Object.freeze({
|
|
66
|
+
codexSearch: false,
|
|
67
|
+
codexImages: false,
|
|
68
|
+
codexImageEdits: false,
|
|
69
|
+
codexUsage: false,
|
|
70
|
+
codexFast: false,
|
|
71
|
+
grokImagineImage: false,
|
|
72
|
+
grokImagineVideo: false,
|
|
73
|
+
searchResults: CAPABILITY_SETTINGS_BOUNDS.searchResults.default,
|
|
74
|
+
imageCount: CAPABILITY_SETTINGS_BOUNDS.imageCount.default,
|
|
75
|
+
videoArtifactTtlMs: CAPABILITY_SETTINGS_BOUNDS.videoArtifactTtlMs.default,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Real Schemastery schema registered with the Host settings service. Defaults
|
|
80
|
+
* remain conservative, and bounds are enforced before a user document commits.
|
|
81
|
+
*/
|
|
82
|
+
export const CapabilitySettingsSchema = Schema.object({
|
|
83
|
+
codexSearch: Schema.boolean().default(false),
|
|
84
|
+
codexImages: Schema.boolean().default(false),
|
|
85
|
+
codexImageEdits: Schema.boolean().default(false),
|
|
86
|
+
codexUsage: Schema.boolean().default(false),
|
|
87
|
+
codexFast: Schema.boolean().default(false),
|
|
88
|
+
grokImagineImage: Schema.boolean().default(false),
|
|
89
|
+
grokImagineVideo: Schema.boolean().default(false),
|
|
90
|
+
searchResults: Schema.number()
|
|
91
|
+
.step(1)
|
|
92
|
+
.min(CAPABILITY_SETTINGS_BOUNDS.searchResults.min)
|
|
93
|
+
.max(CAPABILITY_SETTINGS_BOUNDS.searchResults.max)
|
|
94
|
+
.default(CAPABILITY_SETTINGS_BOUNDS.searchResults.default),
|
|
95
|
+
imageCount: Schema.number()
|
|
96
|
+
.step(1)
|
|
97
|
+
.min(CAPABILITY_SETTINGS_BOUNDS.imageCount.min)
|
|
98
|
+
.max(CAPABILITY_SETTINGS_BOUNDS.imageCount.max)
|
|
99
|
+
.default(CAPABILITY_SETTINGS_BOUNDS.imageCount.default),
|
|
100
|
+
videoArtifactTtlMs: Schema.number()
|
|
101
|
+
.step(1)
|
|
102
|
+
.min(CAPABILITY_SETTINGS_BOUNDS.videoArtifactTtlMs.min)
|
|
103
|
+
.max(CAPABILITY_SETTINGS_BOUNDS.videoArtifactTtlMs.max)
|
|
104
|
+
.default(CAPABILITY_SETTINGS_BOUNDS.videoArtifactTtlMs.default),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** Serialized schema metadata consumed by Settings UI tests and diagnostics. */
|
|
108
|
+
export const CAPABILITY_SETTINGS_SCHEMA_JSON = CapabilitySettingsSchema.toJSON();
|
|
109
|
+
|
|
110
|
+
export type CapabilitySettingsSchemaType = typeof CapabilitySettingsSchema;
|
|
111
|
+
|
|
112
|
+
/** Revision-bearing, secret-free snapshot used for CAS writes and UI. */
|
|
113
|
+
export interface CapabilitySettingsSnapshot {
|
|
114
|
+
readonly ns: typeof CAPABILITY_SETTINGS_NAMESPACE;
|
|
115
|
+
readonly value: CapabilitySettings;
|
|
116
|
+
readonly base?: CapabilitySettingsPatch;
|
|
117
|
+
readonly user?: CapabilitySettingsPatch;
|
|
118
|
+
readonly revision: number;
|
|
119
|
+
readonly writable: boolean;
|
|
120
|
+
readonly applies: "live";
|
|
121
|
+
readonly secrets: readonly [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Owner-facing subset of `ctx.settings.register()` used when the parent injects a provider. */
|
|
125
|
+
export interface CapabilitySettingsScope {
|
|
126
|
+
get(): unknown;
|
|
127
|
+
watch(callback: (next: unknown, prev: unknown) => void | Promise<void>): () => void;
|
|
128
|
+
update(patch: object): Promise<void>;
|
|
129
|
+
replace(section: object): Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One namespace descriptor as returned by a structural `describe()`. */
|
|
133
|
+
export interface CapabilitySettingsDescriptor {
|
|
134
|
+
readonly ns: string;
|
|
135
|
+
readonly value?: unknown;
|
|
136
|
+
readonly base?: unknown;
|
|
137
|
+
readonly user?: unknown;
|
|
138
|
+
readonly revision?: number;
|
|
139
|
+
readonly applies?: "live" | "restart";
|
|
140
|
+
readonly secrets?: readonly { readonly path?: readonly string[]; readonly set?: boolean }[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Duck-typed settings service. A real `ctx.settings` satisfies this without a
|
|
145
|
+
* compile-time dependency on `@deepseek-ai/dsh-settings`.
|
|
146
|
+
*/
|
|
147
|
+
export interface CapabilitySettingsService {
|
|
148
|
+
readonly writable?: boolean;
|
|
149
|
+
describe?(options?: { readonly redactSecrets?: boolean }): readonly CapabilitySettingsDescriptor[];
|
|
150
|
+
get?(ns: string): unknown;
|
|
151
|
+
update?(ns: string, patch: object, expectedRevision?: number): Promise<void>;
|
|
152
|
+
replace?(ns: string, section: object, expectedRevision?: number): Promise<void>;
|
|
153
|
+
register?(
|
|
154
|
+
ns: string,
|
|
155
|
+
schema: CapabilitySettingsSchemaType,
|
|
156
|
+
options?: {
|
|
157
|
+
readonly base?: CapabilitySettingsPatch;
|
|
158
|
+
readonly applies?: "live" | "restart";
|
|
159
|
+
readonly validate?: (value: CapabilitySettings) => void;
|
|
160
|
+
},
|
|
161
|
+
): CapabilitySettingsScope;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Construction options. `base` is the YAML / composition entry layered under the user section. */
|
|
165
|
+
export interface CapabilitySettingsControllerOptions {
|
|
166
|
+
readonly settings?: CapabilitySettingsService | undefined;
|
|
167
|
+
readonly base?: CapabilitySettingsPatch | undefined;
|
|
168
|
+
/** Contain both synchronous and asynchronous observer failures. */
|
|
169
|
+
readonly onListenerError?: ((error: unknown) => void) | undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Listener invoked after a committed snapshot change. */
|
|
173
|
+
export type CapabilitySettingsListener = (snapshot: CapabilitySettingsSnapshot) => void | Promise<void>;
|
|
174
|
+
|
|
175
|
+
const SECRET_KEY = /secret|token|password|passphrase|apikey|api_key|authorization|credential|cookie|private[_-]?key/iu;
|
|
176
|
+
const RESERVED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
177
|
+
const KNOWN_KEYS = new Set<string>(CAPABILITY_SETTINGS_KEYS);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A write refused because the namespace moved since the caller read it.
|
|
181
|
+
* `code` matches the Host settings seam so a later wire layer can map it.
|
|
182
|
+
*/
|
|
183
|
+
export class CapabilitySettingsConflictError extends Error {
|
|
184
|
+
readonly code = "SETTINGS_CONFLICT";
|
|
185
|
+
readonly ns = CAPABILITY_SETTINGS_NAMESPACE;
|
|
186
|
+
readonly expected: number;
|
|
187
|
+
readonly actual: number;
|
|
188
|
+
|
|
189
|
+
constructor(expected: number, actual: number) {
|
|
190
|
+
super(
|
|
191
|
+
`settings namespace "${CAPABILITY_SETTINGS_NAMESPACE}" changed since it was read ` +
|
|
192
|
+
`(expected revision ${String(expected)}, now ${String(actual)})`,
|
|
193
|
+
);
|
|
194
|
+
this.name = "CapabilitySettingsConflictError";
|
|
195
|
+
this.expected = expected;
|
|
196
|
+
this.actual = actual;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A write refused because no writable settings provider is attached. */
|
|
201
|
+
export class CapabilitySettingsReadOnlyError extends Error {
|
|
202
|
+
readonly code: "SETTINGS_PROVIDER_ABSENT" | "SETTINGS_READ_ONLY" | "SETTINGS_DISPOSED";
|
|
203
|
+
readonly ns = CAPABILITY_SETTINGS_NAMESPACE;
|
|
204
|
+
readonly reason: "absent" | "read-only" | "disposed";
|
|
205
|
+
|
|
206
|
+
constructor(reason: "absent" | "read-only" | "disposed") {
|
|
207
|
+
const code =
|
|
208
|
+
reason === "absent"
|
|
209
|
+
? "SETTINGS_PROVIDER_ABSENT"
|
|
210
|
+
: reason === "disposed"
|
|
211
|
+
? "SETTINGS_DISPOSED"
|
|
212
|
+
: "SETTINGS_READ_ONLY";
|
|
213
|
+
const detail =
|
|
214
|
+
reason === "absent"
|
|
215
|
+
? `settings provider is absent: "${CAPABILITY_SETTINGS_NAMESPACE}" cannot be updated`
|
|
216
|
+
: reason === "disposed"
|
|
217
|
+
? `settings controller is disposed: "${CAPABILITY_SETTINGS_NAMESPACE}" cannot be updated`
|
|
218
|
+
: `settings provider is read-only: "${CAPABILITY_SETTINGS_NAMESPACE}" cannot be updated in-process`;
|
|
219
|
+
super(detail);
|
|
220
|
+
this.name = "CapabilitySettingsReadOnlyError";
|
|
221
|
+
this.code = code;
|
|
222
|
+
this.reason = reason;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isCapabilitySettingsConflictError(error: unknown): error is CapabilitySettingsConflictError {
|
|
227
|
+
return error instanceof CapabilitySettingsConflictError;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function isCapabilitySettingsReadOnlyError(error: unknown): error is CapabilitySettingsReadOnlyError {
|
|
231
|
+
return error instanceof CapabilitySettingsReadOnlyError;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Pick every independently default-off flag from a resolved section. */
|
|
235
|
+
export function capabilityFlags(settings: CapabilitySettings): Pick<CapabilitySettings, CapabilityFlagKey> {
|
|
236
|
+
return {
|
|
237
|
+
codexSearch: settings.codexSearch,
|
|
238
|
+
codexImages: settings.codexImages,
|
|
239
|
+
codexImageEdits: settings.codexImageEdits,
|
|
240
|
+
codexUsage: settings.codexUsage,
|
|
241
|
+
codexFast: settings.codexFast,
|
|
242
|
+
grokImagineImage: settings.grokImagineImage,
|
|
243
|
+
grokImagineVideo: settings.grokImagineVideo,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Pick the conservative numeric limits from a resolved section. */
|
|
248
|
+
export function capabilityLimits(settings: CapabilitySettings): Pick<CapabilitySettings, CapabilityLimitKey> {
|
|
249
|
+
return {
|
|
250
|
+
searchResults: settings.searchResults,
|
|
251
|
+
imageCount: settings.imageCount,
|
|
252
|
+
videoArtifactTtlMs: settings.videoArtifactTtlMs,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Layer schema defaults, then YAML/composition `base`, then the user section. */
|
|
257
|
+
export function resolveCapabilitySettings(
|
|
258
|
+
base?: CapabilitySettingsPatch | undefined,
|
|
259
|
+
user?: CapabilitySettingsPatch | undefined,
|
|
260
|
+
): CapabilitySettings {
|
|
261
|
+
return normalizeCapabilitySettings({
|
|
262
|
+
...DEFAULT_CAPABILITY_SETTINGS,
|
|
263
|
+
...normalizeCapabilitySettingsPatch(base),
|
|
264
|
+
...normalizeCapabilitySettingsPatch(user),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Admit a candidate section: known keys only, flags default off, limits clamped,
|
|
270
|
+
* secret-shaped keys dropped. Used for both reads and the structural schema.
|
|
271
|
+
*/
|
|
272
|
+
export function normalizeCapabilitySettings(input?: unknown): CapabilitySettings {
|
|
273
|
+
const patch = normalizeCapabilitySettingsPatch(input);
|
|
274
|
+
return Object.freeze({
|
|
275
|
+
codexSearch: patch.codexSearch ?? DEFAULT_CAPABILITY_SETTINGS.codexSearch,
|
|
276
|
+
codexImages: patch.codexImages ?? DEFAULT_CAPABILITY_SETTINGS.codexImages,
|
|
277
|
+
codexImageEdits: patch.codexImageEdits ?? DEFAULT_CAPABILITY_SETTINGS.codexImageEdits,
|
|
278
|
+
codexUsage: patch.codexUsage ?? DEFAULT_CAPABILITY_SETTINGS.codexUsage,
|
|
279
|
+
codexFast: patch.codexFast ?? DEFAULT_CAPABILITY_SETTINGS.codexFast,
|
|
280
|
+
grokImagineImage: patch.grokImagineImage ?? DEFAULT_CAPABILITY_SETTINGS.grokImagineImage,
|
|
281
|
+
grokImagineVideo: patch.grokImagineVideo ?? DEFAULT_CAPABILITY_SETTINGS.grokImagineVideo,
|
|
282
|
+
searchResults: patch.searchResults ?? DEFAULT_CAPABILITY_SETTINGS.searchResults,
|
|
283
|
+
imageCount: patch.imageCount ?? DEFAULT_CAPABILITY_SETTINGS.imageCount,
|
|
284
|
+
videoArtifactTtlMs: patch.videoArtifactTtlMs ?? DEFAULT_CAPABILITY_SETTINGS.videoArtifactTtlMs,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Normalize a sparse overlay. Invalid or secret fields are omitted so a lower
|
|
290
|
+
* layer (YAML base / schema default) remains authoritative for that key.
|
|
291
|
+
*/
|
|
292
|
+
export function normalizeCapabilitySettingsPatch(input?: unknown): CapabilitySettingsPatch {
|
|
293
|
+
if (!isPlainObject(input)) return {};
|
|
294
|
+
const patch: {
|
|
295
|
+
codexSearch?: boolean;
|
|
296
|
+
codexImages?: boolean;
|
|
297
|
+
codexImageEdits?: boolean;
|
|
298
|
+
codexUsage?: boolean;
|
|
299
|
+
codexFast?: boolean;
|
|
300
|
+
grokImagineImage?: boolean;
|
|
301
|
+
grokImagineVideo?: boolean;
|
|
302
|
+
searchResults?: number;
|
|
303
|
+
imageCount?: number;
|
|
304
|
+
videoArtifactTtlMs?: number;
|
|
305
|
+
} = {};
|
|
306
|
+
const flags = input as Record<string, unknown>;
|
|
307
|
+
assignFlag(patch, "codexSearch", flags["codexSearch"]);
|
|
308
|
+
assignFlag(patch, "codexImages", flags["codexImages"]);
|
|
309
|
+
assignFlag(patch, "codexImageEdits", flags["codexImageEdits"]);
|
|
310
|
+
assignFlag(patch, "codexUsage", flags["codexUsage"]);
|
|
311
|
+
assignFlag(patch, "codexFast", flags["codexFast"]);
|
|
312
|
+
assignFlag(patch, "grokImagineImage", flags["grokImagineImage"]);
|
|
313
|
+
assignFlag(patch, "grokImagineVideo", flags["grokImagineVideo"]);
|
|
314
|
+
assignLimit(patch, "searchResults", flags["searchResults"]);
|
|
315
|
+
assignLimit(patch, "imageCount", flags["imageCount"]);
|
|
316
|
+
assignLimit(patch, "videoArtifactTtlMs", flags["videoArtifactTtlMs"]);
|
|
317
|
+
return Object.freeze(patch);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Strictly admit a caller-authored sparse section before normalizing it. Reads
|
|
322
|
+
* remain compatibility-tolerant, but writes must never silently drop unknown
|
|
323
|
+
* fields, coerce types, truncate decimals, or clamp out-of-range limits.
|
|
324
|
+
*/
|
|
325
|
+
export function assertCapabilitySettingsPatch(
|
|
326
|
+
input: unknown,
|
|
327
|
+
label = "capability settings",
|
|
328
|
+
): asserts input is CapabilitySettingsPatch {
|
|
329
|
+
assertPlainObject(input, label);
|
|
330
|
+
for (const [key, value] of Object.entries(input)) {
|
|
331
|
+
if (!KNOWN_KEYS.has(key)) throw new TypeError(`${label} contains unknown key ${key}`);
|
|
332
|
+
if ((CAPABILITY_FLAG_KEYS as readonly string[]).includes(key)) {
|
|
333
|
+
if (typeof value !== "boolean") throw new TypeError(`${label}.${key} must be a boolean`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const limitKey = key as CapabilityLimitKey;
|
|
337
|
+
const bounds = CAPABILITY_SETTINGS_BOUNDS[limitKey];
|
|
338
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
339
|
+
throw new TypeError(`${label}.${key} must be an integer`);
|
|
340
|
+
}
|
|
341
|
+
if (value < bounds.min || value > bounds.max) {
|
|
342
|
+
throw new TypeError(`${label}.${key} must be in [${String(bounds.min)}, ${String(bounds.max)}]`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Reject a resolved section the owner could not act on. Schema-valid by construction after normalize. */
|
|
348
|
+
export function assertServiceableCapabilitySettings(value: CapabilitySettings): void {
|
|
349
|
+
for (const key of CAPABILITY_FLAG_KEYS) {
|
|
350
|
+
if (typeof value[key] !== "boolean") {
|
|
351
|
+
throw new TypeError(`capability settings: ${key} must be a boolean`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
for (const key of CAPABILITY_LIMIT_KEYS) {
|
|
355
|
+
const bounds = CAPABILITY_SETTINGS_BOUNDS[key];
|
|
356
|
+
const numeric = value[key];
|
|
357
|
+
if (!Number.isFinite(numeric) || !Number.isInteger(numeric) || numeric < bounds.min || numeric > bounds.max) {
|
|
358
|
+
throw new TypeError(
|
|
359
|
+
`capability settings: ${key} must be an integer in [${String(bounds.min)}, ${String(bounds.max)}]`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Live capability-settings controller. Without an injected provider the
|
|
367
|
+
* resolved state is the YAML/default layer and every write fails explicitly.
|
|
368
|
+
*/
|
|
369
|
+
export class CapabilitySettingsController {
|
|
370
|
+
readonly ns = CAPABILITY_SETTINGS_NAMESPACE;
|
|
371
|
+
private readonly settings: CapabilitySettingsService | undefined;
|
|
372
|
+
private readonly base: CapabilitySettingsPatch;
|
|
373
|
+
private readonly onListenerError: (error: unknown) => void;
|
|
374
|
+
private readonly listeners = new Set<CapabilitySettingsListener>();
|
|
375
|
+
private scope: CapabilitySettingsScope | undefined;
|
|
376
|
+
private scopeDisposer: (() => void) | undefined;
|
|
377
|
+
private localRevision = 0;
|
|
378
|
+
private lastSnapshot: CapabilitySettingsSnapshot;
|
|
379
|
+
private disposed = false;
|
|
380
|
+
|
|
381
|
+
constructor(options: CapabilitySettingsControllerOptions = {}) {
|
|
382
|
+
this.settings = options.settings;
|
|
383
|
+
this.base = normalizeCapabilitySettingsPatch(options.base);
|
|
384
|
+
this.onListenerError = options.onListenerError ?? (() => undefined);
|
|
385
|
+
this.attachScope();
|
|
386
|
+
this.lastSnapshot = this.readSnapshot();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Current revision-bearing snapshot. Re-reads the injected provider when present. */
|
|
390
|
+
snapshot(): CapabilitySettingsSnapshot {
|
|
391
|
+
const next = this.readSnapshot();
|
|
392
|
+
this.lastSnapshot = next;
|
|
393
|
+
return next;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Resolved capability section (schema defaults ← YAML base ← user). */
|
|
397
|
+
current(): CapabilitySettings {
|
|
398
|
+
return this.snapshot().value;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Merge a secret-free patch into the user layer using compare-and-swap on
|
|
403
|
+
* `expectedRevision` from a previously read {@link snapshot}.
|
|
404
|
+
*/
|
|
405
|
+
async patch(patch: CapabilitySettingsPatch, expectedRevision: number): Promise<CapabilitySettingsSnapshot> {
|
|
406
|
+
return this.write("update", patch, expectedRevision);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Replace the user section wholesale (`{}` re-inherits YAML base and defaults).
|
|
411
|
+
* Compare-and-swap uses the same revision token as {@link patch}.
|
|
412
|
+
*/
|
|
413
|
+
async replace(section: CapabilitySettingsPatch, expectedRevision: number): Promise<CapabilitySettingsSnapshot> {
|
|
414
|
+
return this.write("replace", section, expectedRevision);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Observe committed snapshot changes. The disposer removes this listener;
|
|
419
|
+
* an invocation already running still settles.
|
|
420
|
+
*/
|
|
421
|
+
subscribe(listener: CapabilitySettingsListener): () => void {
|
|
422
|
+
if (this.disposed) return () => undefined;
|
|
423
|
+
this.listeners.add(listener);
|
|
424
|
+
return () => {
|
|
425
|
+
this.listeners.delete(listener);
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Re-read the injected provider (or the local YAML/default layer) and notify
|
|
431
|
+
* listeners when the secret-free snapshot moved.
|
|
432
|
+
*/
|
|
433
|
+
reconcile(): CapabilitySettingsSnapshot {
|
|
434
|
+
const next = this.readSnapshot();
|
|
435
|
+
this.publish(next);
|
|
436
|
+
return this.lastSnapshot;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Drop the register() watcher and every listener. Further writes fail. */
|
|
440
|
+
dispose(): void {
|
|
441
|
+
if (this.disposed) return;
|
|
442
|
+
this.disposed = true;
|
|
443
|
+
const releaseScope = this.scopeDisposer;
|
|
444
|
+
this.scopeDisposer = undefined;
|
|
445
|
+
this.scope = undefined;
|
|
446
|
+
this.listeners.clear();
|
|
447
|
+
try {
|
|
448
|
+
releaseScope?.();
|
|
449
|
+
} catch (error: unknown) {
|
|
450
|
+
this.onListenerError(error);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private attachScope(): void {
|
|
455
|
+
const register = this.settings?.register;
|
|
456
|
+
if (register === undefined) return;
|
|
457
|
+
this.scope = register.call(this.settings, CAPABILITY_SETTINGS_NAMESPACE, CapabilitySettingsSchema, {
|
|
458
|
+
base: this.base,
|
|
459
|
+
applies: "live",
|
|
460
|
+
validate: assertServiceableCapabilitySettings,
|
|
461
|
+
});
|
|
462
|
+
this.scopeDisposer = this.scope.watch(() => {
|
|
463
|
+
if (this.disposed) return;
|
|
464
|
+
this.reconcile();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private writeReason(): "absent" | "read-only" | "disposed" | undefined {
|
|
469
|
+
if (this.disposed) return "disposed";
|
|
470
|
+
if (this.settings === undefined) return "absent";
|
|
471
|
+
if (this.settings.writable === false) return "read-only";
|
|
472
|
+
const canWrite =
|
|
473
|
+
typeof this.settings.update === "function" ||
|
|
474
|
+
typeof this.settings.replace === "function" ||
|
|
475
|
+
this.scope !== undefined;
|
|
476
|
+
if (!canWrite) return "read-only";
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private isWritable(): boolean {
|
|
481
|
+
return this.writeReason() === undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private async write(
|
|
485
|
+
mode: "update" | "replace",
|
|
486
|
+
input: CapabilitySettingsPatch,
|
|
487
|
+
expectedRevision: number,
|
|
488
|
+
): Promise<CapabilitySettingsSnapshot> {
|
|
489
|
+
const reason = this.writeReason();
|
|
490
|
+
if (reason !== undefined) throw new CapabilitySettingsReadOnlyError(reason);
|
|
491
|
+
assertCapabilitySettingsPatch(input, `capability settings ${mode}`);
|
|
492
|
+
const current = this.readSnapshot();
|
|
493
|
+
if (expectedRevision !== current.revision) {
|
|
494
|
+
throw new CapabilitySettingsConflictError(expectedRevision, current.revision);
|
|
495
|
+
}
|
|
496
|
+
const normalized = normalizeCapabilitySettingsPatch(input);
|
|
497
|
+
if (mode === "update" && !hasOwnKeys(normalized)) return current;
|
|
498
|
+
const settings = this.settings!;
|
|
499
|
+
try {
|
|
500
|
+
if (mode === "update") {
|
|
501
|
+
if (typeof settings.update === "function") {
|
|
502
|
+
await settings.update(CAPABILITY_SETTINGS_NAMESPACE, { ...normalized }, expectedRevision);
|
|
503
|
+
} else {
|
|
504
|
+
await this.scope!.update({ ...normalized });
|
|
505
|
+
}
|
|
506
|
+
} else if (typeof settings.replace === "function") {
|
|
507
|
+
await settings.replace(CAPABILITY_SETTINGS_NAMESPACE, { ...normalized }, expectedRevision);
|
|
508
|
+
} else {
|
|
509
|
+
await this.scope!.replace({ ...normalized });
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
throw toConflictError(error) ?? error;
|
|
513
|
+
}
|
|
514
|
+
this.localRevision = current.revision + 1;
|
|
515
|
+
const next = this.readSnapshot();
|
|
516
|
+
this.publish(next);
|
|
517
|
+
return this.lastSnapshot;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private readSnapshot(): CapabilitySettingsSnapshot {
|
|
521
|
+
const writable = this.isWritable();
|
|
522
|
+
const described = this.readDescribed();
|
|
523
|
+
const base = described?.base !== undefined ? normalizeCapabilitySettingsPatch(described.base) : this.base;
|
|
524
|
+
const user = described?.user !== undefined ? normalizeCapabilitySettingsPatch(described.user) : undefined;
|
|
525
|
+
const resolved =
|
|
526
|
+
described?.value !== undefined
|
|
527
|
+
? normalizeCapabilitySettings(described.value)
|
|
528
|
+
: this.readResolvedFromService(base, user);
|
|
529
|
+
const revision =
|
|
530
|
+
typeof described?.revision === "number" && Number.isFinite(described.revision)
|
|
531
|
+
? described.revision
|
|
532
|
+
: this.localRevision;
|
|
533
|
+
return freezeSnapshot({
|
|
534
|
+
ns: CAPABILITY_SETTINGS_NAMESPACE,
|
|
535
|
+
value: resolved,
|
|
536
|
+
revision,
|
|
537
|
+
writable,
|
|
538
|
+
applies: "live",
|
|
539
|
+
secrets: [],
|
|
540
|
+
...(hasOwnKeys(base) ? { base } : {}),
|
|
541
|
+
...(user !== undefined && hasOwnKeys(user) ? { user } : {}),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private readResolvedFromService(
|
|
546
|
+
base: CapabilitySettingsPatch,
|
|
547
|
+
user: CapabilitySettingsPatch | undefined,
|
|
548
|
+
): CapabilitySettings {
|
|
549
|
+
const raw = this.scope?.get() ?? this.settings?.get?.(CAPABILITY_SETTINGS_NAMESPACE);
|
|
550
|
+
if (raw !== undefined) return normalizeCapabilitySettings(raw);
|
|
551
|
+
return resolveCapabilitySettings(base, user);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private readDescribed(): CapabilitySettingsDescriptor | undefined {
|
|
555
|
+
const describe = this.settings?.describe;
|
|
556
|
+
if (describe === undefined) return undefined;
|
|
557
|
+
try {
|
|
558
|
+
const descriptors = describe.call(this.settings, { redactSecrets: true });
|
|
559
|
+
if (!Array.isArray(descriptors)) return undefined;
|
|
560
|
+
return descriptors.find((entry) => entry?.ns === CAPABILITY_SETTINGS_NAMESPACE);
|
|
561
|
+
} catch {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private publish(next: CapabilitySettingsSnapshot): void {
|
|
567
|
+
if (sameSnapshot(this.lastSnapshot, next)) {
|
|
568
|
+
this.lastSnapshot = next;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.lastSnapshot = next;
|
|
572
|
+
for (const listener of [...this.listeners]) {
|
|
573
|
+
try {
|
|
574
|
+
const result = listener(next);
|
|
575
|
+
if (result !== undefined) void Promise.resolve(result).catch(this.onListenerError);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
// One broken observer must not starve the rest or the write path.
|
|
578
|
+
this.onListenerError(error);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Construct a {@link CapabilitySettingsController}. */
|
|
585
|
+
export function createCapabilitySettingsController(
|
|
586
|
+
options: CapabilitySettingsControllerOptions = {},
|
|
587
|
+
): CapabilitySettingsController {
|
|
588
|
+
return new CapabilitySettingsController(options);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function assignFlag<K extends CapabilityFlagKey>(target: { [P in K]?: boolean }, key: K, value: unknown): void {
|
|
592
|
+
if (typeof value === "boolean") target[key] = value;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function assignLimit<K extends CapabilityLimitKey>(target: { [P in K]?: number }, key: K, value: unknown): void {
|
|
596
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
597
|
+
const bounds = CAPABILITY_SETTINGS_BOUNDS[key];
|
|
598
|
+
const integer = Math.trunc(value);
|
|
599
|
+
target[key] = Math.min(bounds.max, Math.max(bounds.min, integer));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
603
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
604
|
+
const proto = Object.getPrototypeOf(value);
|
|
605
|
+
return proto === Object.prototype || proto === null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function assertPlainObject(value: unknown, label: string): asserts value is Record<string, unknown> {
|
|
609
|
+
if (!isPlainObject(value)) throw new TypeError(`${label} must be a plain object`);
|
|
610
|
+
for (const key of Object.keys(value)) {
|
|
611
|
+
if (RESERVED_KEYS.has(key) || SECRET_KEY.test(key)) {
|
|
612
|
+
throw new TypeError(`${label} must be secret-free (rejected key ${key})`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function hasOwnKeys(value: object): boolean {
|
|
618
|
+
return Object.keys(value).length > 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function freezeSnapshot(snapshot: CapabilitySettingsSnapshot): CapabilitySettingsSnapshot {
|
|
622
|
+
return Object.freeze(snapshot);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function sameSnapshot(left: CapabilitySettingsSnapshot | undefined, right: CapabilitySettingsSnapshot): boolean {
|
|
626
|
+
if (left === undefined) return false;
|
|
627
|
+
return (
|
|
628
|
+
left.revision === right.revision &&
|
|
629
|
+
left.writable === right.writable &&
|
|
630
|
+
deepEqualJson(left.value, right.value) &&
|
|
631
|
+
deepEqualJson(left.base, right.base) &&
|
|
632
|
+
deepEqualJson(left.user, right.user)
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function deepEqualJson(a: unknown, b: unknown): boolean {
|
|
637
|
+
if (a === b) return true;
|
|
638
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
639
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
640
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
641
|
+
return a.every((entry, index) => deepEqualJson(entry, b[index]));
|
|
642
|
+
}
|
|
643
|
+
const left = a as Record<string, unknown>;
|
|
644
|
+
const right = b as Record<string, unknown>;
|
|
645
|
+
const keys = Object.keys(left);
|
|
646
|
+
if (keys.length !== Object.keys(right).length) return false;
|
|
647
|
+
return keys.every((key) => key in right && deepEqualJson(left[key], right[key]));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function toConflictError(error: unknown): CapabilitySettingsConflictError | undefined {
|
|
651
|
+
if (error instanceof CapabilitySettingsConflictError) return error;
|
|
652
|
+
if (typeof error !== "object" || error === null) return undefined;
|
|
653
|
+
const candidate = error as { code?: unknown; expected?: unknown; actual?: unknown };
|
|
654
|
+
if (candidate.code !== "SETTINGS_CONFLICT") return undefined;
|
|
655
|
+
if (typeof candidate.expected !== "number" || typeof candidate.actual !== "number") return undefined;
|
|
656
|
+
return new CapabilitySettingsConflictError(candidate.expected, candidate.actual);
|
|
657
|
+
}
|