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.
Files changed (131) hide show
  1. package/docs/README.md +7 -1
  2. package/docs/admin-deployments.md +201 -0
  3. package/docs/admin-dns-certificates.md +3 -3
  4. package/docs/admin-dns-domains.md +92 -0
  5. package/docs/admin-dns.md +4 -2
  6. package/docs/admin-edge.md +200 -196
  7. package/docs/admin-geolocation.md +100 -0
  8. package/docs/admin-maintenance.md +133 -0
  9. package/docs/admin-modals.md +17 -2
  10. package/docs/admin-operational-fleet.md +206 -0
  11. package/docs/admin-presentation.md +51 -0
  12. package/docs/admin-signin.md +12 -2
  13. package/docs/client.md +7 -0
  14. package/docs/combobox.md +18 -0
  15. package/docs/popover.md +5 -4
  16. package/package.json +1 -1
  17. package/src/admin/core/OperationsHelp.tsx +10 -0
  18. package/src/admin/core/index.ts +6 -4
  19. package/src/admin/core/route-context.ts +6 -0
  20. package/src/admin/deployments/ApiDeploymentDetail.tsx +58 -0
  21. package/src/admin/deployments/DeploymentsPage.tsx +73 -0
  22. package/src/admin/deployments/WebAppDeploySetup.tsx +49 -0
  23. package/src/admin/deployments/WebAppDeploymentDetail.tsx +59 -0
  24. package/src/admin/deployments/WebAppDetail.tsx +104 -0
  25. package/src/admin/deployments/WebAppEditDialog.tsx +70 -0
  26. package/src/admin/deployments/WebAppKeyDialog.tsx +25 -0
  27. package/src/admin/deployments/WebAppOnboardingDialog.tsx +217 -0
  28. package/src/admin/deployments/WebAppRollbackDialog.tsx +82 -0
  29. package/src/admin/deployments/WebAppUploadPanel.tsx +132 -0
  30. package/src/admin/deployments/api.ts +158 -0
  31. package/src/admin/deployments/authority-state.ts +9 -0
  32. package/src/admin/deployments/deployment-state.ts +13 -0
  33. package/src/admin/deployments/index.ts +4 -0
  34. package/src/admin/deployments/onboarding-state.ts +11 -0
  35. package/src/admin/deployments/permissions.ts +37 -0
  36. package/src/admin/deployments/receipts.ts +78 -0
  37. package/src/admin/deployments/sanitize.ts +73 -0
  38. package/src/admin/deployments/shared.tsx +39 -0
  39. package/src/admin/deployments/types.ts +238 -0
  40. package/src/admin/deployments/upload.ts +53 -0
  41. package/src/admin/deployments/useDeploymentObservation.ts +104 -0
  42. package/src/admin/dns/DomainDetail.tsx +10 -6
  43. package/src/admin/dns/DomainOnboardingContent.tsx +44 -0
  44. package/src/admin/dns/DomainOnboardingDialog.tsx +5 -24
  45. package/src/admin/dns/DomainPurchaseWizard.tsx +1 -1
  46. package/src/admin/dns/DomainRegisterExisting.tsx +63 -0
  47. package/src/admin/dns/DomainRegistrarSettings.tsx +52 -0
  48. package/src/admin/dns/DomainsPage.tsx +22 -13
  49. package/src/admin/dns/SharedValidationDialog.tsx +1 -1
  50. package/src/admin/dns/api.ts +4 -2
  51. package/src/admin/dns/domain-navigation.ts +16 -0
  52. package/src/admin/dns/registrar-api.ts +13 -0
  53. package/src/admin/domains/assistant.ts +5 -4
  54. package/src/admin/domains/communications.ts +10 -9
  55. package/src/admin/domains/identity.ts +8 -7
  56. package/src/admin/domains/infrastructure.ts +19 -13
  57. package/src/admin/domains/lazy-page.ts +6 -0
  58. package/src/admin/domains/observability.ts +6 -5
  59. package/src/admin/domains/operations.ts +22 -7
  60. package/src/admin/domains/security.ts +16 -15
  61. package/src/admin/edge/EdgeHelp.tsx +10 -0
  62. package/src/admin/edge/ServingReadiness.tsx +54 -0
  63. package/src/admin/edge/VhostDetail.tsx +145 -139
  64. package/src/admin/edge/VhostWizard.tsx +204 -115
  65. package/src/admin/edge/VhostsPage.tsx +27 -28
  66. package/src/admin/edge/api.ts +24 -10
  67. package/src/admin/edge/models.ts +4 -4
  68. package/src/admin/edge/serving-data.ts +78 -0
  69. package/src/admin/fleet/CapacityEditor.tsx +81 -0
  70. package/src/admin/fleet/CapacityInspector.tsx +19 -0
  71. package/src/admin/fleet/CapacityProgress.tsx +9 -0
  72. package/src/admin/fleet/FleetHelp.tsx +7 -0
  73. package/src/admin/fleet/FleetOverview.tsx +150 -0
  74. package/src/admin/fleet/FleetPage.tsx +11 -0
  75. package/src/admin/fleet/FleetWorkspace.tsx +82 -0
  76. package/src/admin/fleet/ScaleFleetDialog.tsx +169 -0
  77. package/src/admin/fleet/access.ts +22 -0
  78. package/src/admin/fleet/api.ts +77 -0
  79. package/src/admin/fleet/data.ts +148 -0
  80. package/src/admin/fleet/index.ts +8 -0
  81. package/src/admin/fleet/operations.ts +179 -0
  82. package/src/admin/fleet/standalone-data.ts +87 -0
  83. package/src/admin/fleet/types.ts +208 -0
  84. package/src/admin/geolocation/GeoLocationPage.tsx +171 -0
  85. package/src/admin/geolocation/data.ts +138 -0
  86. package/src/admin/geolocation/index.ts +3 -0
  87. package/src/admin/identity/members/GroupMembersPanel.tsx +2 -8
  88. package/src/admin/index.ts +8 -0
  89. package/src/admin/maintenance/MaintenanceDialogs.tsx +161 -0
  90. package/src/admin/maintenance/MaintenancePage.tsx +442 -0
  91. package/src/admin/maintenance/data.ts +486 -0
  92. package/src/admin/maintenance/index.ts +3 -0
  93. package/src/admin/maintenance/operations.ts +208 -0
  94. package/src/admin/public/infrastructure.ts +1 -0
  95. package/src/admin/public/operations.ts +3 -0
  96. package/src/admin/registry.ts +6 -2
  97. package/src/admin/settings/SettingDetail.tsx +7 -2
  98. package/src/admin/settings/SettingEditor.tsx +2 -1
  99. package/src/admin/settings/SettingsPage.tsx +3 -1
  100. package/src/admin/settings/model.ts +1 -0
  101. package/src/admin/signin/SigninPage.tsx +17 -3
  102. package/src/admin/signin/api.ts +2 -0
  103. package/src/charts/MetricsChartView.tsx +4 -11
  104. package/src/charts/MetricsMiniWidget.tsx +3 -15
  105. package/src/client/client.ts +6 -0
  106. package/src/client/deployments-mock.ts +375 -0
  107. package/src/client/domain-operations-mock.ts +37 -0
  108. package/src/client/geo-provider-mock.ts +82 -0
  109. package/src/client/maintenance-mock.ts +285 -0
  110. package/src/client/mock.ts +52 -10
  111. package/src/client/operational-fleet-mock.ts +279 -0
  112. package/src/client/operations-mock.ts +85 -0
  113. package/src/client/serving-mock.ts +22 -0
  114. package/src/styles/components/admin-deployments.css +35 -0
  115. package/src/styles/components/admin-domains.css +12 -0
  116. package/src/styles/components/admin-edge.css +125 -1
  117. package/src/styles/components/admin-geolocation.css +40 -0
  118. package/src/styles/components/admin-maintenance.css +127 -0
  119. package/src/styles/components/admin-operational-fleet.css +38 -0
  120. package/src/styles/components/admin-operations.css +9 -0
  121. package/src/styles/components/admin-signin.css +2 -0
  122. package/src/styles/components/combobox.css +12 -2
  123. package/src/styles/components/fleet-page.css +9 -0
  124. package/src/styles/index.css +7 -0
  125. package/src/ui/ComboBox.tsx +39 -8
  126. package/src/ui/FilterBar.tsx +3 -5
  127. package/src/ui/ModelTable.tsx +2 -8
  128. package/src/ui/Popover.tsx +15 -7
  129. package/src/ui/date/TimePicker.tsx +4 -14
  130. package/src/ui/image-editor/ImageEditor.tsx +4 -5
  131. 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
+ }
@@ -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 && !caller.is_superuser) return permissionDenied();
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: String(body.domain).toLowerCase(), 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) } };
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 is required', error_code: 400 };
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: String(body.domain).toLowerCase(), 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: {} };
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)) {