@centerforagenticai/pi-multi-account 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preview-confirm-atomic-write workflow behind `multi-account account
|
|
3
|
+
* set-plan`.
|
|
4
|
+
*
|
|
5
|
+
* This module holds the domain transaction only: resolving and validating a
|
|
6
|
+
* candidate assignment against the current catalog and canonical account set,
|
|
7
|
+
* building the immutable {@link AccountRateRecord} it selects, and committing
|
|
8
|
+
* it through exactly one atomic {@link writeConfig} call. It never guesses
|
|
9
|
+
* `effectiveFrom`, a history start, or a next-renewal boundary, and it never
|
|
10
|
+
* suggests a nearby preset for an unknown id -- both are rejected outright.
|
|
11
|
+
*
|
|
12
|
+
* Ordering mirrors `routing-config-transaction.ts`'s documented invariant:
|
|
13
|
+
* human dialogs never hold a machine lease. The preview is built from an
|
|
14
|
+
* unlocked read; the machine lease guarding `config.json` is acquired only
|
|
15
|
+
* after the operator has confirmed, and every value written to disk is
|
|
16
|
+
* re-resolved from a fresh, locked read immediately beforehand, so a stale
|
|
17
|
+
* preview -- or a catalog/account edit racing the confirmation dialog -- can
|
|
18
|
+
* never reach the single atomic write.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
isCanonicalSubscriptionAccountId,
|
|
23
|
+
readConfig,
|
|
24
|
+
validateConfig,
|
|
25
|
+
writeConfig,
|
|
26
|
+
type ConfigWriteOutcome,
|
|
27
|
+
type MultiAccountConfig,
|
|
28
|
+
} from "./config.js";
|
|
29
|
+
import {
|
|
30
|
+
findSubscriptionPlanPreset,
|
|
31
|
+
loadEffectiveSubscriptionPlanCatalog,
|
|
32
|
+
type CatalogAccountType,
|
|
33
|
+
type CatalogProvider,
|
|
34
|
+
type SubscriptionPlanCatalog,
|
|
35
|
+
type SubscriptionPlanPreset,
|
|
36
|
+
} from "./subscription-plan-catalog.js";
|
|
37
|
+
import {
|
|
38
|
+
assertValidEffectiveFrom,
|
|
39
|
+
createAccountRateRecord,
|
|
40
|
+
normalizeAccountRateHistory,
|
|
41
|
+
type AccountRateRecord,
|
|
42
|
+
} from "./account-rate-history.js";
|
|
43
|
+
import {
|
|
44
|
+
MACHINE_LEASE_TTL_MS,
|
|
45
|
+
acquireMachineLease,
|
|
46
|
+
type MachineLeaseHandle,
|
|
47
|
+
type MachineLeaseOptions,
|
|
48
|
+
} from "./machine-lease.js";
|
|
49
|
+
import { routingConfigLockPath } from "./routing-config-transaction.js";
|
|
50
|
+
|
|
51
|
+
export class AccountPlanAssignmentError extends Error {
|
|
52
|
+
constructor(message: string) {
|
|
53
|
+
super(`[account-plan-assignment] ${message}`);
|
|
54
|
+
this.name = "AccountPlanAssignmentError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A fixed, non-caller-controlled provenance string for an explicit
|
|
60
|
+
* `--monthly-usd` override. `createAccountRateRecord` requires override
|
|
61
|
+
* provenance whenever an override amount is supplied; this command never
|
|
62
|
+
* accepts free-text provenance from the operator, so every override this
|
|
63
|
+
* command produces carries exactly this string.
|
|
64
|
+
*/
|
|
65
|
+
export const OPERATOR_OVERRIDE_PROVENANCE =
|
|
66
|
+
"operator-provided override via account set-plan";
|
|
67
|
+
|
|
68
|
+
export interface AccountPlanAssignmentInput {
|
|
69
|
+
readonly accountId: string;
|
|
70
|
+
readonly presetId: string;
|
|
71
|
+
/** RFC 3339 instant with an explicit "Z" or numeric offset. */
|
|
72
|
+
readonly effectiveFrom: string;
|
|
73
|
+
readonly monthlyUsdOverride?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface AccountPlanAssignmentPreview {
|
|
77
|
+
readonly accountId: string;
|
|
78
|
+
readonly provider: CatalogProvider;
|
|
79
|
+
readonly accountType: CatalogAccountType;
|
|
80
|
+
readonly presetId: string;
|
|
81
|
+
readonly presetLabel: string;
|
|
82
|
+
readonly monthlyUsd: number;
|
|
83
|
+
readonly isOverride: boolean;
|
|
84
|
+
readonly effectiveFrom: string;
|
|
85
|
+
readonly catalogVersion: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolves and validates one candidate assignment against a supplied config
|
|
90
|
+
* and resolved catalog snapshot. Pure: performs no I/O and never guesses a
|
|
91
|
+
* missing or malformed input. Throws {@link AccountPlanAssignmentError} naming
|
|
92
|
+
* the rejected account id or preset id, or an `AccountRateHistoryError` (via
|
|
93
|
+
* {@link assertValidEffectiveFrom}) for a malformed `effectiveFrom`.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveAccountPlanAssignmentPreview(
|
|
96
|
+
input: AccountPlanAssignmentInput,
|
|
97
|
+
config: Pick<MultiAccountConfig, "accountLimit">,
|
|
98
|
+
catalog: SubscriptionPlanCatalog,
|
|
99
|
+
): AccountPlanAssignmentPreview {
|
|
100
|
+
if (!isCanonicalSubscriptionAccountId(input.accountId, config.accountLimit)) {
|
|
101
|
+
throw new AccountPlanAssignmentError(
|
|
102
|
+
`Unknown account "${input.accountId}". It is not a configured canonical account.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const preset = findSubscriptionPlanPreset(catalog, input.presetId);
|
|
106
|
+
if (preset === undefined) {
|
|
107
|
+
throw new AccountPlanAssignmentError(
|
|
108
|
+
`Unknown preset "${input.presetId}". No catalog preset matches that id.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
assertValidEffectiveFrom(input.effectiveFrom, "--effective-from");
|
|
112
|
+
if (
|
|
113
|
+
input.monthlyUsdOverride !== undefined &&
|
|
114
|
+
(!Number.isFinite(input.monthlyUsdOverride) || input.monthlyUsdOverride < 0)
|
|
115
|
+
) {
|
|
116
|
+
throw new AccountPlanAssignmentError(
|
|
117
|
+
"--monthly-usd must be a finite, non-negative USD amount.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const isOverride = input.monthlyUsdOverride !== undefined;
|
|
121
|
+
return {
|
|
122
|
+
accountId: input.accountId,
|
|
123
|
+
provider: preset.provider,
|
|
124
|
+
accountType: preset.accountType,
|
|
125
|
+
presetId: preset.id,
|
|
126
|
+
presetLabel: preset.label,
|
|
127
|
+
monthlyUsd: isOverride ? (input.monthlyUsdOverride as number) : preset.monthlyUsd,
|
|
128
|
+
isOverride,
|
|
129
|
+
effectiveFrom: input.effectiveFrom,
|
|
130
|
+
catalogVersion: catalog.version,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds the immutable candidate record for one resolved preset, copying only
|
|
136
|
+
* the allowlisted fields `createAccountRateRecord` accepts. An explicit
|
|
137
|
+
* override always carries the fixed {@link OPERATOR_OVERRIDE_PROVENANCE};
|
|
138
|
+
* this function never accepts caller-supplied provenance text.
|
|
139
|
+
*/
|
|
140
|
+
export function buildAccountPlanAssignmentRecord(
|
|
141
|
+
input: AccountPlanAssignmentInput,
|
|
142
|
+
preset: SubscriptionPlanPreset,
|
|
143
|
+
catalogVersion: number,
|
|
144
|
+
): AccountRateRecord {
|
|
145
|
+
return createAccountRateRecord({
|
|
146
|
+
accountId: input.accountId,
|
|
147
|
+
preset,
|
|
148
|
+
catalogVersion,
|
|
149
|
+
effectiveFrom: input.effectiveFrom,
|
|
150
|
+
...(input.monthlyUsdOverride !== undefined
|
|
151
|
+
? {
|
|
152
|
+
monthlyUsdOverride: input.monthlyUsdOverride,
|
|
153
|
+
overrideProvenance: OPERATOR_OVERRIDE_PROVENANCE,
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns true when every term `preview` displayed to the operator for
|
|
161
|
+
* confirmation is still present, byte-for-byte, in `freshPreview` -- the
|
|
162
|
+
* preview re-resolved from the locked, current read immediately before the
|
|
163
|
+
* atomic write. Compares every operator-confirmed term (account, provider,
|
|
164
|
+
* account type, preset id and label, the copied or overridden monthly rate,
|
|
165
|
+
* the effective instant, and catalog version), never merely `catalogVersion`
|
|
166
|
+
* or object identity: {@link resolveSubscriptionPlanCatalog} keeps the
|
|
167
|
+
* shipped catalog's `version` unchanged across a machine-global override
|
|
168
|
+
* edit, so a `catalogVersion`-only check would miss exactly the price or
|
|
169
|
+
* definition drift this function exists to catch, and a fresh preview is
|
|
170
|
+
* always a newly built object, so reference identity never applies.
|
|
171
|
+
*/
|
|
172
|
+
function confirmedPreviewTermsStillMatch(
|
|
173
|
+
preview: AccountPlanAssignmentPreview,
|
|
174
|
+
freshPreview: AccountPlanAssignmentPreview,
|
|
175
|
+
): boolean {
|
|
176
|
+
return (
|
|
177
|
+
preview.accountId === freshPreview.accountId &&
|
|
178
|
+
preview.provider === freshPreview.provider &&
|
|
179
|
+
preview.accountType === freshPreview.accountType &&
|
|
180
|
+
preview.presetId === freshPreview.presetId &&
|
|
181
|
+
preview.presetLabel === freshPreview.presetLabel &&
|
|
182
|
+
preview.monthlyUsd === freshPreview.monthlyUsd &&
|
|
183
|
+
preview.isOverride === freshPreview.isOverride &&
|
|
184
|
+
preview.effectiveFrom === freshPreview.effectiveFrom &&
|
|
185
|
+
preview.catalogVersion === freshPreview.catalogVersion
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type AccountPlanAssignmentCommitResult =
|
|
190
|
+
| { readonly status: "applied"; readonly record: AccountRateRecord; readonly outcome: ConfigWriteOutcome }
|
|
191
|
+
| { readonly status: "declined" }
|
|
192
|
+
| { readonly status: "busy" };
|
|
193
|
+
|
|
194
|
+
export interface AccountPlanAssignmentTransactionOptions {
|
|
195
|
+
readonly configPath: string;
|
|
196
|
+
readonly lockPath?: string;
|
|
197
|
+
readonly input: AccountPlanAssignmentInput;
|
|
198
|
+
/** Called once with the resolved preview before {@link confirm} runs. */
|
|
199
|
+
readonly onPreview: (preview: AccountPlanAssignmentPreview) => void;
|
|
200
|
+
/** Never called while a machine lease is held. */
|
|
201
|
+
readonly confirm: () => boolean | Promise<boolean>;
|
|
202
|
+
readonly now?: () => number;
|
|
203
|
+
readonly acquireLease?: (
|
|
204
|
+
options: MachineLeaseOptions,
|
|
205
|
+
) => MachineLeaseHandle | undefined;
|
|
206
|
+
readonly readConfigImpl?: (configPath: string) => MultiAccountConfig;
|
|
207
|
+
readonly writeConfigImpl?: (
|
|
208
|
+
configPath: string,
|
|
209
|
+
config: MultiAccountConfig,
|
|
210
|
+
onCommitted?: () => void,
|
|
211
|
+
) => ConfigWriteOutcome;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Commits one account plan assignment.
|
|
216
|
+
*
|
|
217
|
+
* 1. Reads config unlocked, resolves and validates the preview, and hands it
|
|
218
|
+
* to `onPreview` -- no lease is held yet.
|
|
219
|
+
* 2. Awaits `confirm()`. A decline returns `{status:"declined"}` and performs
|
|
220
|
+
* no further work: retained bytes are untouched.
|
|
221
|
+
* 3. Acquires the machine lease guarding `config.json`. A held lease returns
|
|
222
|
+
* `{status:"busy"}` immediately; no write is attempted.
|
|
223
|
+
* 4. Re-reads config under the lease and re-resolves the preview against that
|
|
224
|
+
* fresh snapshot, then compares every operator-confirmed term (account,
|
|
225
|
+
* provider, account type, preset id and label, the copied or overridden
|
|
226
|
+
* monthly rate, the effective instant, and catalog version) against that
|
|
227
|
+
* fresh preview. A stale preset id, removed account slot, or any catalog
|
|
228
|
+
* edit racing the dialog -- including a price or definition change the
|
|
229
|
+
* operator never saw -- is rejected here, before anything is written. No
|
|
230
|
+
* automatic re-prompt: the caller must rerun the command to see and
|
|
231
|
+
* confirm the fresh terms.
|
|
232
|
+
* 5. Builds the candidate record, appends it to that account's existing
|
|
233
|
+
* history, and normalizes the result -- a duplicate or overlapping
|
|
234
|
+
* `effectiveFrom` throws `AccountRateHistoryError` and nothing is written.
|
|
235
|
+
* 6. Verifies the fully-built candidate config parses back through
|
|
236
|
+
* `validateConfig` -- a structural self-check performed before the
|
|
237
|
+
* candidate ever reaches disk.
|
|
238
|
+
* 7. Performs exactly one atomic write via `writeConfig`.
|
|
239
|
+
*
|
|
240
|
+
* Every step before (7) can throw or return without ever calling the write
|
|
241
|
+
* implementation, so a failure at validation, confirmation, the lease, or this
|
|
242
|
+
* pre-write verification always leaves the persisted bytes exactly as they
|
|
243
|
+
* were.
|
|
244
|
+
*/
|
|
245
|
+
export async function commitAccountPlanAssignment(
|
|
246
|
+
options: AccountPlanAssignmentTransactionOptions,
|
|
247
|
+
): Promise<AccountPlanAssignmentCommitResult> {
|
|
248
|
+
const read = options.readConfigImpl ?? readConfig;
|
|
249
|
+
const write = options.writeConfigImpl ?? writeConfig;
|
|
250
|
+
const acquire = options.acquireLease ?? acquireMachineLease;
|
|
251
|
+
const now = options.now ?? Date.now;
|
|
252
|
+
const lockPath = options.lockPath ?? routingConfigLockPath(options.configPath);
|
|
253
|
+
|
|
254
|
+
const promptConfig = read(options.configPath);
|
|
255
|
+
const promptCatalog = loadEffectiveSubscriptionPlanCatalog(
|
|
256
|
+
promptConfig.subscriptionPlanCatalogOverrides ?? {},
|
|
257
|
+
);
|
|
258
|
+
const preview = resolveAccountPlanAssignmentPreview(
|
|
259
|
+
options.input,
|
|
260
|
+
promptConfig,
|
|
261
|
+
promptCatalog,
|
|
262
|
+
);
|
|
263
|
+
options.onPreview(preview);
|
|
264
|
+
|
|
265
|
+
const confirmed = await options.confirm();
|
|
266
|
+
if (!confirmed) return { status: "declined" };
|
|
267
|
+
|
|
268
|
+
const handle = acquire({ lockPath, ttlMs: MACHINE_LEASE_TTL_MS, now });
|
|
269
|
+
if (handle === undefined) return { status: "busy" };
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
// Fresh, locked re-read and re-validation. This is the pre-write
|
|
273
|
+
// verification step: the candidate is re-derived and re-checked against
|
|
274
|
+
// the CURRENT catalog, account limit, and history immediately before the
|
|
275
|
+
// single atomic write, so a stale preview -- or a concurrent edit that
|
|
276
|
+
// landed between the preview and this confirmation -- can never reach
|
|
277
|
+
// disk.
|
|
278
|
+
const fresh = read(options.configPath);
|
|
279
|
+
const freshCatalog = loadEffectiveSubscriptionPlanCatalog(
|
|
280
|
+
fresh.subscriptionPlanCatalogOverrides ?? {},
|
|
281
|
+
);
|
|
282
|
+
const freshPreview = resolveAccountPlanAssignmentPreview(
|
|
283
|
+
options.input,
|
|
284
|
+
fresh,
|
|
285
|
+
freshCatalog,
|
|
286
|
+
);
|
|
287
|
+
if (!confirmedPreviewTermsStillMatch(preview, freshPreview)) {
|
|
288
|
+
// Fail closed: a term the operator confirmed (account, provider,
|
|
289
|
+
// account type, preset, its label, the copied or overridden monthly
|
|
290
|
+
// rate, the effective instant, or catalog version) no longer matches
|
|
291
|
+
// the fresh, locked snapshot -- most commonly a machine-global
|
|
292
|
+
// catalog override edit racing the confirmation dialog. Reject
|
|
293
|
+
// outright with no write and no automatic re-prompt; the operator
|
|
294
|
+
// reruns the command to see and confirm the current terms. This
|
|
295
|
+
// never touches `fresh`, so any unrelated concurrent config edit it
|
|
296
|
+
// already observed is preserved, not rolled back.
|
|
297
|
+
throw new AccountPlanAssignmentError(
|
|
298
|
+
`Confirmed terms for "${options.input.accountId}" changed before the write ` +
|
|
299
|
+
`could be committed (e.g. a concurrent catalog edit). Nothing was ` +
|
|
300
|
+
`written; rerun account set-plan to review and confirm the current ` +
|
|
301
|
+
`terms.`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const preset = findSubscriptionPlanPreset(freshCatalog, freshPreview.presetId);
|
|
305
|
+
if (preset === undefined) {
|
|
306
|
+
// Unreachable except under a racing catalog edit between the two
|
|
307
|
+
// lookups immediately above: resolveAccountPlanAssignmentPreview
|
|
308
|
+
// already proved this preset id resolves in freshCatalog.
|
|
309
|
+
throw new AccountPlanAssignmentError(
|
|
310
|
+
`Preset "${options.input.presetId}" is no longer available.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const record = buildAccountPlanAssignmentRecord(
|
|
314
|
+
options.input,
|
|
315
|
+
preset,
|
|
316
|
+
freshCatalog.version,
|
|
317
|
+
);
|
|
318
|
+
const existingCandidates = (
|
|
319
|
+
fresh.accountRateHistory?.[options.input.accountId] ?? []
|
|
320
|
+
).map((existingRecord) => ({ record: existingRecord }));
|
|
321
|
+
const normalized = normalizeAccountRateHistory([
|
|
322
|
+
...existingCandidates,
|
|
323
|
+
{ record },
|
|
324
|
+
]);
|
|
325
|
+
const candidate: MultiAccountConfig = {
|
|
326
|
+
...fresh,
|
|
327
|
+
accountRateHistory: {
|
|
328
|
+
...fresh.accountRateHistory,
|
|
329
|
+
[options.input.accountId]: normalized,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
// Structural self-check only -- no disk I/O -- performed before the
|
|
333
|
+
// single atomic write is ever attempted.
|
|
334
|
+
validateConfig(candidate);
|
|
335
|
+
const outcome = write(options.configPath, candidate);
|
|
336
|
+
return { status: "applied", record, outcome };
|
|
337
|
+
} finally {
|
|
338
|
+
handle.release();
|
|
339
|
+
}
|
|
340
|
+
}
|