portal-mojo 0.2.20 → 0.2.21
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/docs/README.md +7 -1
- package/docs/admin-deployments.md +201 -0
- package/docs/admin-dns-certificates.md +3 -3
- package/docs/admin-dns-domains.md +92 -0
- package/docs/admin-dns.md +4 -2
- package/docs/admin-edge.md +200 -196
- package/docs/admin-geolocation.md +100 -0
- package/docs/admin-maintenance.md +133 -0
- package/docs/admin-modals.md +17 -2
- package/docs/admin-operational-fleet.md +206 -0
- package/docs/admin-presentation.md +51 -0
- package/docs/admin-signin.md +12 -2
- package/docs/client.md +7 -0
- package/docs/combobox.md +18 -0
- package/docs/popover.md +5 -4
- package/package.json +1 -1
- package/src/admin/core/OperationsHelp.tsx +10 -0
- package/src/admin/core/index.ts +6 -4
- package/src/admin/core/route-context.ts +6 -0
- package/src/admin/deployments/ApiDeploymentDetail.tsx +58 -0
- package/src/admin/deployments/DeploymentsPage.tsx +73 -0
- package/src/admin/deployments/WebAppDeploySetup.tsx +49 -0
- package/src/admin/deployments/WebAppDeploymentDetail.tsx +59 -0
- package/src/admin/deployments/WebAppDetail.tsx +104 -0
- package/src/admin/deployments/WebAppEditDialog.tsx +70 -0
- package/src/admin/deployments/WebAppKeyDialog.tsx +25 -0
- package/src/admin/deployments/WebAppOnboardingDialog.tsx +217 -0
- package/src/admin/deployments/WebAppRollbackDialog.tsx +82 -0
- package/src/admin/deployments/WebAppUploadPanel.tsx +132 -0
- package/src/admin/deployments/api.ts +158 -0
- package/src/admin/deployments/authority-state.ts +9 -0
- package/src/admin/deployments/deployment-state.ts +13 -0
- package/src/admin/deployments/index.ts +4 -0
- package/src/admin/deployments/onboarding-state.ts +11 -0
- package/src/admin/deployments/permissions.ts +37 -0
- package/src/admin/deployments/receipts.ts +78 -0
- package/src/admin/deployments/sanitize.ts +73 -0
- package/src/admin/deployments/shared.tsx +39 -0
- package/src/admin/deployments/types.ts +238 -0
- package/src/admin/deployments/upload.ts +53 -0
- package/src/admin/deployments/useDeploymentObservation.ts +104 -0
- package/src/admin/dns/DomainDetail.tsx +10 -6
- package/src/admin/dns/DomainOnboardingContent.tsx +44 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +5 -24
- package/src/admin/dns/DomainPurchaseWizard.tsx +1 -1
- package/src/admin/dns/DomainRegisterExisting.tsx +63 -0
- package/src/admin/dns/DomainRegistrarSettings.tsx +52 -0
- package/src/admin/dns/DomainsPage.tsx +22 -13
- package/src/admin/dns/SharedValidationDialog.tsx +1 -1
- package/src/admin/dns/api.ts +4 -2
- package/src/admin/dns/domain-navigation.ts +16 -0
- package/src/admin/dns/registrar-api.ts +13 -0
- package/src/admin/domains/assistant.ts +5 -4
- package/src/admin/domains/communications.ts +10 -9
- package/src/admin/domains/identity.ts +8 -7
- package/src/admin/domains/infrastructure.ts +19 -13
- package/src/admin/domains/lazy-page.ts +6 -0
- package/src/admin/domains/observability.ts +6 -5
- package/src/admin/domains/operations.ts +22 -7
- package/src/admin/domains/security.ts +16 -15
- package/src/admin/edge/EdgeHelp.tsx +10 -0
- package/src/admin/edge/ServingReadiness.tsx +54 -0
- package/src/admin/edge/VhostDetail.tsx +145 -139
- package/src/admin/edge/VhostWizard.tsx +204 -115
- package/src/admin/edge/VhostsPage.tsx +27 -28
- package/src/admin/edge/api.ts +24 -10
- package/src/admin/edge/models.ts +4 -4
- package/src/admin/edge/serving-data.ts +78 -0
- package/src/admin/fleet/CapacityEditor.tsx +81 -0
- package/src/admin/fleet/CapacityInspector.tsx +19 -0
- package/src/admin/fleet/CapacityProgress.tsx +9 -0
- package/src/admin/fleet/FleetHelp.tsx +7 -0
- package/src/admin/fleet/FleetOverview.tsx +150 -0
- package/src/admin/fleet/FleetPage.tsx +11 -0
- package/src/admin/fleet/FleetWorkspace.tsx +82 -0
- package/src/admin/fleet/ScaleFleetDialog.tsx +169 -0
- package/src/admin/fleet/access.ts +22 -0
- package/src/admin/fleet/api.ts +77 -0
- package/src/admin/fleet/data.ts +148 -0
- package/src/admin/fleet/index.ts +8 -0
- package/src/admin/fleet/operations.ts +179 -0
- package/src/admin/fleet/standalone-data.ts +87 -0
- package/src/admin/fleet/types.ts +208 -0
- package/src/admin/geolocation/GeoLocationPage.tsx +171 -0
- package/src/admin/geolocation/data.ts +138 -0
- package/src/admin/geolocation/index.ts +3 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +2 -8
- package/src/admin/index.ts +8 -0
- package/src/admin/maintenance/MaintenanceDialogs.tsx +161 -0
- package/src/admin/maintenance/MaintenancePage.tsx +442 -0
- package/src/admin/maintenance/data.ts +486 -0
- package/src/admin/maintenance/index.ts +3 -0
- package/src/admin/maintenance/operations.ts +208 -0
- package/src/admin/public/infrastructure.ts +1 -0
- package/src/admin/public/operations.ts +3 -0
- package/src/admin/registry.ts +6 -2
- package/src/admin/settings/SettingDetail.tsx +7 -2
- package/src/admin/settings/SettingEditor.tsx +2 -1
- package/src/admin/settings/SettingsPage.tsx +3 -1
- package/src/admin/settings/model.ts +1 -0
- package/src/admin/signin/SigninPage.tsx +17 -3
- package/src/admin/signin/api.ts +2 -0
- package/src/charts/MetricsChartView.tsx +4 -11
- package/src/charts/MetricsMiniWidget.tsx +3 -15
- package/src/client/client.ts +6 -0
- package/src/client/deployments-mock.ts +375 -0
- package/src/client/domain-operations-mock.ts +37 -0
- package/src/client/geo-provider-mock.ts +82 -0
- package/src/client/maintenance-mock.ts +285 -0
- package/src/client/mock.ts +52 -10
- package/src/client/operational-fleet-mock.ts +279 -0
- package/src/client/operations-mock.ts +85 -0
- package/src/client/serving-mock.ts +22 -0
- package/src/styles/components/admin-deployments.css +35 -0
- package/src/styles/components/admin-domains.css +12 -0
- package/src/styles/components/admin-edge.css +125 -1
- package/src/styles/components/admin-geolocation.css +40 -0
- package/src/styles/components/admin-maintenance.css +127 -0
- package/src/styles/components/admin-operational-fleet.css +38 -0
- package/src/styles/components/admin-operations.css +9 -0
- package/src/styles/components/admin-signin.css +2 -0
- package/src/styles/components/combobox.css +12 -2
- package/src/styles/components/fleet-page.css +9 -0
- package/src/styles/index.css +7 -0
- package/src/ui/ComboBox.tsx +39 -8
- package/src/ui/FilterBar.tsx +3 -5
- package/src/ui/ModelTable.tsx +2 -8
- package/src/ui/Popover.tsx +15 -7
- package/src/ui/date/TimePicker.tsx +4 -14
- package/src/ui/image-editor/ImageEditor.tsx +4 -5
- package/src/ui/schema-form-core.tsx +3 -5
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/** Mock-owned Maintenance provider simulation. Never performs a provider request. */
|
|
2
|
+
import { backendGlobalCan, type BackendGlobalUser } from '../admin/fleet/access';
|
|
3
|
+
import type { Finding, Framework, Report } from '../admin/maintenance/data';
|
|
4
|
+
export const MAINTENANCE_SCENARIOS = [
|
|
5
|
+
'findings',
|
|
6
|
+
'support-overdue', 'support-soon', 'later-target', 'status-missing', 'status-error',
|
|
7
|
+
'framework-cached', 'policy-lost-response', 'policy-refused', 'framework-converged', 'framework-converged-version-missing', 'framework-failed', 'framework-missing-deployment',
|
|
8
|
+
'empty',
|
|
9
|
+
'denied-inventory',
|
|
10
|
+
'unavailable',
|
|
11
|
+
'scanning-off',
|
|
12
|
+
'external',
|
|
13
|
+
'bootstrap-denied',
|
|
14
|
+
'bootstrap-malformed',
|
|
15
|
+
'next-window',
|
|
16
|
+
'target-before-settled',
|
|
17
|
+
'settled-old',
|
|
18
|
+
'mixed-cache',
|
|
19
|
+
'all-members-upgraded',
|
|
20
|
+
'provider-unknown',
|
|
21
|
+
'duplicate',
|
|
22
|
+
'cache-refusal',
|
|
23
|
+
'stale-target',
|
|
24
|
+
'reauth',
|
|
25
|
+
'framework-held',
|
|
26
|
+
'framework-pinned',
|
|
27
|
+
'framework-current',
|
|
28
|
+
'framework-unknown',
|
|
29
|
+
'framework-no-converged',
|
|
30
|
+
'framework-requires-superuser',
|
|
31
|
+
'framework-pin-cleared-503',
|
|
32
|
+
'framework-lost-response',
|
|
33
|
+
] as const;
|
|
34
|
+
export type MaintenanceScenario = (typeof MAINTENANCE_SCENARIOS)[number];
|
|
35
|
+
let scenario: MaintenanceScenario = 'findings';
|
|
36
|
+
const pending = new Map<string, { target: string; immediate: boolean; reads: number }>();
|
|
37
|
+
let frameworkRequested = false;
|
|
38
|
+
let policyValue: string | undefined;
|
|
39
|
+
let deploymentReads = 0;
|
|
40
|
+
const deploymentId = '92450000-0000-4000-8000-000000000001';
|
|
41
|
+
export function setMockMaintenanceScenario(value: MaintenanceScenario) {
|
|
42
|
+
scenario = value;
|
|
43
|
+
pending.clear();
|
|
44
|
+
frameworkRequested = false; policyValue = undefined; deploymentReads = 0;
|
|
45
|
+
}
|
|
46
|
+
export function readMaintenanceBootstrapFacts(): {
|
|
47
|
+
mode: 'managed' | 'external';
|
|
48
|
+
infrastructureManaged: boolean;
|
|
49
|
+
bootstrap: 'ok' | 'denied' | 'malformed';
|
|
50
|
+
} {
|
|
51
|
+
return {
|
|
52
|
+
mode: scenario === 'external' ? 'external' : 'managed',
|
|
53
|
+
infrastructureManaged: scenario !== 'external',
|
|
54
|
+
bootstrap:
|
|
55
|
+
scenario === 'bootstrap-denied'
|
|
56
|
+
? 'denied'
|
|
57
|
+
: scenario === 'bootstrap-malformed'
|
|
58
|
+
? 'malformed'
|
|
59
|
+
: 'ok',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const date = () => new Date().toISOString();
|
|
63
|
+
const finding = (
|
|
64
|
+
kind: Finding['kind'],
|
|
65
|
+
resource_id: string,
|
|
66
|
+
engine: string,
|
|
67
|
+
current_version: string,
|
|
68
|
+
available_major: string,
|
|
69
|
+
): Finding => ({
|
|
70
|
+
kind,
|
|
71
|
+
resource_id,
|
|
72
|
+
engine,
|
|
73
|
+
current_version,
|
|
74
|
+
available_major,
|
|
75
|
+
deadline: kind === 'elasticache' ? null : '2027-02-28T00:00:00Z',
|
|
76
|
+
extended_deadline: kind === 'elasticache' ? null : '2030-02-28T00:00:00Z',
|
|
77
|
+
days_remaining: kind === 'elasticache' ? null : scenario === 'support-overdue' ? -14 : scenario === 'support-soon' ? 30 : 130,
|
|
78
|
+
note:
|
|
79
|
+
kind === 'elasticache'
|
|
80
|
+
? 'A replication group can contain mixed member versions.'
|
|
81
|
+
: 'Review engine compatibility and snapshot recovery before applying.',
|
|
82
|
+
release_notes_url: 'https://docs.aws.amazon.com/',
|
|
83
|
+
status: 'available',
|
|
84
|
+
pending_version: null,
|
|
85
|
+
settled: true,
|
|
86
|
+
...(kind === 'elasticache' ? { members: 2, engine_versions: ['6.2', '6.0'] } : {}),
|
|
87
|
+
});
|
|
88
|
+
export function maintenanceMockReport(): Report {
|
|
89
|
+
return {
|
|
90
|
+
schema_version: 1,
|
|
91
|
+
generated_at: date(),
|
|
92
|
+
region: 'us-east-1',
|
|
93
|
+
status: scenario === 'unavailable' ? 'unavailable' : 'ok',
|
|
94
|
+
level: 5,
|
|
95
|
+
scheduled: scenario !== 'scanning-off',
|
|
96
|
+
findings: ['empty', 'unavailable'].includes(scenario)
|
|
97
|
+
? []
|
|
98
|
+
: [
|
|
99
|
+
{ ...finding('rds-instance', 'orders-db', 'postgres', scenario === 'later-target' && (pending.get('rds-instance:orders-db')?.reads ?? 0) >= 2 ? pending.get('rds-instance:orders-db')!.target : '14.12', scenario === 'later-target' && (pending.get('rds-instance:orders-db')?.reads ?? 0) >= 2 ? '17.2' : '16.4') },
|
|
100
|
+
finding('rds-cluster', 'analytics-cluster', 'aurora-postgresql', '13.12', '15.8'),
|
|
101
|
+
finding('elasticache', 'session-cache', 'redis', '6.2', '7.1'),
|
|
102
|
+
finding('elasticache', 'standalone-cache', 'redis', '6.0', '7.1'),
|
|
103
|
+
],
|
|
104
|
+
warnings:
|
|
105
|
+
scenario === 'denied-inventory'
|
|
106
|
+
? [
|
|
107
|
+
{ iam_action: 'rds:DescribeDBClusters', aws_code: 'AccessDenied' },
|
|
108
|
+
{ iam_action: 'elasticache:DescribeReplicationGroups', aws_code: 'AccessDenied' },
|
|
109
|
+
]
|
|
110
|
+
: [],
|
|
111
|
+
reason: scenario === 'unavailable' ? 'AWS credentials unavailable' : null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function maintenanceMockFramework(user: BackendGlobalUser): Framework {
|
|
115
|
+
const defaultPin = ['framework-held','framework-pin-cleared-503'].includes(scenario) ? 'hold' : scenario === 'framework-pinned' ? '4.0.0' : '';
|
|
116
|
+
const pin = policyValue ?? (frameworkRequested ? '' : defaultPin);
|
|
117
|
+
const mode = pin === '' ? 'latest' : pin === 'hold' ? 'hold' : 'pinned';
|
|
118
|
+
const unknown = scenario === 'framework-unknown';
|
|
119
|
+
const latest = unknown ? null : scenario === 'framework-current' ? '4.0.0' : '4.1.0';
|
|
120
|
+
const installed = '4.0.0';
|
|
121
|
+
const update_available = !!latest && latest !== installed && mode === 'latest';
|
|
122
|
+
const blocked_reason: Framework['blocked_reason'] = scenario === 'external' ? 'infrastructure_external' : !latest || !update_available && mode === 'latest' ? 'update_unavailable' : mode !== 'latest' && user.is_superuser !== true ? 'requires_superuser' : scenario === 'framework-no-converged' ? 'no_converged_deployment' : scenario === 'framework-requires-superuser' ? 'requires_superuser' : null;
|
|
123
|
+
return {schema_version:1, installed, latest, checked_at:date(), source:unknown?'unavailable':scenario==='framework-cached'?'cache':'pypi',update_available,pin:{mode,value:pin||null},can_update:blocked_reason===null,blocked_reason};
|
|
124
|
+
}
|
|
125
|
+
const ok = (data: unknown) => ({ status: true, data });
|
|
126
|
+
const fail = (code: string, status: number, data?: unknown) => ({
|
|
127
|
+
status: false,
|
|
128
|
+
error: code.replaceAll('_', ' '),
|
|
129
|
+
error_code: code,
|
|
130
|
+
code: status,
|
|
131
|
+
...(data ? { data } : {}),
|
|
132
|
+
});
|
|
133
|
+
export interface MaintenanceMockContext {
|
|
134
|
+
user?: (BackendGlobalUser & { is_active?: boolean; id?: number | string }) | null;
|
|
135
|
+
keyBacked?: boolean;
|
|
136
|
+
fresh?: boolean;
|
|
137
|
+
}
|
|
138
|
+
export function maintenanceMock(
|
|
139
|
+
path: string,
|
|
140
|
+
method: string,
|
|
141
|
+
body: Record<string, unknown> | undefined,
|
|
142
|
+
params: Record<string, unknown> = {},
|
|
143
|
+
context: MaintenanceMockContext = {},
|
|
144
|
+
): unknown {
|
|
145
|
+
const aws = [
|
|
146
|
+
'/api/aws/maintenance/versions',
|
|
147
|
+
'/api/aws/maintenance/status',
|
|
148
|
+
'/api/aws/maintenance/apply',
|
|
149
|
+
].includes(path),
|
|
150
|
+
framework =
|
|
151
|
+
path === '/api/account/admin/platform/framework' ||
|
|
152
|
+
path === '/api/account/admin/platform/framework/update';
|
|
153
|
+
const policy = path === '/api/account/admin/advanced/settings' && Object.hasOwn(body ?? {}, 'framework_pin');
|
|
154
|
+
const platform = path === '/api/account/admin/platform' && params.sections === 'deployments';
|
|
155
|
+
if (!aws && !framework && !policy && !platform) return undefined;
|
|
156
|
+
const user = context.user;
|
|
157
|
+
if (!user || user.is_active === false) return fail('permission_denied', 403);
|
|
158
|
+
if (!backendGlobalCan(user, aws ? 'manage_aws' : ['view_platform', 'manage_platform', 'admin']))
|
|
159
|
+
return fail('permission_denied', 403);
|
|
160
|
+
if (platform && method === 'GET') {
|
|
161
|
+
deploymentReads++;
|
|
162
|
+
const status = scenario === 'framework-failed' ? 'failed' : deploymentReads >= 2 ? 'converged' : 'fleet';
|
|
163
|
+
return ok({ schema_version: 1, sections: { deployments: { status: 'ok', data: { items: !frameworkRequested || scenario === 'framework-missing-deployment' ? [] : [{ id: deploymentId, sha: 'a'.repeat(40), status, framework_version: '4.1.0', frozen_roster: ['runner-maint-1'], node_evidence: [{runner: 'runner-maint-1', state: status === 'converged' ? 'proven' : 'dispatched', observed_at: date(), stale_after: new Date(Date.now()+60000).toISOString(), proof: {platform_deployment: deploymentId, platform_sha: 'a'.repeat(40), django_mojo_version: scenario === 'framework-converged-version-missing' ? null : '4.1.0'}}] }] } } } });
|
|
164
|
+
}
|
|
165
|
+
if (method === 'GET' && path === '/api/aws/maintenance/versions') return ok(maintenanceMockReport());
|
|
166
|
+
if (method === 'GET' && path === '/api/account/admin/platform/framework')
|
|
167
|
+
return ok(maintenanceMockFramework(user));
|
|
168
|
+
if (method === 'GET' && path === '/api/aws/maintenance/status') {
|
|
169
|
+
if (scenario === 'status-error') return fail('provider_unavailable',503);
|
|
170
|
+
if (scenario === 'status-missing') return ok({schema_version:1,kind:params.kind,resource:params.resource,target_version:params.target_version,found:false,status:null,engine_version:null,engine_versions:[],pending_version:null,settled:false,upgraded:false});
|
|
171
|
+
const f = maintenanceMockReport().findings.find(
|
|
172
|
+
(f) => f.kind === params.kind && f.resource_id === params.resource,
|
|
173
|
+
);
|
|
174
|
+
if (!f || typeof params.target_version !== 'string') return fail('invalid_request', 400);
|
|
175
|
+
const attempt = pending.get(`${f.kind}:${f.resource_id}`);
|
|
176
|
+
if (attempt) attempt.reads++;
|
|
177
|
+
const waiting =
|
|
178
|
+
['next-window', 'settled-old', 'provider-unknown'].includes(scenario) ||
|
|
179
|
+
(!!attempt && !attempt.immediate);
|
|
180
|
+
const targetObserved = !!attempt && !waiting && attempt.reads >= 2;
|
|
181
|
+
const settled =
|
|
182
|
+
targetObserved && (scenario !== 'target-before-settled' || (attempt?.reads ?? 0) >= 4);
|
|
183
|
+
const mixed = scenario === 'mixed-cache' && f.kind === 'elasticache';
|
|
184
|
+
const versions = targetObserved
|
|
185
|
+
? mixed
|
|
186
|
+
? [params.target_version, f.current_version]
|
|
187
|
+
: [params.target_version, params.target_version]
|
|
188
|
+
: [f.current_version];
|
|
189
|
+
return ok({
|
|
190
|
+
schema_version: 1,
|
|
191
|
+
kind: f.kind,
|
|
192
|
+
resource: f.resource_id,
|
|
193
|
+
target_version: params.target_version,
|
|
194
|
+
found: true,
|
|
195
|
+
status: waiting || settled ? 'available' : 'modifying',
|
|
196
|
+
engine_version: versions[0],
|
|
197
|
+
engine_versions: versions,
|
|
198
|
+
pending_version: attempt && !settled ? attempt.target : null,
|
|
199
|
+
settled: waiting || settled,
|
|
200
|
+
upgraded: targetObserved && !mixed,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
method !== 'POST' ||
|
|
205
|
+
!['/api/aws/maintenance/apply', '/api/account/admin/platform/framework/update', '/api/account/admin/advanced/settings'].includes(path)
|
|
206
|
+
)
|
|
207
|
+
return fail('method_not_allowed', 405);
|
|
208
|
+
if (context.keyBacked) return fail('permission_denied', 403);
|
|
209
|
+
if (!context.fresh || scenario === 'reauth') return fail('fresh_auth_required', 440);
|
|
210
|
+
if (!backendGlobalCan(user, ['manage_platform', 'admin'])) return fail('permission_denied', 403);
|
|
211
|
+
if (policy) {
|
|
212
|
+
if (user.is_superuser !== true) return fail('permission_denied',403);
|
|
213
|
+
const value = body?.framework_pin;
|
|
214
|
+
if (typeof value !== 'string' || !(value === '' || value === 'hold' || /^[0-9][a-z0-9._!+-]{0,63}$/.test(value))) return fail('invalid_request',400);
|
|
215
|
+
if (scenario === 'policy-refused') return fail('invalid_request',400);
|
|
216
|
+
policyValue = value;
|
|
217
|
+
if (scenario === 'policy-lost-response') return fail('service_unavailable',503);
|
|
218
|
+
return ok({schema_version:1,saved:true,value});
|
|
219
|
+
}
|
|
220
|
+
if (scenario === 'external') return fail('infrastructure_external', 403);
|
|
221
|
+
if (!body) return fail('invalid_request', 400);
|
|
222
|
+
if (aws) {
|
|
223
|
+
const f = maintenanceMockReport().findings.find(
|
|
224
|
+
(f) => f.kind === body.kind && f.resource_id === body.resource,
|
|
225
|
+
);
|
|
226
|
+
if (!f || body.confirm_resource !== body.resource || typeof body.apply_immediately !== 'boolean')
|
|
227
|
+
return fail('invalid_request', 400);
|
|
228
|
+
if (body.target_version !== f.available_major || scenario === 'stale-target')
|
|
229
|
+
return fail('upgrade_not_offered', 409);
|
|
230
|
+
const key = `${f.kind}:${f.resource_id}`;
|
|
231
|
+
if (pending.has(key) && !(scenario === 'later-target' && pending.get(key)!.reads >= 2) || scenario === 'duplicate') return fail('upgrade_in_progress', 409);
|
|
232
|
+
if (scenario === 'cache-refusal') return fail('cache_unavailable', 503);
|
|
233
|
+
pending.set(key, { target: f.available_major, immediate: body.apply_immediately, reads: 0 });
|
|
234
|
+
if (scenario === 'provider-unknown')
|
|
235
|
+
return fail('provider_unavailable', 503, {
|
|
236
|
+
failure: {
|
|
237
|
+
operation: 'rds.modify_db_instance',
|
|
238
|
+
provider_code: 'RequestTimeout',
|
|
239
|
+
retryable: true,
|
|
240
|
+
mutation_state: 'unknown',
|
|
241
|
+
iam_action: 'rds:ModifyDBInstance',
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return ok({
|
|
245
|
+
schema_version: 1,
|
|
246
|
+
requested: true,
|
|
247
|
+
resource: f.resource_id,
|
|
248
|
+
kind: f.kind,
|
|
249
|
+
engine: f.engine,
|
|
250
|
+
from_version: f.current_version,
|
|
251
|
+
target_version: f.available_major,
|
|
252
|
+
apply_immediately: body.apply_immediately,
|
|
253
|
+
release_notes_url: f.release_notes_url,
|
|
254
|
+
operation:
|
|
255
|
+
f.kind === 'elasticache'
|
|
256
|
+
? f.resource_id === 'standalone-cache'
|
|
257
|
+
? 'elasticache.modify_cache_cluster'
|
|
258
|
+
: 'elasticache.modify_replication_group'
|
|
259
|
+
: f.kind === 'rds-cluster'
|
|
260
|
+
? 'rds.modify_db_cluster'
|
|
261
|
+
: 'rds.modify_db_instance',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const fw = maintenanceMockFramework(user);
|
|
265
|
+
if (!fw.can_update || body.version !== fw.latest || body.confirm_version !== body.version)
|
|
266
|
+
return fail('update_unavailable', 409);
|
|
267
|
+
frameworkRequested = true; policyValue = ''; deploymentReads = 0;
|
|
268
|
+
if (['framework-pin-cleared-503', 'framework-lost-response'].includes(scenario))
|
|
269
|
+
return fail('deployment_coordination_unavailable', 503);
|
|
270
|
+
return ok({
|
|
271
|
+
schema_version: 1,
|
|
272
|
+
requested: true,
|
|
273
|
+
version: fw.latest,
|
|
274
|
+
cleared_pin: fw.pin.value,
|
|
275
|
+
deployment: { id: deploymentId, sha: 'a'.repeat(40), status: 'pending' },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const getMockMaintenanceScenario = () => scenario;
|
|
280
|
+
/** Demo-only provider passage: retain receipts and server operations; never replay a write. */
|
|
281
|
+
export function advanceMockMaintenance() {
|
|
282
|
+
scenario = 'all-members-upgraded';
|
|
283
|
+
for (const operation of pending.values()) { operation.immediate = true; operation.reads = Math.max(2, operation.reads); }
|
|
284
|
+
deploymentReads = Math.max(2, deploymentReads);
|
|
285
|
+
}
|
package/src/client/mock.ts
CHANGED
|
@@ -687,6 +687,7 @@ function buildSettings(): MockSetting[] {
|
|
|
687
687
|
{ id: 404, created: now - 5 * 86400, modified: now - 5 * 86400, key: 'SITE_NAME', value: 'NativeMojo fallback', is_secret: false, group: null, secretValue: null },
|
|
688
688
|
// #7729: one rendition override so Media rendering shows a changed field beside greyed defaults on first load.
|
|
689
689
|
{ id: 405, created: now - 3 * 86400, modified: now - 3 * 86400, key: 'FILEMAN_RENDITIONS_VIDEO', value: JSON.stringify({ video_hevc: { crf: 24 } }), is_secret: false, group: null, secretValue: null },
|
|
690
|
+
{ id: 406, created: now - 3 * 86400, modified: now - 3 * 86400, key: 'EDGE_FRAMEWORK_VERSION', value: '', is_secret: false, group: null, secretValue: null },
|
|
690
691
|
];
|
|
691
692
|
}
|
|
692
693
|
|
|
@@ -2894,6 +2895,12 @@ function decorateUsers(users: MockUser[], groups: MockGroup[]): void {
|
|
|
2894
2895
|
dnsPlatform.email = 'dns.platform@nativemojo.com';
|
|
2895
2896
|
dnsPlatform.display_name = 'DNS Platform Operator';
|
|
2896
2897
|
dnsPlatform.permissions = {};
|
|
2898
|
+
for (const [offset, role, grants] of [
|
|
2899
|
+
[53, 'viewer', { view_platform: true, view_dns: true }],
|
|
2900
|
+
[54, 'platform', { view_admin: true, manage_platform: true }],
|
|
2901
|
+
[55, 'webapp', { view_admin: true, manage_webapp: true, manage_dns: true, manage_groups: true }],
|
|
2902
|
+
[56, 'group', {}], [57, 'denied', {}],
|
|
2903
|
+
] as const) Object.assign(at(offset), { is_active: true, is_superuser: false, username: `deployments.${role}`, email: `deployments.${role}@nativemojo.com`, display_name: `Deployments ${role}`, permissions: { ...grants } });
|
|
2897
2904
|
const dnsTenant = at(23);
|
|
2898
2905
|
dnsTenant.is_active = true;
|
|
2899
2906
|
dnsTenant.is_superuser = false;
|
|
@@ -4109,6 +4116,7 @@ const deviceRows = buildDevices();
|
|
|
4109
4116
|
const geoIpRows = buildGeoIps();
|
|
4110
4117
|
const loginEventRows = linkLoginDevices(buildLoginEvents(users), deviceRows);
|
|
4111
4118
|
const members = buildMembers(users, groups);
|
|
4119
|
+
members.push({ id: 192, created: groups[0]!.created, modified: groups[0]!.modified, is_active: true, permissions: { manage_webapp: true, manage_dns: true }, metadata: {}, user: 56, group: 1 });
|
|
4112
4120
|
members.push({
|
|
4113
4121
|
id: 190, created: groups[0]!.created, modified: groups[0]!.modified,
|
|
4114
4122
|
is_active: true, permissions: { view_dns: true }, metadata: {},
|
|
@@ -6767,6 +6775,7 @@ let dnsConfigMalformed = false;
|
|
|
6767
6775
|
let dnsAcmeMode: 'production' | 'staging' | 'unconfigured' = 'staging';
|
|
6768
6776
|
let dnsRegistrarMode: 'ready' | 'disabled' | 'contact-missing' = 'ready';
|
|
6769
6777
|
let dnsRegistrarFault: 'failed' | 'ambiguous' | null = null;
|
|
6778
|
+
let domainRegistrarSaveFault: 'warning' | 'ambiguous' | null = null;
|
|
6770
6779
|
let dnsWriteFault: 'reject' | 'ambiguous' | 'reconcile' | null = null;
|
|
6771
6780
|
let dnsFailNextRead = false;
|
|
6772
6781
|
const mockRegistrantScopeState = new Map<string, 'saved' | 'cleared'>();
|
|
@@ -6895,6 +6904,7 @@ export function setMockDnsRegistrarMode(value: 'ready' | 'disabled' | 'contact-m
|
|
|
6895
6904
|
|
|
6896
6905
|
/** Verifier-only one-shot result after a confirmation token is consumed. */
|
|
6897
6906
|
export function armMockRegistrarPurchaseFault(value: 'failed' | 'ambiguous'): void { dnsRegistrarFault = value; }
|
|
6907
|
+
export function armMockDomainRegistrarSaveFault(value: 'warning' | 'ambiguous'): void { domainRegistrarSaveFault = value; }
|
|
6898
6908
|
|
|
6899
6909
|
/** Verifier-only one-shot DNS transport outcomes; all state still lives in db.dnsRecords. */
|
|
6900
6910
|
export function armMockDnsWriteFault(mode: 'reject' | 'ambiguous' | 'reconcile'): void {
|
|
@@ -9916,6 +9926,15 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
9916
9926
|
armedReauth = null;
|
|
9917
9927
|
return { status: false, error: 'reauth_required', error_code: 440 };
|
|
9918
9928
|
}
|
|
9929
|
+
if (path === '/api/account/admin/bootstrap' || path === '/api/account/admin/settings' || path === '/api/account/admin/advanced/settings' && Object.hasOwn(opts.body ?? {}, 'framework_pin')
|
|
9930
|
+
|| path === '/api/account/admin/dashboard' || path.startsWith('/api/aws/capacity')
|
|
9931
|
+
|| path.startsWith('/api/aws/maintenance/') || path.startsWith('/api/account/admin/platform') || path === '/api/account/admin/setup/readiness'
|
|
9932
|
+
|| path.startsWith('/api/edge/webapp') || path.startsWith('/api/edge/release') || path === '/api/edge/deployment') {
|
|
9933
|
+
const { operationsMock } = await import('./operations-mock');
|
|
9934
|
+
const claims = decodeMockJwt((opts.headers?.Authorization ?? '').replace(/^Bearer /, ''));
|
|
9935
|
+
const operationsResult = await operationsMock(path, opts, { user: userFromBearer(opts.headers), credentialKind: claims?.credential_kind ?? claims?.token_type, authTime: authTimeFromBearer(opts.headers), origin: opts.headers?.Origin });
|
|
9936
|
+
if (operationsResult !== undefined) return operationsResult;
|
|
9937
|
+
}
|
|
9919
9938
|
const pushResult = await pushFetch(path, opts);
|
|
9920
9939
|
if (pushResult !== undefined) return pushResult;
|
|
9921
9940
|
const phoneHubResult = await phoneHubFetch(path, opts);
|
|
@@ -10428,6 +10447,8 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10428
10447
|
|
|
10429
10448
|
const credentialMatch = path.match(/^\/api\/dnsman\/credential(?:\/(\d+))?$/);
|
|
10430
10449
|
if (credentialMatch) {
|
|
10450
|
+
const { prepareMockDomainCredentials } = await import('./domain-operations-mock');
|
|
10451
|
+
prepareMockDomainCredentials(db.dnsCredentials);
|
|
10431
10452
|
const caller = userFromBearer(opts.headers);
|
|
10432
10453
|
if (!caller) return permissionDenied(401);
|
|
10433
10454
|
const id = credentialMatch[1] ? Number(credentialMatch[1]) : null;
|
|
@@ -10477,6 +10498,11 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10477
10498
|
if ('privacy' in opts.body) row.privacy = Boolean(opts.body.privacy);
|
|
10478
10499
|
if ('credential' in opts.body) row.credential = opts.body.credential == null ? null : Number(opts.body.credential);
|
|
10479
10500
|
row.modified = Math.floor(Date.now() / 1000);
|
|
10501
|
+
if (row.provider === 'route53' && domainRegistrarSaveFault) {
|
|
10502
|
+
const fault = domainRegistrarSaveFault; domainRegistrarSaveFault = null;
|
|
10503
|
+
row.last_error = 'Registrar update was not confirmed. Inspect the registrar before changing this setting again.';
|
|
10504
|
+
if (fault === 'ambiguous') return { status: false, error: 'Registrar update acknowledgement unavailable', error_code: 503 };
|
|
10505
|
+
}
|
|
10480
10506
|
}
|
|
10481
10507
|
return { status: true, data: serializeDnsDomain(row), graph: 'default' };
|
|
10482
10508
|
}
|
|
@@ -10672,7 +10698,13 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10672
10698
|
const action = path.slice('/api/dnsman/registrar/'.length);
|
|
10673
10699
|
const body = opts.body ?? {};
|
|
10674
10700
|
const houseAction = ['discover', 'adopt', 'assign-group'].includes(action);
|
|
10675
|
-
if (houseAction &&
|
|
10701
|
+
if (houseAction && caller.is_superuser !== true) return permissionDenied();
|
|
10702
|
+
if (houseAction || action === 'purchase') {
|
|
10703
|
+
const { mockRegistrarSession } = await import('./domain-operations-mock');
|
|
10704
|
+
const refusal = mockRegistrarSession(decodeMockJwt((opts.headers?.Authorization ?? '').replace(/^Bearer /, '')), action === 'purchase');
|
|
10705
|
+
if (caller.is_active !== true || refusal === 'denied') return permissionDenied();
|
|
10706
|
+
if (refusal === 'stale') return { status: false, error: 'reauth_required', error_code: 440 };
|
|
10707
|
+
}
|
|
10676
10708
|
if (!houseAction) {
|
|
10677
10709
|
const groupId = Number(body.group ?? opts.params?.group ?? 0);
|
|
10678
10710
|
if (groupId > 0 ? !dnsMemberCan(caller, groupId, action !== 'search' && action !== 'suggest')
|
|
@@ -10716,14 +10748,16 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10716
10748
|
const now = Math.floor(Date.now() / 1000);
|
|
10717
10749
|
const id = Math.max(8300, ...db.domainPurchases.map((candidate) => candidate.id)) + 1;
|
|
10718
10750
|
const token = `mock-once-${id}-${Math.random().toString(36).slice(2)}`;
|
|
10719
|
-
const purchase: MockDomainPurchase = { id, created: now, modified: now, group: groupId, user: caller.id, domain_name:
|
|
10751
|
+
const purchase: MockDomainPurchase = { id, created: now, modified: now, group: groupId, user: caller.id, domain_name: answer.name, kind: 'register', status: 'quoted', price: '12.00', cost: '12.00', currency: 'USD', years, quote_expires: now + 900, operation_id: null, error: null, metadata: { confirm_digest: mockTokenDigest(token) } };
|
|
10720
10752
|
db.domainPurchases.unshift(purchase);
|
|
10721
10753
|
return { status: true, data: { purchase: id, name: purchase.domain_name, price: purchase.price, currency: purchase.currency, years: purchase.years, token, expires: purchase.quote_expires, privacy_supported: true } };
|
|
10722
10754
|
}
|
|
10723
10755
|
if (action === 'purchase') {
|
|
10756
|
+
if (method !== 'POST') return { status: false, error: 'Method not allowed', error_code: 405 };
|
|
10757
|
+
const { validMockPurchaseTuple } = await import('./domain-operations-mock');
|
|
10724
10758
|
const purchase = db.domainPurchases.find((candidate) => candidate.id === Number(body.purchase));
|
|
10725
10759
|
const digest = typeof purchase?.metadata.confirm_digest === 'string' ? purchase.metadata.confirm_digest : '';
|
|
10726
|
-
if (!purchase || mockTokenDigest(String(body.confirm_token ?? '')) !== digest || purchase.status !== 'quoted' || (purchase.quote_expires ?? 0) <= Date.now() / 1000) return { status: false, error: 'That purchase confirmation is not valid. Request a new quote and try again.', error_code: 400 };
|
|
10760
|
+
if (!purchase || !validMockPurchaseTuple(purchase, body, caller.id) || mockTokenDigest(String(body.confirm_token ?? '')) !== digest || purchase.status !== 'quoted' || (purchase.quote_expires ?? 0) <= Date.now() / 1000) return { status: false, error: 'That purchase confirmation is not valid. Request a new quote and try again.', error_code: 400 };
|
|
10727
10761
|
delete purchase.metadata.confirm_digest;
|
|
10728
10762
|
purchase.status = dnsRegistrarFault === 'failed' ? 'failed' : 'submitted'; purchase.error = dnsRegistrarFault === 'failed' ? 'Registrar rejected the operation after durable intent' : null; purchase.lifecycle_reads = dnsRegistrarFault === 'failed' ? undefined : 0; purchase.modified = Math.floor(Date.now() / 1000);
|
|
10729
10763
|
const now = purchase.modified;
|
|
@@ -10735,10 +10769,15 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10735
10769
|
return { status: true, data: { purchase: purchase.id, domain: domain.id, name: domain.name, status: domain.status, operation_id: null, privacy: true, privacy_downgraded: false } };
|
|
10736
10770
|
}
|
|
10737
10771
|
if (action === 'register-existing') {
|
|
10772
|
+
const { mockDomainRegistrationName, prepareMockDomainCredentials } = await import('./domain-operations-mock');
|
|
10773
|
+
prepareMockDomainCredentials(db.dnsCredentials);
|
|
10774
|
+
const name = mockDomainRegistrationName(body.domain);
|
|
10738
10775
|
const credential = db.dnsCredentials.find((candidate) => candidate.id === Number(body.credential));
|
|
10739
|
-
if (!credential || !credential.is_active || !credential.verified || credential.group !== Number(body.group)) return { status: false, error: 'A verified credential
|
|
10776
|
+
if (method !== 'POST' || !name || !credential || credential.provider !== 'godaddy' || !credential.is_active || !credential.verified || credential.group !== Number(body.group)) return { status: false, error: 'A same-group verified GoDaddy credential and exact domain are required', error_code: 400 };
|
|
10777
|
+
if (db.dnsDomains.some(row => row.name === name)) return { status: false, error: 'Domain is already registered', error_code: 400 };
|
|
10778
|
+
if (name.includes('provider-failure')) return { status: false, error: 'Provider ownership could not be verified', error_code: 400 };
|
|
10740
10779
|
const now = Math.floor(Date.now() / 1000);
|
|
10741
|
-
const domain: MockDnsDomain = { id: Math.max(8200, ...db.dnsDomains.map((candidate) => candidate.id)) + 1, created: now, modified: now, group: credential.group, user: caller.id, name
|
|
10780
|
+
const domain: MockDnsDomain = { id: Math.max(8200, ...db.dnsDomains.map((candidate) => candidate.id)) + 1, created: now, modified: now, group: credential.group, user: caller.id, name, provider: credential.provider, credential: credential.id, status: 'active', hosted_zone_id: null, auto_renew: false, privacy: false, verified: true, registered_on: null, expires: null, last_error: null, metadata: {} };
|
|
10742
10781
|
db.dnsDomains.push(domain); db.dnsRecords.set(domain.id, []);
|
|
10743
10782
|
return { status: true, data: serializeDnsDomain(domain) };
|
|
10744
10783
|
}
|
|
@@ -10822,11 +10861,6 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10822
10861
|
return { status: true, data: { name: domain.name, registrant: transientContact, admin: transientContact, tech: transientContact, privacy: domain.privacy, admin_privacy: domain.privacy, registrant_privacy: domain.privacy, tech_privacy: domain.privacy, auto_renew: domain.auto_renew, nameservers: ['ns-1.example.invalid', 'ns-2.example.invalid'], registrar: 'Amazon Registrar, Inc.', registered_on: domain.registered_on, expires: domain.expires, status_list: ['ok'], privacy_supported: true } };
|
|
10823
10862
|
}
|
|
10824
10863
|
// ══ end DNSMan wire ═══════════════════════════════════════════════
|
|
10825
|
-
if (path === '/api/account/admin/bootstrap') {
|
|
10826
|
-
const caller = userFromBearer(opts.headers); if (!caller) return permissionDenied(401);
|
|
10827
|
-
if (!caller.is_superuser) return permissionDenied();
|
|
10828
|
-
return { status: true, data: { admin_path: '/admin/', capabilities: { setup: true }, infrastructure: { mode: 'managed' }, features: { platform: { enabled: true, capabilities: { setup: true, view: true, capacity: true } }, settings: { enabled: true, capabilities: { owner_edit: true } } } } };
|
|
10829
|
-
}
|
|
10830
10864
|
if (path === '/api/account/admin/fleet/shared-validation/recipient') {
|
|
10831
10865
|
const caller = userFromBearer(opts.headers); if (!caller) return permissionDenied(401);
|
|
10832
10866
|
if (caller.is_superuser !== true) return permissionDenied();
|
|
@@ -10858,11 +10892,18 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10858
10892
|
const caller = userFromBearer(opts.headers);
|
|
10859
10893
|
if (!caller) return permissionDenied(401);
|
|
10860
10894
|
if (!hasGlobalPermission(caller, ['manage_settings', 'groups'])) return permissionDenied();
|
|
10895
|
+
if (method === 'GET') {
|
|
10896
|
+
const { maintenanceMockFramework } = await import('./maintenance-mock');
|
|
10897
|
+
const pin = maintenanceMockFramework(caller).pin;
|
|
10898
|
+
const row = db.settings.find(row => row.key === 'EDGE_FRAMEWORK_VERSION');
|
|
10899
|
+
if (row) row.value = pin.mode === 'latest' ? '' : pin.mode === 'hold' ? 'hold' : pin.value ?? '';
|
|
10900
|
+
}
|
|
10861
10901
|
if (oneSetting) {
|
|
10862
10902
|
const row = db.settings.find((candidate) => candidate.id === Number(oneSetting[1]));
|
|
10863
10903
|
if (!row) return { status: false, error: 'Setting not found', error_code: 404 };
|
|
10864
10904
|
if (opts.method === 'DELETE') return { status: false, error: 'DELETE not allowed: Setting', error_code: 403 };
|
|
10865
10905
|
if (opts.method === 'POST' && opts.body) {
|
|
10906
|
+
if (row.key === 'EDGE_FRAMEWORK_VERSION' || opts.body.key === 'EDGE_FRAMEWORK_VERSION') return {status:false,error:'Framework policy is protected; use its dedicated owner writer.',error_code:400};
|
|
10866
10907
|
if ('value' in opts.body && !row.is_secret) {
|
|
10867
10908
|
// Registered validators run before the row mutates (Setting.on_rest_pre_save → config.validate).
|
|
10868
10909
|
const refusal = validateRenditionSetting(typeof opts.body.key === 'string' ? opts.body.key : row.key, opts.body.value);
|
|
@@ -10881,6 +10922,7 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10881
10922
|
return { status: true, data: serializeSetting(row), graph: 'default' };
|
|
10882
10923
|
}
|
|
10883
10924
|
if (opts.method === 'POST' && opts.body) {
|
|
10925
|
+
if (opts.body.key === 'EDGE_FRAMEWORK_VERSION') return {status:false,error:'Framework policy is protected; use its dedicated owner writer.',error_code:400};
|
|
10884
10926
|
const now = Math.floor(Date.now() / 1000);
|
|
10885
10927
|
const row: MockSetting = { id: Math.max(0, ...db.settings.map((candidate) => candidate.id)) + 1, created: now, modified: now, key: '', value: '', is_secret: false, group: null, secretValue: null };
|
|
10886
10928
|
for (const [field, value] of Object.entries(opts.body)) {
|