portal-mojo 0.2.19 → 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-credentials.md +27 -0
- package/docs/admin-deployments.md +201 -0
- package/docs/admin-dns-certificates.md +160 -0
- package/docs/admin-dns-domains.md +92 -0
- package/docs/admin-dns.md +18 -0
- package/docs/admin-edge.md +200 -196
- package/docs/admin-fleet-configuration.md +98 -2
- package/docs/admin-geolocation.md +100 -0
- package/docs/admin-identity-directories.md +12 -0
- package/docs/admin-maintenance.md +133 -0
- package/docs/admin-modals.md +45 -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/auth-pages.md +18 -6
- package/docs/auth.md +16 -2
- package/docs/client.md +7 -0
- package/docs/collection-select.md +14 -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/credentials/AcmeConsumerKeyDialog.tsx +88 -0
- package/src/admin/credentials/acme-consumer-key.ts +34 -0
- package/src/admin/credentials/group-api-keys.tsx +5 -3
- 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/CertificateDetail.tsx +8 -3
- package/src/admin/dns/CertificateRequestDialog.tsx +45 -7
- package/src/admin/dns/DomainDetail.tsx +14 -4
- 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 +26 -13
- package/src/admin/dns/SharedValidationDialog.tsx +298 -0
- package/src/admin/dns/SharedValidationPanel.tsx +76 -0
- 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/dns/shared-validation-api.ts +90 -0
- package/src/admin/dns/shared-validation-data.ts +106 -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 +27 -10
- 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/fleet-configuration/FleetConfigurationPage.tsx +14 -8
- package/src/admin/fleet-configuration/SharedValidationConnectionPanel.tsx +166 -0
- package/src/admin/fleet-configuration/data.ts +4 -0
- package/src/admin/fleet-configuration/operator-destinations.ts +20 -0
- package/src/admin/fleet-configuration/shared-validation-data.ts +143 -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/groups/group-sections/SubGroupsSection.tsx +73 -2
- package/src/admin/identity/groups/models.ts +2 -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/auth.ts +77 -25
- 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/fleet-mock.ts +73 -1
- package/src/client/geo-provider-mock.ts +82 -0
- package/src/client/index.ts +2 -2
- package/src/client/maintenance-mock.ts +285 -0
- package/src/client/mock.ts +280 -11
- 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-dns-certificates.css +33 -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/CollectionSelect.tsx +30 -8
- 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
package/src/client/mock.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AssistantSetupState, SetupCheck } from '../admin/assistant/setup';
|
|
2
|
-
import { fleetMock } from './fleet-mock';
|
|
2
|
+
import { fleetMock, configureMockHubRecipient, consumeMockConnectionFault, getMockConnectionScenario, registerMockConsumerKey, setMockConnectionScenario, connectionFail } from './fleet-mock';
|
|
3
|
+
export { setMockConnectionScenario };
|
|
4
|
+
export type { MockConnectionScenario } from './fleet-mock';
|
|
3
5
|
import { describeRenditions, validateRenditionSetting } from './renditions-mock';
|
|
4
6
|
// In-memory django-mojo mock. Speaks the EXACT wire contract the real client
|
|
5
7
|
// uses — envelope {status, data|rows..., message}, start/size paging, sort
|
|
@@ -210,6 +212,8 @@ function buildGroups(): MockGroup[] {
|
|
|
210
212
|
ORG_NAMES.forEach((orgName, oi) => {
|
|
211
213
|
const org = mk({
|
|
212
214
|
name: orgName, kind: 'org',
|
|
215
|
+
// The guided protected-purpose fixture needs one original active hub identity.
|
|
216
|
+
...(oi === 0 ? { uuid: '11111111111111111111111111111111' } : {}),
|
|
213
217
|
created: Math.floor((now - (420 - oi * 40) * 864e5) / 1000),
|
|
214
218
|
metadata: oi % 2 === 0
|
|
215
219
|
? { timezone: 'America/Los_Angeles', short_name: orgName.split(' ')[0]!.toLowerCase() }
|
|
@@ -683,6 +687,7 @@ function buildSettings(): MockSetting[] {
|
|
|
683
687
|
{ id: 404, created: now - 5 * 86400, modified: now - 5 * 86400, key: 'SITE_NAME', value: 'NativeMojo fallback', is_secret: false, group: null, secretValue: null },
|
|
684
688
|
// #7729: one rendition override so Media rendering shows a changed field beside greyed defaults on first load.
|
|
685
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 },
|
|
686
691
|
];
|
|
687
692
|
}
|
|
688
693
|
|
|
@@ -2890,6 +2895,12 @@ function decorateUsers(users: MockUser[], groups: MockGroup[]): void {
|
|
|
2890
2895
|
dnsPlatform.email = 'dns.platform@nativemojo.com';
|
|
2891
2896
|
dnsPlatform.display_name = 'DNS Platform Operator';
|
|
2892
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 } });
|
|
2893
2904
|
const dnsTenant = at(23);
|
|
2894
2905
|
dnsTenant.is_active = true;
|
|
2895
2906
|
dnsTenant.is_superuser = false;
|
|
@@ -4105,6 +4116,7 @@ const deviceRows = buildDevices();
|
|
|
4105
4116
|
const geoIpRows = buildGeoIps();
|
|
4106
4117
|
const loginEventRows = linkLoginDevices(buildLoginEvents(users), deviceRows);
|
|
4107
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 });
|
|
4108
4120
|
members.push({
|
|
4109
4121
|
id: 190, created: groups[0]!.created, modified: groups[0]!.modified,
|
|
4110
4122
|
is_active: true, permissions: { view_dns: true }, metadata: {},
|
|
@@ -5425,17 +5437,29 @@ function saveUser(user: MockUser, body: Record<string, unknown>, caller?: MockUs
|
|
|
5425
5437
|
// django-mojo account/models/group.py RestMeta: POST_SAVE_ACTIONS include
|
|
5426
5438
|
// disable/reactivate (reason REQUIRED ∈ GROUP_REST_REASONS = admin | abuse |
|
|
5427
5439
|
// archived — one more than users get); NO_SAVE_FIELDS id/pk/created.
|
|
5428
|
-
|
|
5440
|
+
// allow_member_subgroups (#9126) switches metadata.protected's sub-group
|
|
5441
|
+
// policy (#9107): a boolean only, off removes the key, and the reply is the
|
|
5442
|
+
// handler's flat {status, allow_member_subgroups}, not the row.
|
|
5443
|
+
const GROUP_ACTIONS = new Set(['disable', 'reactivate', 'allow_member_subgroups']);
|
|
5444
|
+
const GROUP_SUBGROUP_POLICY_KEY = 'allow_member_subgroups';
|
|
5429
5445
|
const GROUP_DISABLE_REASONS = new Set(['abuse', 'admin', 'archived']);
|
|
5430
5446
|
// NO_SAVE_FIELDS parity (id/pk/created); member_count additionally blocked
|
|
5431
5447
|
// here because it is a graph EXTRA, not a column (uuid IS savable live).
|
|
5432
5448
|
const GROUP_NO_SAVE = new Set(['id', 'pk', 'created', 'member_count']);
|
|
5433
5449
|
|
|
5450
|
+
// A controlled legacy wire fixture; the stored Group identity is never rewritten.
|
|
5451
|
+
function acmeProjectIdentity(group: MockGroup) {
|
|
5452
|
+
return getMockConnectionScenario() === 'legacy-project-identity' && group.id === 1 ? '74a3c6699bed4ddf' : group.uuid;
|
|
5453
|
+
}
|
|
5434
5454
|
function serializeGroup(g: MockGroup): Record<string, unknown> {
|
|
5435
|
-
return { ...g };
|
|
5455
|
+
return { ...g, uuid: acmeProjectIdentity(g) };
|
|
5436
5456
|
}
|
|
5437
5457
|
|
|
5438
5458
|
function saveGroup(group: MockGroup, body: Record<string, unknown>): unknown {
|
|
5459
|
+
// Group.on_rest_save judges the switch before anything is written.
|
|
5460
|
+
if (GROUP_SUBGROUP_POLICY_KEY in body && typeof body[GROUP_SUBGROUP_POLICY_KEY] !== 'boolean') {
|
|
5461
|
+
return { status: false, error: `${GROUP_SUBGROUP_POLICY_KEY} must be true or false`, error_code: 400 };
|
|
5462
|
+
}
|
|
5439
5463
|
const fields: Record<string, unknown> = {};
|
|
5440
5464
|
const actionEntries: [string, unknown][] = [];
|
|
5441
5465
|
for (const [key, value] of Object.entries(body)) {
|
|
@@ -5469,9 +5493,17 @@ function saveGroup(group: MockGroup, body: Record<string, unknown>): unknown {
|
|
|
5469
5493
|
target[key] = isPlainObject(value) && isPlainObject(existing) ? mergeDicts(existing, value) : value;
|
|
5470
5494
|
}
|
|
5471
5495
|
}
|
|
5496
|
+
let actionReply: Record<string, unknown> | null = null;
|
|
5472
5497
|
for (const [action, value] of actionEntries) {
|
|
5473
5498
|
const dict = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>;
|
|
5474
|
-
if (action ===
|
|
5499
|
+
if (action === GROUP_SUBGROUP_POLICY_KEY) {
|
|
5500
|
+
const metadata = isPlainObject(group.metadata) ? group.metadata : {};
|
|
5501
|
+
const protectedMeta = isPlainObject(metadata.protected) ? { ...metadata.protected } : {};
|
|
5502
|
+
if (value === true) protectedMeta[GROUP_SUBGROUP_POLICY_KEY] = true;
|
|
5503
|
+
else delete protectedMeta[GROUP_SUBGROUP_POLICY_KEY];
|
|
5504
|
+
group.metadata = { ...metadata, protected: protectedMeta };
|
|
5505
|
+
actionReply = { status: true, [GROUP_SUBGROUP_POLICY_KEY]: value };
|
|
5506
|
+
} else if (action === 'disable') {
|
|
5475
5507
|
const reason = String(dict.reason ?? '');
|
|
5476
5508
|
if (!GROUP_DISABLE_REASONS.has(reason)) {
|
|
5477
5509
|
return { status: false, error: `reason must be one of: ${[...GROUP_DISABLE_REASONS].sort().join(', ')}`, error_code: 400 };
|
|
@@ -5482,6 +5514,7 @@ function saveGroup(group: MockGroup, body: Record<string, unknown>): unknown {
|
|
|
5482
5514
|
}
|
|
5483
5515
|
}
|
|
5484
5516
|
group.modified = Math.floor(Date.now() / 1000);
|
|
5517
|
+
if (actionReply) return actionReply;
|
|
5485
5518
|
return { status: true, data: serializeGroup(group), graph: 'default' };
|
|
5486
5519
|
}
|
|
5487
5520
|
|
|
@@ -6563,10 +6596,186 @@ function accountSelfFetch(path: string, method: string, opts: MockFetchOpts): un
|
|
|
6563
6596
|
return { status: false, error: `Method not allowed: ${method} ${path}`, error_code: 405 };
|
|
6564
6597
|
}
|
|
6565
6598
|
|
|
6599
|
+
export type MockSharedValidationScenario = 'ready' | 'missing-trust' | 'revoked' | 'pending-dns' | 'changed-alias' | 'chained-alias' | 'unavailable' | 'unconfigured' | 'old-api' | 'renewal-error' | 'malformed' | 'disabled' | 'controller-maintenance';
|
|
6600
|
+
let sharedScenario: MockSharedValidationScenario = 'ready';
|
|
6601
|
+
let sharedFault: 'ambiguous' | 'reconcile' | 'reauth' | 'revoke-cleanup' | null = null;
|
|
6602
|
+
let sharedReadFails = false;
|
|
6603
|
+
let sharedReadFailure: { path: 'allocation/list' | 'trust/list' | 'capabilities'; status: 401 | 403 | 440 | 404 | 405 | 503 } | null = null;
|
|
6604
|
+
let sharedCounter = 1;
|
|
6605
|
+
const SHARED_CONSUMER_UUID = '11111111-1111-4111-8111-111111111111';
|
|
6606
|
+
const SHARED_OWNER_UUID = '22222222-2222-4222-8222-222222222222';
|
|
6607
|
+
interface MockSharedConnection { id: number; domain: string; domainId: number | null; group: number; allocation: string; client: string; participant: string | null; state: string; verified: string | null; reads: number }
|
|
6608
|
+
interface MockSharedParticipant { allocation_ref: string; participant_ref: string; client_ref: string; domain: string; source: string; target: string; role: string; trust_state: string; scope: string[]; can_publish: boolean; can_withdraw: boolean; revoked_at: string | null; consumer_project_uuid: string; consumer_label: string }
|
|
6609
|
+
const sharedConnections = new Map<string, MockSharedConnection>();
|
|
6610
|
+
const sharedParticipants: MockSharedParticipant[] = [];
|
|
6611
|
+
const sharedAllocations = [
|
|
6612
|
+
{ allocation_ref: 'shared-acme.acme-hub.example', domain: 'acme.example' },
|
|
6613
|
+
{ allocation_ref: 'shared-customer.acme-hub.example', domain: 'customer.example' },
|
|
6614
|
+
].map((v) => ({ ...v, source: `_acme-challenge.${v.domain}`, target: v.allocation_ref, owner_project_uuid: SHARED_OWNER_UUID, can_manage_trust: true }));
|
|
6615
|
+
const sharedUuid = () => `33333333-3333-4333-8333-${String(sharedCounter++).padStart(12, '0')}`;
|
|
6616
|
+
function sharedDescriptor(row: MockSharedConnection) { return { version: 1, consumer_project_uuid: SHARED_CONSUMER_UUID, client_ref: row.client, domain: row.domain, allocation_ref: row.allocation }; }
|
|
6617
|
+
function sharedParticipant(request: Record<string, unknown>): MockSharedParticipant { const domain = String(request.domain); return { allocation_ref: String(request.allocation_ref), participant_ref: sharedUuid(), client_ref: String(request.client_ref), domain, source: `_acme-challenge.${domain}`, target: String(request.allocation_ref), role: 'trusted', trust_state: 'active', scope: [domain, `*.${domain}`], can_publish: false, can_withdraw: false, revoked_at: null, consumer_project_uuid: String(request.consumer_project_uuid), consumer_label: 'Customer project · independent wildcard issuer' }; }
|
|
6618
|
+
/** Bounded mock-only controls. No setup draft or protected credential is observed. */
|
|
6619
|
+
export function setMockSharedValidationScenario(scenario: MockSharedValidationScenario): void {
|
|
6620
|
+
const allowed: MockSharedValidationScenario[] = ['ready', 'missing-trust', 'revoked', 'pending-dns', 'changed-alias', 'chained-alias', 'unavailable', 'unconfigured', 'old-api', 'renewal-error', 'malformed', 'disabled', 'controller-maintenance'];
|
|
6621
|
+
if (!allowed.includes(scenario)) throw new Error('Unknown shared validation mock scenario');
|
|
6622
|
+
sharedScenario = scenario; sharedReadFailure = null; sharedFault = null; sharedReadFails = false; sharedConnections.clear(); sharedParticipants.splice(0);
|
|
6623
|
+
sharedAllocations.splice(2);
|
|
6624
|
+
if (scenario === 'controller-maintenance') {
|
|
6625
|
+
for (let i = 1; i <= 21; i++) {
|
|
6626
|
+
const target = `maintenance-${String(i).padStart(2, '0')}.acme-hub.example`;
|
|
6627
|
+
sharedAllocations.push({ allocation_ref: target, domain: 'maintenance.example', source: '_acme-challenge.maintenance.example', target, owner_project_uuid: SHARED_OWNER_UUID, can_manage_trust: true });
|
|
6628
|
+
}
|
|
6629
|
+
for (let i = 0; i < 22; i++) {
|
|
6630
|
+
const participant = sharedParticipant({ domain: 'maintenance.example', allocation_ref: 'maintenance-21.acme-hub.example', client_ref: sharedUuid(), consumer_project_uuid: i === 21 ? SHARED_CONSUMER_UUID : sharedUuid() });
|
|
6631
|
+
participant.consumer_label = i === 21 ? 'Inactive consumer · cleanup remains available' : i === 0 ? 'Allocation owner' : `Trusted project ${i}`;
|
|
6632
|
+
if (i === 0) { participant.role = 'owner'; participant.consumer_project_uuid = SHARED_OWNER_UUID; }
|
|
6633
|
+
sharedParticipants.push(participant);
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
db.dnsDomains = db.dnsDomains.filter((d) => d.id < 8900);
|
|
6637
|
+
db.dnsCertificates = db.dnsCertificates.filter((c) => c.id < 8900);
|
|
6638
|
+
if (['missing-trust', 'revoked', 'pending-dns', 'changed-alias', 'chained-alias', 'renewal-error'].includes(scenario)) {
|
|
6639
|
+
const row: MockSharedConnection = { id: 8901, domain: 'acme.example', domainId: 8201, group: 1, allocation: 'shared-acme.acme-hub.example', client: sharedUuid(), participant: null, state: 'pending', verified: null, reads: 0 };
|
|
6640
|
+
if (scenario !== 'missing-trust') { const participant = sharedParticipant(sharedDescriptor(row)); if (scenario === 'revoked') { participant.trust_state = 'revoked'; participant.revoked_at = new Date().toISOString(); } sharedParticipants.push(participant); row.participant = participant.participant_ref; row.state = 'verified'; row.verified = new Date().toISOString(); }
|
|
6641
|
+
sharedConnections.set(row.domain, row);
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
export function armMockSharedValidationFault(fault: typeof sharedFault): void { sharedFault = fault; if (fault === null) sharedReadFails = false; }
|
|
6645
|
+
export function armMockSharedValidationReadFailure(path: NonNullable<typeof sharedReadFailure>['path'], status: NonNullable<typeof sharedReadFailure>['status']): void { sharedReadFailure = { path, status }; }
|
|
6646
|
+
function mockSharedValidation(path: string, opts: MockFetchOpts, method: string): unknown {
|
|
6647
|
+
const caller = userFromBearer(opts.headers); if (!caller) return permissionDenied(401);
|
|
6648
|
+
if (sharedScenario === 'old-api') return { status: false, error: 'Route unavailable', error_code: 404 };
|
|
6649
|
+
const body = opts.body ?? {}, params = opts.params ?? {};
|
|
6650
|
+
const failure = (code: string, status = 409, data?: unknown) => ({ status: false, code: status, error: code, error_code: code, ...(data ? { data } : {}) });
|
|
6651
|
+
const manage = hasGlobalPermission(caller, DNS_MANAGE_GRANTS);
|
|
6652
|
+
const controller = caller.is_superuser === true;
|
|
6653
|
+
const iso = (value: number | null) => value == null ? null : new Date(value * 1000).toISOString();
|
|
6654
|
+
const at = new Date().toISOString();
|
|
6655
|
+
if (path === '/api/dnsman/delegation/group-choice') {
|
|
6656
|
+
if (method !== 'GET' || !manage) return permissionDenied();
|
|
6657
|
+
const rows = db.groups.filter((g) => g.is_active && dnsMemberCan(caller, g.id, true) && (params.id == null || g.id === Number(params.id)) && (!params.search || g.name.toLowerCase().includes(String(params.search).toLowerCase())));
|
|
6658
|
+
const start = Math.max(0, Number(params.start ?? 0)); const size = Math.min(50, Math.max(1, Number(params.size ?? 25)));
|
|
6659
|
+
return { status: true, data: rows.slice(start, start + size).map((g) => ({ id: g.id, name: g.name })), count: rows.length, start, size };
|
|
6660
|
+
}
|
|
6661
|
+
if (path.startsWith('/api/dnsman/acme/')) {
|
|
6662
|
+
if (method !== 'POST' || !controller) return permissionDenied();
|
|
6663
|
+
if (sharedReadFailure && path.endsWith(`/${sharedReadFailure.path}`)) { const status = sharedReadFailure.status; sharedReadFailure = null; return failure('mock_controller_read_refused', status); }
|
|
6664
|
+
if (path.endsWith('/capabilities')) return { status: true, data: { version: 2, shared_validation: sharedScenario !== 'disabled', consumer_project_uuid: null, can_consume: false, can_manage_trust: sharedScenario !== 'disabled', trust_scope: 'apex_and_wildcard' } };
|
|
6665
|
+
if (sharedScenario === 'disabled') return failure('acme_not_authorized', 403);
|
|
6666
|
+
const page = <T,>(rows: T[]) => { const start = body.cursor == null ? 0 : Number(body.cursor); if (!Number.isSafeInteger(start) || start < 0 || (body.cursor != null && !/^[1-9]\d*$/.test(String(body.cursor)))) return failure('acme_invalid_request', 400); return { status: true, data: { items: rows.slice(start, start + 20), next_cursor: start + 20 < rows.length ? String(start + 20) : null } }; };
|
|
6667
|
+
if (path.endsWith('/allocation/list')) return page(sharedAllocations.filter((v) => v.domain === body.domain));
|
|
6668
|
+
const allocation = sharedAllocations.find((v) => v.allocation_ref === body.allocation_ref || v.allocation_ref === (body.connection_request as Record<string, unknown> | undefined)?.allocation_ref);
|
|
6669
|
+
if (!allocation) return permissionDenied();
|
|
6670
|
+
if (path.endsWith('/trust/list')) return sharedReadFails ? failure('acme_hub_unavailable', 503) : page(sharedParticipants.filter((p) => p.allocation_ref === allocation.allocation_ref));
|
|
6671
|
+
if (path.endsWith('/trust/recipient')) return sharedScenario !== 'controller-maintenance' && body.consumer_project_uuid === SHARED_CONSUMER_UUID ? { status: true, data: { consumer_project_uuid: SHARED_CONSUMER_UUID, label: 'Customer project · independent wildcard issuer', active: true } } : permissionDenied();
|
|
6672
|
+
if (path.endsWith('/trust/grant')) {
|
|
6673
|
+
const request = body.connection_request as Record<string, unknown>;
|
|
6674
|
+
if (sharedScenario === 'controller-maintenance') return permissionDenied();
|
|
6675
|
+
if (!request || Object.keys(request).sort().join(',') !== 'allocation_ref,client_ref,consumer_project_uuid,domain,version' || request.version !== 1 || request.domain !== allocation.domain || request.consumer_project_uuid !== SHARED_CONSUMER_UUID || !/^[a-f0-9-]{36}$/.test(String(request.client_ref))) return failure('acme_reference_conflict');
|
|
6676
|
+
let participant = sharedParticipants.find((p) => p.client_ref === request.client_ref && p.consumer_project_uuid === request.consumer_project_uuid);
|
|
6677
|
+
if (participant && (participant.allocation_ref !== allocation.allocation_ref || participant.trust_state === 'revoked')) return failure('acme_reference_conflict');
|
|
6678
|
+
if (['changed-alias', 'chained-alias', 'pending-dns'].includes(sharedScenario)) return failure('acme_dns_mismatch');
|
|
6679
|
+
if (!participant) { participant = sharedParticipant(request); sharedParticipants.push(participant); }
|
|
6680
|
+
if (sharedFault === 'ambiguous') { sharedFault = null; return failure('acme_hub_unavailable', 503); }
|
|
6681
|
+
return { status: true, data: { ...participant } };
|
|
6682
|
+
}
|
|
6683
|
+
if (path.endsWith('/trust/revoke')) {
|
|
6684
|
+
const participant = sharedParticipants.find((p) => p.allocation_ref === allocation.allocation_ref && p.participant_ref === body.participant_ref);
|
|
6685
|
+
if (!participant || participant.role === 'owner') return permissionDenied();
|
|
6686
|
+
participant.trust_state = 'revoked'; participant.revoked_at ??= at;
|
|
6687
|
+
if (sharedFault === 'revoke-cleanup' || sharedFault === 'ambiguous') { sharedFault = null; return failure('acme_hub_unavailable', 503); }
|
|
6688
|
+
return { status: true, data: { ...participant } };
|
|
6689
|
+
}
|
|
6690
|
+
return failure('acme_invalid_request', 400);
|
|
6691
|
+
}
|
|
6692
|
+
const write = method === 'POST';
|
|
6693
|
+
if (sharedFault === 'reauth' && write) { sharedFault = null; return { status: false, code: 440, error: 'reauth_required', error_code: 440 }; }
|
|
6694
|
+
let row = body.delegation == null ? null : [...sharedConnections.values()].find((v) => v.id === Number(body.delegation)) ?? null;
|
|
6695
|
+
const requested = write ? body : params;
|
|
6696
|
+
let domain = db.dnsDomains.find((d) => requested.domain != null ? d.id === Number(requested.domain) : d.name === (row?.domain ?? requested.name));
|
|
6697
|
+
const name = row?.domain ?? domain?.name ?? String(requested.name ?? '');
|
|
6698
|
+
const groupId = row?.group ?? domain?.group ?? Number(requested.group ?? 0);
|
|
6699
|
+
if (!name || !/^[a-z0-9.-]+\.[a-z]{2,}$/.test(name) || (!domain && !db.groups.some((g) => g.id === groupId && g.is_active))) return failure('invalid_target', 400);
|
|
6700
|
+
if (domain && requested.group != null && domain.group !== Number(requested.group)) return permissionDenied();
|
|
6701
|
+
if (!hasGlobalPermission(caller, DNS_VIEW_GRANTS) || (groupId && !dnsMemberCan(caller, groupId, write))) return permissionDenied();
|
|
6702
|
+
if (write && (!manage || !groupId)) return permissionDenied();
|
|
6703
|
+
row ??= sharedConnections.get(name) ?? null;
|
|
6704
|
+
const allocation = row?.allocation ?? (typeof requested.allocation_ref === 'string' ? requested.allocation_ref : null);
|
|
6705
|
+
const plan = () => {
|
|
6706
|
+
if (sharedReadFails) return failure('hub_unavailable', 503);
|
|
6707
|
+
const participant = row ? sharedParticipants.find((p) => p.client_ref === row!.client) : null;
|
|
6708
|
+
const trust = !row ? 'not_requested' : participant?.trust_state ?? 'missing';
|
|
6709
|
+
const hub = sharedScenario === 'unconfigured' ? 'unconfigured' : sharedScenario === 'unavailable' ? 'unavailable' : sharedScenario === 'disabled' ? 'unsupported' : 'ready';
|
|
6710
|
+
const aliasState = ['changed-alias', 'chained-alias'].includes(sharedScenario) ? 'mismatch' : sharedScenario === 'pending-dns' ? 'lookup_failed' : row?.participant ? 'match' : 'unknown';
|
|
6711
|
+
let cert = db.dnsCertificates.find((c) => c.domain === domain?.id && c.sans.length === 2 && c.sans.includes(name) && c.sans.includes(`*.${name}`));
|
|
6712
|
+
if (cert?.id && cert.id >= 8900 && cert.status === 'pending' && row && ++row.reads >= 3) { cert.status = 'active'; cert.not_after = Math.floor(Date.now() / 1000) + 90 * 86400; cert.renew_after = Math.floor(Date.now() / 1000) + 60 * 86400; cert.material_present = true; cert.attempts = 1; }
|
|
6713
|
+
const valid = !!cert && cert.status === 'active' && (cert.not_after ?? 0) > Date.now() / 1000;
|
|
6714
|
+
const renewal = sharedScenario === 'renewal-error' ? 'failed' : cert?.status === 'pending' ? 'queued' : valid ? (trust === 'active' && aliasState === 'match' ? 'scheduled' : row ? 'blocked' : 'scheduled') : cert ? 'failed' : 'not_requested';
|
|
6715
|
+
let next = 'none', actor = 'project_operator';
|
|
6716
|
+
if (!groupId) { next = 'unsupported_domain_scope'; actor = 'deployment_operator'; }
|
|
6717
|
+
else if (hub !== 'ready') { next = hub === 'unconfigured' ? 'configure_hub' : hub === 'unsupported' ? 'upgrade_hub' : 'retry_hub'; actor = 'deployment_operator'; }
|
|
6718
|
+
else if (!row) next = allocation ? 'prepare_connection' : 'choose_allocation';
|
|
6719
|
+
else if (trust === 'revoked') next = 'restart_connection';
|
|
6720
|
+
else if (trust === 'missing') { next = 'grant_trust'; actor = 'allocation_controller'; }
|
|
6721
|
+
else if (!row.participant) next = 'connect';
|
|
6722
|
+
else if (aliasState !== 'match') { next = 'repair_alias'; actor = 'domain_owner'; }
|
|
6723
|
+
else if (row.state !== 'verified') next = 'check';
|
|
6724
|
+
else if (renewal === 'queued') next = 'wait_for_job';
|
|
6725
|
+
else if (renewal === 'failed') next = 'inspect_failure';
|
|
6726
|
+
else if (!cert) next = 'request';
|
|
6727
|
+
else actor = 'none';
|
|
6728
|
+
const allowed = manage && !!groupId;
|
|
6729
|
+
return { status: true, data: {
|
|
6730
|
+
version: sharedScenario === 'malformed' ? 99 : 1, checked_at: at,
|
|
6731
|
+
domain: { id: domain?.id ?? null, name, provider: domain?.provider ?? null, group: groupId ? { id: groupId, name: db.groups.find((g) => g.id === groupId)?.name ?? 'Customer' } : null },
|
|
6732
|
+
profile: { name: 'apex_wildcard', names: [name, `*.${name}`] }, hub: { state: hub, protocol_version: hub === 'ready' ? 2 : null, consumer_project_uuid: hub === 'ready' ? SHARED_CONSUMER_UUID : null },
|
|
6733
|
+
connection: row ? { id: row.id, mode: 'shared', state: row.state, client_ref: row.client, allocation_ref: row.allocation, participant_ref: row.participant, verified_at: row.verified } : null,
|
|
6734
|
+
trust: { state: trust, can_publish: trust === 'active', can_withdraw: !!participant },
|
|
6735
|
+
alias: { state: aliasState, source: `_acme-challenge.${name}`, target: allocation, observed_target: aliasState === 'unknown' || aliasState === 'lookup_failed' ? null : sharedScenario === 'changed-alias' ? 'changed.acme-hub.example' : allocation, one_hop: aliasState === 'unknown' || aliasState === 'lookup_failed' ? null : sharedScenario !== 'chained-alias', checked_at: aliasState === 'unknown' ? null : at },
|
|
6736
|
+
certificate: cert ? { id: cert.id, state: cert.status, names: cert.sans, not_after: iso(cert.not_after), renew_after: iso(cert.renew_after), serving_valid: valid, material_state: cert.material_present ? 'available' : 'missing' } : null,
|
|
6737
|
+
renewal: { state: renewal, next_attempt_at: cert ? iso(cert.renew_after) : null, attempts: cert?.attempts ?? 0, error_code: renewal === 'failed' ? 'trust_missing' : null, cleanup_pending: false, job: renewal === 'queued' ? { id: 'mock-shared-job', state: 'pending' } : null, worker_version: null },
|
|
6738
|
+
capabilities: { group_choices: allowed, can_connect: allowed && hub === 'ready' && !!allocation && trust !== 'revoked', can_restart: allowed && !!row && (trust === 'revoked' || (!row.participant && row.state === 'pending')), can_check: allowed && !!row?.participant && trust === 'active', can_request: allowed && row?.state === 'verified' && trust === 'active' && aliasState === 'match' && sharedScenario !== 'chained-alias' },
|
|
6739
|
+
next_action: { code: next, actor }, connection_request: row && allowed ? sharedDescriptor(row) : null, operation: { action: 'plan', outcome: 'observed', changed: false },
|
|
6740
|
+
} };
|
|
6741
|
+
};
|
|
6742
|
+
if (!write) return plan();
|
|
6743
|
+
if (sharedScenario === 'unconfigured' || sharedScenario === 'unavailable' || sharedScenario === 'disabled') return failure('hub_unavailable', 503);
|
|
6744
|
+
let sharedOutcome = 'queued';
|
|
6745
|
+
if (path.endsWith('/connect')) {
|
|
6746
|
+
if (!allocation || !/^[a-z0-9.-]+\.[a-z]{2,}$/.test(allocation)) return failure('allocation_conflict');
|
|
6747
|
+
if (row && row.allocation !== allocation) return failure('allocation_conflict');
|
|
6748
|
+
const existing = row ? sharedParticipants.find((p) => p.client_ref === row!.client) : null;
|
|
6749
|
+
if (existing?.trust_state === 'revoked' && body.restart !== true) return failure('trust_revoked', 409, (plan() as { data: unknown }).data);
|
|
6750
|
+
if (body.restart === true && row && existing?.trust_state === 'active') return failure('stale_connection');
|
|
6751
|
+
if (!row || body.restart === true) { row = { id: 8900 + sharedCounter, domain: name, domainId: domain?.id ?? null, group: groupId, allocation, client: sharedUuid(), participant: null, state: 'pending', verified: null, reads: 0 }; sharedConnections.set(name, row); }
|
|
6752
|
+
const participant = sharedParticipants.find((p) => p.client_ref === row!.client && p.trust_state === 'active');
|
|
6753
|
+
if (participant) row.participant = participant.participant_ref;
|
|
6754
|
+
} else if (path.endsWith('/check')) {
|
|
6755
|
+
if (!row || !row.participant) return failure('connection_not_verified');
|
|
6756
|
+
const participant = sharedParticipants.find((p) => p.participant_ref === row!.participant);
|
|
6757
|
+
if (participant?.trust_state !== 'active') return failure('trust_revoked');
|
|
6758
|
+
if (['pending-dns', 'changed-alias', 'chained-alias'].includes(sharedScenario)) { row.state = 'broken'; return failure('alias_mismatch', 409, (plan() as { data: unknown }).data); }
|
|
6759
|
+
if (!domain) { domain = { ...buildDnsDomains()[0]!, id: 8900 + sharedCounter++, name, group: groupId, provider: 'mojo', credential: null, hosted_zone_id: null }; db.dnsDomains.push(domain); row.domainId = domain.id; }
|
|
6760
|
+
row.state = 'verified'; row.verified = at;
|
|
6761
|
+
} else if (path.endsWith('/request')) {
|
|
6762
|
+
const current = plan() as { data: { capabilities: { can_request: boolean } } };
|
|
6763
|
+
if (!row || !domain || !current.data?.capabilities.can_request) return failure('connection_not_verified');
|
|
6764
|
+
let cert = db.dnsCertificates.find((c) => c.domain === domain!.id && c.sans.length === 2 && c.sans.includes(name) && c.status !== 'revoked');
|
|
6765
|
+
sharedOutcome = cert?.status === 'active' ? 'already_current' : cert ? 'already_queued' : 'queued';
|
|
6766
|
+
if (!cert) { const now = Math.floor(Date.now() / 1000); cert = { id: 8900 + sharedCounter++, created: now, modified: now, domain: domain.id, common_name: name, sans: [name, `*.${name}`], status: 'pending', issuer: null, serial: null, not_before: null, not_after: null, renew_after: null, last_error: null, attempts: 0, material_present: false }; db.dnsCertificates.push(cert); }
|
|
6767
|
+
} else return failure('invalid_action', 400);
|
|
6768
|
+
const result = plan() as { data: { operation: unknown } };
|
|
6769
|
+
result.data.operation = { action: path.split('/').pop(), outcome: path.endsWith('/connect') ? row?.participant ? 'connected' : 'awaiting_trust' : path.endsWith('/check') ? 'verified' : sharedOutcome, changed: path.endsWith('/request') ? sharedOutcome === 'queued' : true };
|
|
6770
|
+
if (sharedFault === 'ambiguous' || sharedFault === 'reconcile') { sharedReadFails = sharedFault === 'reconcile'; sharedFault = null; return failure('hub_unavailable', 503); }
|
|
6771
|
+
return result;
|
|
6772
|
+
}
|
|
6773
|
+
|
|
6566
6774
|
let dnsConfigMalformed = false;
|
|
6567
6775
|
let dnsAcmeMode: 'production' | 'staging' | 'unconfigured' = 'staging';
|
|
6568
6776
|
let dnsRegistrarMode: 'ready' | 'disabled' | 'contact-missing' = 'ready';
|
|
6569
6777
|
let dnsRegistrarFault: 'failed' | 'ambiguous' | null = null;
|
|
6778
|
+
let domainRegistrarSaveFault: 'warning' | 'ambiguous' | null = null;
|
|
6570
6779
|
let dnsWriteFault: 'reject' | 'ambiguous' | 'reconcile' | null = null;
|
|
6571
6780
|
let dnsFailNextRead = false;
|
|
6572
6781
|
const mockRegistrantScopeState = new Map<string, 'saved' | 'cleared'>();
|
|
@@ -6695,6 +6904,7 @@ export function setMockDnsRegistrarMode(value: 'ready' | 'disabled' | 'contact-m
|
|
|
6695
6904
|
|
|
6696
6905
|
/** Verifier-only one-shot result after a confirmation token is consumed. */
|
|
6697
6906
|
export function armMockRegistrarPurchaseFault(value: 'failed' | 'ambiguous'): void { dnsRegistrarFault = value; }
|
|
6907
|
+
export function armMockDomainRegistrarSaveFault(value: 'warning' | 'ambiguous'): void { domainRegistrarSaveFault = value; }
|
|
6698
6908
|
|
|
6699
6909
|
/** Verifier-only one-shot DNS transport outcomes; all state still lives in db.dnsRecords. */
|
|
6700
6910
|
export function armMockDnsWriteFault(mode: 'reject' | 'ambiguous' | 'reconcile'): void {
|
|
@@ -9716,6 +9926,15 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
9716
9926
|
armedReauth = null;
|
|
9717
9927
|
return { status: false, error: 'reauth_required', error_code: 440 };
|
|
9718
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
|
+
}
|
|
9719
9938
|
const pushResult = await pushFetch(path, opts);
|
|
9720
9939
|
if (pushResult !== undefined) return pushResult;
|
|
9721
9940
|
const phoneHubResult = await phoneHubFetch(path, opts);
|
|
@@ -10106,6 +10325,7 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10106
10325
|
}
|
|
10107
10326
|
// ══ end network security wire ════════════════════════════════════
|
|
10108
10327
|
// ══ DNSMan — capabilities, safe models and custom operations ════════
|
|
10328
|
+
if (path.startsWith('/api/dnsman/delegation/') || path.startsWith('/api/dnsman/acme/')) return mockSharedValidation(path, opts, method);
|
|
10109
10329
|
if (path === '/api/dnsman/config') {
|
|
10110
10330
|
const caller = userFromBearer(opts.headers);
|
|
10111
10331
|
if (!caller) return permissionDenied(401);
|
|
@@ -10227,6 +10447,8 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10227
10447
|
|
|
10228
10448
|
const credentialMatch = path.match(/^\/api\/dnsman\/credential(?:\/(\d+))?$/);
|
|
10229
10449
|
if (credentialMatch) {
|
|
10450
|
+
const { prepareMockDomainCredentials } = await import('./domain-operations-mock');
|
|
10451
|
+
prepareMockDomainCredentials(db.dnsCredentials);
|
|
10230
10452
|
const caller = userFromBearer(opts.headers);
|
|
10231
10453
|
if (!caller) return permissionDenied(401);
|
|
10232
10454
|
const id = credentialMatch[1] ? Number(credentialMatch[1]) : null;
|
|
@@ -10276,6 +10498,11 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10276
10498
|
if ('privacy' in opts.body) row.privacy = Boolean(opts.body.privacy);
|
|
10277
10499
|
if ('credential' in opts.body) row.credential = opts.body.credential == null ? null : Number(opts.body.credential);
|
|
10278
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
|
+
}
|
|
10279
10506
|
}
|
|
10280
10507
|
return { status: true, data: serializeDnsDomain(row), graph: 'default' };
|
|
10281
10508
|
}
|
|
@@ -10428,6 +10655,7 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10428
10655
|
const domain = db.dnsDomains.find((candidate) => candidate.id === Number(body.domain));
|
|
10429
10656
|
if (!domain) return { status: false, error: 'Domain not found', error_code: 404 };
|
|
10430
10657
|
if (domain.group == null ? !hasGlobalPermission(caller, DNS_MANAGE_GRANTS) : !dnsMemberCan(caller, domain.group, true)) return permissionDenied();
|
|
10658
|
+
if (sharedConnections.has(domain.name)) return { status: false, error: 'Shared validation forbids direct fallback', error_code: 409 };
|
|
10431
10659
|
if (dnsAcmeMode === 'unconfigured') return { status: false, error: 'ACME is not configured', error_code: 400 };
|
|
10432
10660
|
if (domain.status !== 'active') return { status: false, error: 'Certificates require an active domain', error_code: 400 };
|
|
10433
10661
|
const sticky = db.acmeDelegations.find((candidate) => candidate.domain === domain.id && candidate.verified_at != null && candidate.state !== 'retired');
|
|
@@ -10470,7 +10698,13 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10470
10698
|
const action = path.slice('/api/dnsman/registrar/'.length);
|
|
10471
10699
|
const body = opts.body ?? {};
|
|
10472
10700
|
const houseAction = ['discover', 'adopt', 'assign-group'].includes(action);
|
|
10473
|
-
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
|
+
}
|
|
10474
10708
|
if (!houseAction) {
|
|
10475
10709
|
const groupId = Number(body.group ?? opts.params?.group ?? 0);
|
|
10476
10710
|
if (groupId > 0 ? !dnsMemberCan(caller, groupId, action !== 'search' && action !== 'suggest')
|
|
@@ -10514,14 +10748,16 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10514
10748
|
const now = Math.floor(Date.now() / 1000);
|
|
10515
10749
|
const id = Math.max(8300, ...db.domainPurchases.map((candidate) => candidate.id)) + 1;
|
|
10516
10750
|
const token = `mock-once-${id}-${Math.random().toString(36).slice(2)}`;
|
|
10517
|
-
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) } };
|
|
10518
10752
|
db.domainPurchases.unshift(purchase);
|
|
10519
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 } };
|
|
10520
10754
|
}
|
|
10521
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');
|
|
10522
10758
|
const purchase = db.domainPurchases.find((candidate) => candidate.id === Number(body.purchase));
|
|
10523
10759
|
const digest = typeof purchase?.metadata.confirm_digest === 'string' ? purchase.metadata.confirm_digest : '';
|
|
10524
|
-
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 };
|
|
10525
10761
|
delete purchase.metadata.confirm_digest;
|
|
10526
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);
|
|
10527
10763
|
const now = purchase.modified;
|
|
@@ -10533,10 +10769,15 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10533
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 } };
|
|
10534
10770
|
}
|
|
10535
10771
|
if (action === 'register-existing') {
|
|
10772
|
+
const { mockDomainRegistrationName, prepareMockDomainCredentials } = await import('./domain-operations-mock');
|
|
10773
|
+
prepareMockDomainCredentials(db.dnsCredentials);
|
|
10774
|
+
const name = mockDomainRegistrationName(body.domain);
|
|
10536
10775
|
const credential = db.dnsCredentials.find((candidate) => candidate.id === Number(body.credential));
|
|
10537
|
-
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 };
|
|
10538
10779
|
const now = Math.floor(Date.now() / 1000);
|
|
10539
|
-
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: {} };
|
|
10540
10781
|
db.dnsDomains.push(domain); db.dnsRecords.set(domain.id, []);
|
|
10541
10782
|
return { status: true, data: serializeDnsDomain(domain) };
|
|
10542
10783
|
}
|
|
@@ -10620,9 +10861,29 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10620
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 } };
|
|
10621
10862
|
}
|
|
10622
10863
|
// ══ end DNSMan wire ═══════════════════════════════════════════════
|
|
10864
|
+
if (path === '/api/account/admin/fleet/shared-validation/recipient') {
|
|
10865
|
+
const caller = userFromBearer(opts.headers); if (!caller) return permissionDenied(401);
|
|
10866
|
+
if (caller.is_superuser !== true) return permissionDenied();
|
|
10867
|
+
const input = method === 'GET' ? opts.params ?? {} : opts.body ?? {};
|
|
10868
|
+
if (method === 'GET' ? Object.keys(input).some((k) => k !== 'consumer_project_uuid') : method !== 'POST' || Object.keys(input).sort().join(',') !== 'consumer_project_uuid,expected_consumer_label,label,purpose' || input.purpose !== 'acme_validation_consumer') return connectionFail('invalid_request', 400);
|
|
10869
|
+
const group = db.groups.find((g) => acmeProjectIdentity(g) === input.consumer_project_uuid && isEffectivelyActive(g));
|
|
10870
|
+
if (!group) return connectionFail('recipient_unavailable', 404);
|
|
10871
|
+
const scenario = getMockConnectionScenario();
|
|
10872
|
+
if (method === 'GET') return { status: true, data: { purpose: 'acme_validation_consumer', consumer_project_uuid: acmeProjectIdentity(group), consumer_label: group.name, permission: 'dnsman_acme_federation', can_issue: scenario !== 'recipient-denied', grant_explanation: 'Protected platform grant required; issuance does not grant allocation trust.' } };
|
|
10873
|
+
if (consumeMockConnectionFault('reauth')) return connectionFail('fresh_auth_required', 440);
|
|
10874
|
+
if (scenario === 'recipient-denied') return permissionDenied();
|
|
10875
|
+
if (scenario === 'recipient-renamed' || input.expected_consumer_label !== group.name) return connectionFail('recipient_changed', 409);
|
|
10876
|
+
if (typeof input.label !== 'string' || !input.label.trim() || input.label.length > 200 || /[\x00-\x1f]/.test(input.label)) return connectionFail('invalid_request', 400);
|
|
10877
|
+
const id = Math.max(0, ...db.groupApiKeys.map((k) => k.id)) + 1, now = Math.floor(Date.now() / 1000), secret = `mock_acme_${mockHex32(mulberry32(id))}`;
|
|
10878
|
+
const row: MockGroupApiKey = { id, group: group.id, user: null, created: now, modified: now, name: input.label.trim(), is_active: true, permissions: { dnsman_acme_federation: true }, limits: {}, last_used: null, expires_at: null, metadata: {}, override_user: false, token: secret };
|
|
10879
|
+
db.groupApiKeys.unshift(row); registerMockConsumerKey(secret, acmeProjectIdentity(group)!);
|
|
10880
|
+
if (consumeMockConnectionFault('issue-uncertain')) return connectionFail('publication_uncertain', 503);
|
|
10881
|
+
return { status: true, data: { purpose: 'acme_validation_consumer', consumer_project_uuid: acmeProjectIdentity(group), consumer_label: group.name, api_key: { id, name: row.name, is_active: true, permission: 'dnsman_acme_federation' }, token: secret } };
|
|
10882
|
+
}
|
|
10623
10883
|
if (path === '/api/account/admin/fleet' || path.startsWith('/api/account/admin/fleet/')) {
|
|
10624
10884
|
const caller = userFromBearer(opts.headers);
|
|
10625
10885
|
if (!caller) return permissionDenied(401);
|
|
10886
|
+
const original = db.groups.find((group) => group.id === 1); if (original?.uuid) configureMockHubRecipient(acmeProjectIdentity(original)!, original.name);
|
|
10626
10887
|
return fleetMock(path, method, opts.body, caller.is_superuser === true);
|
|
10627
10888
|
}
|
|
10628
10889
|
// ── Settings — plaintext stays private; secrets serialize masked ─────
|
|
@@ -10631,11 +10892,18 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10631
10892
|
const caller = userFromBearer(opts.headers);
|
|
10632
10893
|
if (!caller) return permissionDenied(401);
|
|
10633
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
|
+
}
|
|
10634
10901
|
if (oneSetting) {
|
|
10635
10902
|
const row = db.settings.find((candidate) => candidate.id === Number(oneSetting[1]));
|
|
10636
10903
|
if (!row) return { status: false, error: 'Setting not found', error_code: 404 };
|
|
10637
10904
|
if (opts.method === 'DELETE') return { status: false, error: 'DELETE not allowed: Setting', error_code: 403 };
|
|
10638
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};
|
|
10639
10907
|
if ('value' in opts.body && !row.is_secret) {
|
|
10640
10908
|
// Registered validators run before the row mutates (Setting.on_rest_pre_save → config.validate).
|
|
10641
10909
|
const refusal = validateRenditionSetting(typeof opts.body.key === 'string' ? opts.body.key : row.key, opts.body.value);
|
|
@@ -10654,6 +10922,7 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
10654
10922
|
return { status: true, data: serializeSetting(row), graph: 'default' };
|
|
10655
10923
|
}
|
|
10656
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};
|
|
10657
10926
|
const now = Math.floor(Date.now() / 1000);
|
|
10658
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 };
|
|
10659
10928
|
for (const [field, value] of Object.entries(opts.body)) {
|
|
@@ -11753,7 +12022,7 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
11753
12022
|
}
|
|
11754
12023
|
if (opts.method === 'POST' && opts.body) {
|
|
11755
12024
|
if (!groupCanManage(caller, group.id)) return permissionDenied();
|
|
11756
|
-
if (('disable' in opts.body || 'reactivate' in opts.body)
|
|
12025
|
+
if (('disable' in opts.body || 'reactivate' in opts.body || GROUP_SUBGROUP_POLICY_KEY in opts.body)
|
|
11757
12026
|
&& !hasGlobalPermission(caller, ['manage_groups', 'groups'])) return permissionDenied();
|
|
11758
12027
|
return saveGroup(group, opts.body);
|
|
11759
12028
|
}
|
|
@@ -11796,7 +12065,7 @@ async function mockFetchInner(path: string, opts: MockFetchOpts): Promise<unknow
|
|
|
11796
12065
|
db.groups.push(group);
|
|
11797
12066
|
return { status: true, data: serializeGroup(group), graph: 'default' };
|
|
11798
12067
|
}
|
|
11799
|
-
return listRows(visibleGroups
|
|
12068
|
+
return listRows(visibleGroups.map(serializeGroup), opts.params ?? {}, (g) => String(g.name), 'name');
|
|
11800
12069
|
}
|
|
11801
12070
|
// User API keys — /api/account/api_keys (account/rest/user_api_key.py).
|
|
11802
12071
|
// No DELETE (CAN_DELETE defaults false); the kill switch is the `revoke`
|