@fleetless/sdk 4.2.0-next.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/dist/index.cjs +1139 -414
- package/dist/index.d.cts +223 -27
- package/dist/index.d.ts +223 -27
- package/dist/index.js +1139 -414
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -410,7 +410,7 @@ function createAssetsApi(http) {
|
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
413
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/common.js
|
|
414
414
|
import { z } from "zod";
|
|
415
415
|
var SLUG_RULE = "A name is lower-case: it starts with a letter, continues with letters and digits, and joins further words with a single underscore \u2014 `battery_voltage`. Capitals, dashes, dots, spaces, a leading digit and a doubled or trailing underscore are all refused.";
|
|
416
416
|
var slug = z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, SLUG_RULE);
|
|
@@ -441,7 +441,7 @@ var applyError = z.object({
|
|
|
441
441
|
details: z.record(z.string(), z.unknown()).optional()
|
|
442
442
|
});
|
|
443
443
|
|
|
444
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
444
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/mcp.js
|
|
445
445
|
import { z as z2 } from "zod";
|
|
446
446
|
function mcpAppEndpointPath(appIdentifier2) {
|
|
447
447
|
return `/mcp/${appIdentifier2}`;
|
|
@@ -490,10 +490,10 @@ var mcpRolePreviewResponse = z2.object({
|
|
|
490
490
|
});
|
|
491
491
|
var MCP_ASSET_LINK_TTL_MS = 15 * 60 * 1e3;
|
|
492
492
|
|
|
493
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
493
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/protocol.js
|
|
494
494
|
import { z as z8 } from "zod";
|
|
495
495
|
|
|
496
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
496
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/assets.js
|
|
497
497
|
import { z as z3 } from "zod";
|
|
498
498
|
var assetKind = z3.enum(["urdf", "mesh", "texture"]);
|
|
499
499
|
var asset = z3.object({
|
|
@@ -813,10 +813,10 @@ var assetSyncBusyDetails = z3.object({
|
|
|
813
813
|
started_at_ms: z3.number().int().nonnegative()
|
|
814
814
|
});
|
|
815
815
|
|
|
816
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
816
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/config.js
|
|
817
817
|
import { z as z5 } from "zod";
|
|
818
818
|
|
|
819
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
819
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/alerts.js
|
|
820
820
|
import { z as z4 } from "zod";
|
|
821
821
|
var alertRowCondition = z4.discriminatedUnion("kind", [
|
|
822
822
|
z4.strictObject({
|
|
@@ -886,7 +886,7 @@ var putDatapointDisplayRequest = z4.object({
|
|
|
886
886
|
y_max: z4.number().finite().nullable()
|
|
887
887
|
}).strict();
|
|
888
888
|
|
|
889
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
889
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/config.js
|
|
890
890
|
var RTSP_URL_RULE = "The URL has to begin with `rtsp://` or `rtsps://` \u2014 `rtsp://cam-1.plant.local/stream1`. No other scheme is accepted: the bridge opens this with a library that would equally honour `file:`.";
|
|
891
891
|
var MJPEG_URL_RULE = "The URL has to begin with `http://` or `https://` \u2014 `http://cam-1.plant.local/video.mjpg`. No other scheme is accepted: the bridge opens this with a library that would equally serve `file:`.";
|
|
892
892
|
var DEVICE_PATH_RULE = "A capture device is a path under `/dev/`, and the character straight after it is a letter or a digit \u2014 `/dev/video0`, or a stable `/dev/v4l/by-id/...` symlink. Nothing outside `/dev/` is accepted: the string reaches OpenCV, which would as happily open an ordinary file.";
|
|
@@ -1978,7 +1978,7 @@ var configState = z5.object({
|
|
|
1978
1978
|
applied_errors: z5.array(applyError).nullable()
|
|
1979
1979
|
});
|
|
1980
1980
|
|
|
1981
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
1981
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/introspection.js
|
|
1982
1982
|
import { z as z6 } from "zod";
|
|
1983
1983
|
var rosGraphEntry = z6.object({
|
|
1984
1984
|
name: rosName,
|
|
@@ -2017,7 +2017,7 @@ var typeDefinition = z6.discriminatedUnion("kind", [
|
|
|
2017
2017
|
})
|
|
2018
2018
|
]);
|
|
2019
2019
|
|
|
2020
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2020
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/jobs.js
|
|
2021
2021
|
import { z as z7 } from "zod";
|
|
2022
2022
|
var jobState = z7.enum(["running", "unknown", "succeeded", "failed", "cancelled", "lost"]);
|
|
2023
2023
|
var reportedJobState = jobState.exclude(["unknown"]);
|
|
@@ -2132,6 +2132,20 @@ var jobActor = z7.object({
|
|
|
2132
2132
|
*/
|
|
2133
2133
|
label: z7.string().min(1).max(200).meta({
|
|
2134
2134
|
description: "A display name taken at invoke time \u2014 the email for a Fleetless user or an app user, the key's own name for a server key. Storing it rather than joining is the point: renaming a key afterwards does not rewrite history."
|
|
2135
|
+
}),
|
|
2136
|
+
/**
|
|
2137
|
+
* The person's display name, snapshotted beside `label` for the same
|
|
2138
|
+
* reason. **Required and nullable**, so a consumer never has to tell
|
|
2139
|
+
* "absent" from "null": every run a 5.3.0 cloud answers carries it, and
|
|
2140
|
+
* `null` means there is no name to show — a server key, a person without
|
|
2141
|
+
* one, or a run recorded before the field existed.
|
|
2142
|
+
*
|
|
2143
|
+
* `jobActor` stays a plain object, not `.strict()`: a consumer still on an
|
|
2144
|
+
* older contracts version then parses a newer cloud's answer by stripping
|
|
2145
|
+
* the key instead of refusing the whole run.
|
|
2146
|
+
*/
|
|
2147
|
+
name: z7.string().min(1).max(200).nullable().meta({
|
|
2148
|
+
description: "The person's display name when the job started: the Fleetless user's `display_name` for a developer, the app user's `display_name` for an app user. `null` for a server key, when the person had no name set, and for runs recorded before contracts 5.3.0. Show `label` when it is null."
|
|
2135
2149
|
})
|
|
2136
2150
|
});
|
|
2137
2151
|
var jobRunKind = z7.enum(["action", "service"]);
|
|
@@ -2240,7 +2254,7 @@ var jobRunSummary = z7.object({
|
|
|
2240
2254
|
since_ms: z7.number().int().nonnegative()
|
|
2241
2255
|
});
|
|
2242
2256
|
|
|
2243
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2257
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/protocol.js
|
|
2244
2258
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2245
2259
|
var MAX_PATIENCE_MS = 12e4;
|
|
2246
2260
|
var MIN_PATIENCE_MS = 1e3;
|
|
@@ -2373,7 +2387,8 @@ var cloudCancel = z8.object({
|
|
|
2373
2387
|
type: z8.literal("cancel"),
|
|
2374
2388
|
request_id: z8.string().min(1).max(64),
|
|
2375
2389
|
slug,
|
|
2376
|
-
job_id: z8.uuid().nullable()
|
|
2390
|
+
job_id: z8.uuid().nullable(),
|
|
2391
|
+
own_only: z8.boolean().optional()
|
|
2377
2392
|
});
|
|
2378
2393
|
var CANCEL_RETURN_CODES = { none: 0, rejected: 1, unknown_goal_id: 2, goal_terminated: 3 };
|
|
2379
2394
|
var cancelReturnCode = z8.number().int().min(0).max(3);
|
|
@@ -2637,11 +2652,11 @@ var bridgeCameraState = z8.object({
|
|
|
2637
2652
|
request_id: z8.string().min(1).max(64).nullable()
|
|
2638
2653
|
});
|
|
2639
2654
|
|
|
2640
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2655
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/config-issues.js
|
|
2641
2656
|
var EXPOSURE_SECTIONS = ["datapoints", "actions", "services", "publishers", "cameras"];
|
|
2642
2657
|
var EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
|
|
2643
2658
|
|
|
2644
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2659
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/rest.js
|
|
2645
2660
|
import { z as z9 } from "zod";
|
|
2646
2661
|
var robot = z9.object({
|
|
2647
2662
|
id: z9.uuid().meta({
|
|
@@ -3406,13 +3421,13 @@ var slugUsageResponse = z9.object({
|
|
|
3406
3421
|
alert_count: z9.number().int().nonnegative()
|
|
3407
3422
|
});
|
|
3408
3423
|
|
|
3409
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3424
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/realtime.js
|
|
3410
3425
|
import { z as z14 } from "zod";
|
|
3411
3426
|
|
|
3412
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3427
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
3413
3428
|
import { z as z13 } from "zod";
|
|
3414
3429
|
|
|
3415
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3430
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/apps.js
|
|
3416
3431
|
import { z as z10 } from "zod";
|
|
3417
3432
|
var appIdentifier = slug;
|
|
3418
3433
|
var app = z10.object({
|
|
@@ -3553,7 +3568,7 @@ var role = z10.object({
|
|
|
3553
3568
|
description: "The role's name, shown wherever a user's access is chosen. The two roles every app starts with are named `observe` and `operate`."
|
|
3554
3569
|
}),
|
|
3555
3570
|
builtin: z10.boolean().meta({
|
|
3556
|
-
description: "`true` for the two roles every app starts with. Their **rights may be re-scoped** exactly like a custom role's, through `PUT /api/apps/:id/roles/:roleId/permissions` \u2014 the flag exists so the console can explain where they came from, not to protect them.
|
|
3571
|
+
description: "`true` for the two roles every app starts with. Their **rights may be re-scoped** exactly like a custom role's, through `PUT /api/apps/:id/roles/:roleId/permissions` \u2014 the flag exists so the console can explain where they came from, not to protect them. Built-in roles can be renamed and deleted like any other; the flag only records that the cloud seeded them."
|
|
3557
3572
|
})
|
|
3558
3573
|
});
|
|
3559
3574
|
var roleListResponse = z10.object({
|
|
@@ -3561,6 +3576,27 @@ var roleListResponse = z10.object({
|
|
|
3561
3576
|
description: "The app's roles, built-in and custom alike, ordered by `created_at` and then by `name`. The tie-break is not cosmetic \u2014 the two built-in roles are inserted in one statement and share a creation time to the microsecond, so never read a role by position."
|
|
3562
3577
|
})
|
|
3563
3578
|
});
|
|
3579
|
+
var roleRenameRequest = z10.object({
|
|
3580
|
+
name: z10.string().trim().min(1).max(60).meta({
|
|
3581
|
+
description: "The new name, trimmed, 1 to 60 characters. Unique per app: another role of this app with the same name answers `409 role_name_taken`. The role's users keep it under its new name."
|
|
3582
|
+
})
|
|
3583
|
+
}).strict();
|
|
3584
|
+
var roleDeleteQuery = z10.object({
|
|
3585
|
+
move_to: z10.uuid().optional().meta({
|
|
3586
|
+
description: "Another role of the same app that takes over the deleted role's app users, pending invitations and, when it applies, the app's default. The role itself or a role of another app answers `400 validation_error`."
|
|
3587
|
+
})
|
|
3588
|
+
}).strict();
|
|
3589
|
+
var roleInUseDetails = z10.object({
|
|
3590
|
+
users: z10.number().int().nonnegative().meta({
|
|
3591
|
+
description: "App users whose role this is."
|
|
3592
|
+
}),
|
|
3593
|
+
invitations: z10.number().int().nonnegative().meta({
|
|
3594
|
+
description: "Pending invitations that would grant this role when accepted."
|
|
3595
|
+
}),
|
|
3596
|
+
is_default: z10.boolean().meta({
|
|
3597
|
+
description: "`true` when this is the app's `default_role_id`; the default then moves with the users to `move_to`."
|
|
3598
|
+
})
|
|
3599
|
+
}).strict();
|
|
3564
3600
|
var rolePermissions = z10.object({
|
|
3565
3601
|
role_id: z10.uuid(),
|
|
3566
3602
|
/**
|
|
@@ -3614,10 +3650,10 @@ var rolePermissions = z10.object({
|
|
|
3614
3650
|
})
|
|
3615
3651
|
});
|
|
3616
3652
|
|
|
3617
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3653
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/app-users.js
|
|
3618
3654
|
import { z as z12 } from "zod";
|
|
3619
3655
|
|
|
3620
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3656
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/identity.js
|
|
3621
3657
|
import { z as z11 } from "zod";
|
|
3622
3658
|
var password = z11.string().min(12).max(256);
|
|
3623
3659
|
var USER_DISPLAY_NAME_MAX = 120;
|
|
@@ -3629,6 +3665,9 @@ var org = z11.object({
|
|
|
3629
3665
|
name: z11.string().min(1).max(120).meta({
|
|
3630
3666
|
description: "The organisation's display name. Free text, changed through `PATCH /api/org`."
|
|
3631
3667
|
}),
|
|
3668
|
+
require_two_factor: z11.boolean().meta({
|
|
3669
|
+
description: "Whether every member must have a second factor \u2014 a passkey or an authenticator app. A member without one sets it up at their next sign-in, before any session exists; nobody is signed out when it is switched on. It covers the console and the central MCP endpoint; server keys and robot bridges are not people and are not affected. Owners change it through `PATCH /api/org`."
|
|
3670
|
+
}),
|
|
3632
3671
|
created_at: z11.iso.datetime().meta({
|
|
3633
3672
|
description: "When the organisation was created, as an ISO 8601 timestamp."
|
|
3634
3673
|
})
|
|
@@ -3654,6 +3693,12 @@ var fleetlessUser = z11.object({
|
|
|
3654
3693
|
tier: orgAdminTier.meta({
|
|
3655
3694
|
description: "The console powers this person holds. **Required** \u2014 every Fleetless user has a tier; it was optional only while the org also held people with no console powers to grade, and that pool is gone."
|
|
3656
3695
|
}),
|
|
3696
|
+
two_factor: z11.object({
|
|
3697
|
+
passkeys: z11.number().int().min(0).meta({ description: "How many passkeys the person has registered." }),
|
|
3698
|
+
authenticator: z11.boolean().meta({ description: "Whether the person has a confirmed authenticator app." })
|
|
3699
|
+
}).meta({
|
|
3700
|
+
description: "The person's second factors, as the team list shows them: none, passkeys, an authenticator, or both. No credential travels here. An owner resets them through `DELETE /api/org/users/:id/two-factor`."
|
|
3701
|
+
}),
|
|
3657
3702
|
created_at: z11.iso.datetime().meta({
|
|
3658
3703
|
description: "When the account was created, as an ISO 8601 timestamp."
|
|
3659
3704
|
})
|
|
@@ -3675,21 +3720,19 @@ var sessionTokens = z11.object({
|
|
|
3675
3720
|
})
|
|
3676
3721
|
});
|
|
3677
3722
|
var refreshRequest = z11.object({ refresh_token: z11.string().min(1) });
|
|
3678
|
-
var
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3723
|
+
var loginCode = z11.string().regex(/^\d{6}$/, "must be exactly six digits");
|
|
3724
|
+
var totpCode = loginCode;
|
|
3725
|
+
var recoveryCode = z11.string().regex(/^[a-zA-Z2-7]{5}-[a-zA-Z2-7]{5}$/, "must be two groups of five characters, xxxxx-xxxxx");
|
|
3726
|
+
var recoveryCodesList = z11.array(z11.string().regex(/^[a-z2-7]{5}-[a-z2-7]{5}$/)).length(10);
|
|
3727
|
+
var twoFactorSetupResponse = z11.object({
|
|
3728
|
+
secret: z11.string().min(1).meta({
|
|
3729
|
+
description: "The shared secret, base32, for an authenticator app that cannot scan a QR code. Shown once; the cloud stores it encrypted."
|
|
3730
|
+
}),
|
|
3731
|
+
otpauth_url: z11.string().startsWith("otpauth://totp/").meta({
|
|
3732
|
+
description: "The same secret as an `otpauth://totp/` URL, to render as a QR code. It carries the secret: never log it."
|
|
3733
|
+
})
|
|
3687
3734
|
});
|
|
3688
3735
|
var waitlistRequest = z11.object({ email: z11.email().max(254) });
|
|
3689
|
-
var developerLoginRequest = z11.object({
|
|
3690
|
-
email: z11.email(),
|
|
3691
|
-
password: z11.string().min(1)
|
|
3692
|
-
});
|
|
3693
3736
|
var mailStatus = z11.enum(["sent", "not_requested", "not_configured", "failed"]);
|
|
3694
3737
|
var createTeamInviteRequest = z11.object({
|
|
3695
3738
|
email: z11.email().meta({
|
|
@@ -3730,8 +3773,8 @@ var acceptTeamInviteRequest = z11.object({
|
|
|
3730
3773
|
token: z11.string().min(1).meta({
|
|
3731
3774
|
description: "The opaque invitation token from the link. Unknown, expired and already-accepted all collapse into `410 token_spent` \u2014 telling them apart would say whether a token ever existed."
|
|
3732
3775
|
}),
|
|
3733
|
-
|
|
3734
|
-
description: "
|
|
3776
|
+
display_name: z11.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable().optional().meta({
|
|
3777
|
+
description: "An optional name, overriding whatever the invitation pre-filled. Absent keeps it."
|
|
3735
3778
|
})
|
|
3736
3779
|
}).strict();
|
|
3737
3780
|
var patchFleetlessUserRequest = z11.object({
|
|
@@ -3753,13 +3796,6 @@ var passwordChangeRequest = z11.object({
|
|
|
3753
3796
|
description: "The replacement password. Every other session is revoked when it is accepted, while the session that made the change survives \u2014 logging somebody out of the tab they just used is indistinguishable from the change having failed."
|
|
3754
3797
|
})
|
|
3755
3798
|
});
|
|
3756
|
-
var passwordResetRequest = z11.object({
|
|
3757
|
-
email: z11.email()
|
|
3758
|
-
});
|
|
3759
|
-
var passwordResetConfirm = z11.object({
|
|
3760
|
-
token: z11.string().min(1),
|
|
3761
|
-
new_password: password
|
|
3762
|
-
});
|
|
3763
3799
|
var idpIssuer = z11.url().max(500).refine((v) => {
|
|
3764
3800
|
let url;
|
|
3765
3801
|
try {
|
|
@@ -3776,10 +3812,66 @@ var idpIssuer = z11.url().max(500).refine((v) => {
|
|
|
3776
3812
|
return url.hostname.length > 0;
|
|
3777
3813
|
}, { message: "issuer must be an http(s) URL with no credentials, query or fragment" });
|
|
3778
3814
|
var authMeResponse = z11.object({ org, user: fleetlessUser });
|
|
3779
|
-
var patchOrgRequest = z11.object({
|
|
3815
|
+
var patchOrgRequest = z11.object({
|
|
3816
|
+
name: z11.string().min(1).max(120).optional().meta({
|
|
3817
|
+
description: "The organisation's new display name. Absent leaves it alone."
|
|
3818
|
+
}),
|
|
3819
|
+
require_two_factor: z11.boolean().optional().meta({
|
|
3820
|
+
description: "Whether every member must have a second factor. Turning it on signs nobody out: each member without one sets it up at their next sign-in. Absent leaves it alone."
|
|
3821
|
+
})
|
|
3822
|
+
}).strict().refine((b) => b.name !== void 0 || b.require_two_factor !== void 0, { message: "Send name, require_two_factor, or both." });
|
|
3780
3823
|
var patchAuthMeRequest = z11.object({ display_name: z11.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable() }).strict();
|
|
3824
|
+
var webauthnJson = z11.record(z11.string(), z11.unknown());
|
|
3825
|
+
var webauthnOptionsResponse = z11.object({
|
|
3826
|
+
options: webauthnJson.meta({
|
|
3827
|
+
description: "The `PublicKeyCredentialCreationOptionsJSON` or `PublicKeyCredentialRequestOptionsJSON` to pass to the browser. Its challenge is single-use and short-lived."
|
|
3828
|
+
})
|
|
3829
|
+
});
|
|
3830
|
+
var developerPasskey = z11.object({
|
|
3831
|
+
id: z11.uuid().meta({ description: "The passkey in the API, as renamed and removed through `/api/auth/passkeys/:id`." }),
|
|
3832
|
+
name: z11.string().min(1).max(80).meta({ description: "What the person called it, such as the device it lives on." }),
|
|
3833
|
+
created_at: z11.iso.datetime().meta({ description: "When it was registered." }),
|
|
3834
|
+
last_used_at: z11.iso.datetime().nullable().meta({ description: "When it last signed the person in or confirmed a sign-in, or `null` if never." }),
|
|
3835
|
+
synced: z11.boolean().nullable().meta({
|
|
3836
|
+
description: "Whether the authenticator reported the passkey as syncable across the person's devices (the backup-eligible flag), or `null` when it said nothing."
|
|
3837
|
+
})
|
|
3838
|
+
});
|
|
3839
|
+
var developerTwoFactor = z11.object({
|
|
3840
|
+
passkeys: z11.array(developerPasskey).meta({ description: "Every passkey the caller has registered, oldest first. Empty when none." }),
|
|
3841
|
+
authenticator: z11.object({ created_at: z11.iso.datetime().meta({ description: "When the authenticator was confirmed." }) }).nullable().meta({ description: "The confirmed authenticator app, or `null` when there is none. At most one." }),
|
|
3842
|
+
recovery_codes_left: z11.number().int().min(0).max(10).meta({
|
|
3843
|
+
description: "How many of the ten recovery codes are unspent. `0` while the caller has no second factor."
|
|
3844
|
+
}),
|
|
3845
|
+
required_by_org: z11.boolean().meta({
|
|
3846
|
+
description: "Whether the organisation requires a second factor. While it does, the last one cannot be removed."
|
|
3847
|
+
})
|
|
3848
|
+
});
|
|
3849
|
+
var createPasskeyRequest = z11.object({
|
|
3850
|
+
name: z11.string().min(1).max(80).meta({ description: "What to call the passkey, such as the device it lives on." }),
|
|
3851
|
+
credential: webauthnJson.meta({ description: "The browser's `RegistrationResponseJSON` for the options `POST /api/auth/passkeys/options` answered." })
|
|
3852
|
+
}).strict();
|
|
3853
|
+
var createPasskeyResponse = z11.object({
|
|
3854
|
+
passkey: developerPasskey.meta({ description: "The passkey as it is now stored." }),
|
|
3855
|
+
recovery_codes: recoveryCodesList.nullable().meta({
|
|
3856
|
+
description: "The ten recovery codes, shown once, when this passkey is the account's first second factor; `null` when the account already had one and its codes stay valid."
|
|
3857
|
+
})
|
|
3858
|
+
});
|
|
3859
|
+
var renamePasskeyRequest = z11.object({
|
|
3860
|
+
name: z11.string().min(1).max(80).meta({ description: "The new name." })
|
|
3861
|
+
}).strict();
|
|
3862
|
+
var totpConfirmRequest = z11.object({
|
|
3863
|
+
code: totpCode.meta({ description: "A code the new authenticator shows now. It proves the secret was copied correctly before anything depends on it." })
|
|
3864
|
+
}).strict();
|
|
3865
|
+
var totpConfirmResponse = z11.object({
|
|
3866
|
+
recovery_codes: recoveryCodesList.nullable().meta({
|
|
3867
|
+
description: "The ten recovery codes, shown once, when this is the account's first second factor; `null` otherwise."
|
|
3868
|
+
})
|
|
3869
|
+
});
|
|
3870
|
+
var recoveryCodesResponse = z11.object({
|
|
3871
|
+
recovery_codes: recoveryCodesList.meta({ description: "The ten new recovery codes, lowercase, shown once. Every earlier code is void." })
|
|
3872
|
+
});
|
|
3781
3873
|
|
|
3782
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3874
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/app-users.js
|
|
3783
3875
|
var APP_USER_DISPLAY_NAME_MAX = 120;
|
|
3784
3876
|
var providerSlug = z12.string().max(40).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "must be lowercase and hyphen-separated, starting with a letter");
|
|
3785
3877
|
var appUserStatus = z12.enum(["pending_verification", "active", "blocked"]);
|
|
@@ -3817,6 +3909,19 @@ var appUser = z12.object({
|
|
|
3817
3909
|
last_login_at: z12.iso.datetime().nullable().meta({
|
|
3818
3910
|
description: "When this user last signed in, or `null` if they never have. Required and nullable rather than optional, so *never logged in* stays distinguishable from *this field was not loaded*."
|
|
3819
3911
|
}),
|
|
3912
|
+
two_factor: z12.object({
|
|
3913
|
+
enabled: z12.boolean().meta({
|
|
3914
|
+
description: "Whether the account has a confirmed authenticator app (TOTP). When it has, every sign-in that yields a session asks for a code as well \u2014 whatever the app's policy \u2014 except a sign-in through an identity provider, which owns that sign-in."
|
|
3915
|
+
}),
|
|
3916
|
+
enabled_at: z12.iso.datetime().nullable().meta({
|
|
3917
|
+
description: "When the authenticator was confirmed, or `null` while `enabled` is `false`."
|
|
3918
|
+
}),
|
|
3919
|
+
recovery_codes_left: z12.number().int().min(0).max(10).meta({
|
|
3920
|
+
description: "How many of the ten single-use recovery codes are still unspent. `0` while `enabled` is `false`."
|
|
3921
|
+
})
|
|
3922
|
+
}).meta({
|
|
3923
|
+
description: "The account's second factor, as a developer's user list shows it. No secret and no code travels here; resetting it is `DELETE /api/apps/:id/users/:userId/two-factor`."
|
|
3924
|
+
}),
|
|
3820
3925
|
created_at: z12.iso.datetime().meta({
|
|
3821
3926
|
description: "When the account was created, as an ISO 8601 timestamp."
|
|
3822
3927
|
})
|
|
@@ -3862,7 +3967,7 @@ var createAppInvitationRequest = z12.object({
|
|
|
3862
3967
|
description: "An optional name to pre-fill the account with; the invitee can change it afterwards."
|
|
3863
3968
|
}),
|
|
3864
3969
|
send_mail: z12.boolean().meta({
|
|
3865
|
-
description: "Whether Fleetless mails the invitation.
|
|
3970
|
+
description: "Whether Fleetless mails the invitation. The link points at the app's `invite_url`, or at the Fleetless-hosted invitation page when the app has configured none \u2014 so the mail always leads somewhere, and nothing is refused for a missing URL."
|
|
3866
3971
|
})
|
|
3867
3972
|
}).strict();
|
|
3868
3973
|
var appInvitation = z12.object({
|
|
@@ -3871,11 +3976,11 @@ var appInvitation = z12.object({
|
|
|
3871
3976
|
email: z12.email().meta({ description: "The address the invitation was addressed to." }),
|
|
3872
3977
|
role_id: z12.uuid().meta({ description: "The role the invitee holds once they accept. Resolved at creation, so a later change to the app's default role does not silently re-aim an outstanding invitation." }),
|
|
3873
3978
|
expires_at: z12.iso.datetime().meta({ description: "When the token stops working. Seven days from issue; an expired token answers exactly as an unknown one does." }),
|
|
3874
|
-
accept_url: z12.url().max(500).
|
|
3875
|
-
description: "The link to give the invitee
|
|
3979
|
+
accept_url: z12.url().max(500).meta({
|
|
3980
|
+
description: "The link to give the invitee: the app's `invite_url` with the token substituted for `{token}`, or the Fleetless-hosted invitation page when the app has configured none. Bounded like every other URL that gets mailed, logged and rendered."
|
|
3876
3981
|
}),
|
|
3877
3982
|
mail: mailStatus.meta({
|
|
3878
|
-
description: "What happened to the mail: `sent` means the SMTP server accepted it, not that it was delivered; `not_requested` means none was attempted
|
|
3983
|
+
description: "What happened to the mail: `sent` means the SMTP server accepted it, not that it was delivered; `not_requested` means none was attempted because the caller asked for none; `not_configured` is an expected state and not a failure; `failed` is the one worth somebody's attention."
|
|
3879
3984
|
})
|
|
3880
3985
|
});
|
|
3881
3986
|
var pendingAppInvitation = appInvitation.omit({ accept_url: true, mail: true });
|
|
@@ -3968,6 +4073,31 @@ var allowedOrigin = z12.string().max(200).refine((v) => {
|
|
|
3968
4073
|
}
|
|
3969
4074
|
}, { message: "must be a bare origin (scheme, host, port) over https, or over http on localhost" });
|
|
3970
4075
|
var emailDomain = z12.string().min(1).max(253).regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/, "must be a lowercase domain name with at least two labels");
|
|
4076
|
+
var appSignInMethods = z12.object({
|
|
4077
|
+
password: z12.boolean().meta({
|
|
4078
|
+
description: "Whether app users may sign in with a password. Off refuses `POST /api/client/login` with `method_not_allowed`, and registration and invitations then take no password."
|
|
4079
|
+
}),
|
|
4080
|
+
email_code: z12.boolean().meta({
|
|
4081
|
+
description: "Whether app users may sign in with a six-digit code mailed to them, valid ten minutes. A code needs no URL, so it works in local development and in an app with no web UI."
|
|
4082
|
+
})
|
|
4083
|
+
}).strict().refine((m) => m.password || m.email_code, { message: "At least one sign-in method must be on.", path: ["password"] });
|
|
4084
|
+
var appTwoFactorPolicy = z12.enum(["off", "optional", "required"]);
|
|
4085
|
+
var appHomeUrl = z12.url().max(500).refine((v) => {
|
|
4086
|
+
try {
|
|
4087
|
+
const u = new URL(v);
|
|
4088
|
+
const hostOk = u.protocol === "https:" || u.protocol === "http:" && ["localhost", "127.0.0.1"].includes(u.hostname);
|
|
4089
|
+
return hostOk && !v.includes("{");
|
|
4090
|
+
} catch {
|
|
4091
|
+
return false;
|
|
4092
|
+
}
|
|
4093
|
+
}, { message: "An https URL (http only on localhost) without a placeholder." });
|
|
4094
|
+
var hostedAccent = z12.string().regex(/^#[0-9a-f]{6}$/, "must be a lowercase hex colour, #rrggbb");
|
|
4095
|
+
var appHostedPages = z12.object({
|
|
4096
|
+
invite_url: z12.url().meta({ description: "The hosted invitation page, `<portal>/app/<identifier>/invite/{token}`." }),
|
|
4097
|
+
verify_url: z12.url().meta({ description: "The hosted email-confirmation page, `<portal>/app/<identifier>/verify/{token}`." }),
|
|
4098
|
+
reset_url: z12.url().meta({ description: "The hosted new-password page, `<portal>/app/<identifier>/reset/{token}`." }),
|
|
4099
|
+
mcp_login_url: z12.url().meta({ description: "The hosted MCP sign-in, `<portal>/app/<identifier>/mcp/{interaction}`." })
|
|
4100
|
+
});
|
|
3971
4101
|
var appAuthConfig = z12.object({
|
|
3972
4102
|
self_registration: z12.boolean().meta({
|
|
3973
4103
|
description: "Whether a stranger may create an account in this app. Off refuses `POST /api/client/register` with `403 registration_closed`, and refuses an unknown identity at an OIDC callback with the same reasoning \u2014 one switch for one decision, whichever door the person arrives at."
|
|
@@ -3982,16 +4112,34 @@ var appAuthConfig = z12.object({
|
|
|
3982
4112
|
description: "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
|
|
3983
4113
|
}),
|
|
3984
4114
|
invite_url: appUrlTemplate("{token}").nullable().meta({
|
|
3985
|
-
description: "The page in the developer's app that accepts an invitation, with `{token}` where the token goes. `null`
|
|
4115
|
+
description: "The page in the developer's app that accepts an invitation, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used."
|
|
3986
4116
|
}),
|
|
3987
4117
|
verify_url: appUrlTemplate("{token}").nullable().meta({
|
|
3988
|
-
description: "The page that confirms a new address, with `{token}` where the token goes.
|
|
4118
|
+
description: "The page that confirms a new address, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used."
|
|
3989
4119
|
}),
|
|
3990
4120
|
reset_url: appUrlTemplate("{token}").nullable().meta({
|
|
3991
|
-
description: "The page that takes a new password, with `{token}` where the token goes."
|
|
4121
|
+
description: "The page that takes a new password, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used."
|
|
3992
4122
|
}),
|
|
3993
4123
|
mcp_login_url: appUrlTemplate("{interaction}").nullable().meta({
|
|
3994
|
-
description: "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
|
|
4124
|
+
description: "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it. `null` means the hosted MCP sign-in in `hosted_pages` is used."
|
|
4125
|
+
}),
|
|
4126
|
+
app_url: appHomeUrl.nullable().meta({
|
|
4127
|
+
description: "The app's own home page, linked as `Open <app>` when a hosted flow is done. `null` makes the hosted done page say `You can close this tab`."
|
|
4128
|
+
}),
|
|
4129
|
+
sign_in_methods: appSignInMethods.meta({
|
|
4130
|
+
description: "Which sign-in methods the app offers: password, emailed code, or both \u2014 at least one. Identity providers stay on top of either. The default is password only."
|
|
4131
|
+
}),
|
|
4132
|
+
two_factor: appTwoFactorPolicy.meta({
|
|
4133
|
+
description: "Whether the app asks for an authenticator code: `off` (the default), `optional` or `required`. A person with a confirmed authenticator is asked at every sign-in whatever the policy; a sign-in through an identity provider is never asked."
|
|
4134
|
+
}),
|
|
4135
|
+
hosted_logo_url: z12.url().nullable().meta({
|
|
4136
|
+
description: "Where the hosted pages load the app's logo from, `<portal>/app/<identifier>/logo`, or `null` when no logo is stored. **Read-only** \u2014 the logo is written through `PUT /api/apps/:id/auth-config/logo`."
|
|
4137
|
+
}),
|
|
4138
|
+
hosted_accent: hostedAccent.nullable().meta({
|
|
4139
|
+
description: "The accent colour of the hosted pages, `#rrggbb` in lowercase, or `null` for the neutral shell's own."
|
|
4140
|
+
}),
|
|
4141
|
+
hosted_pages: appHostedPages.meta({
|
|
4142
|
+
description: "The Fleetless-hosted pages an unset URL falls back to, as templates. **Read-only**: minted by the cloud from the auth portal and the app's identifier."
|
|
3995
4143
|
}),
|
|
3996
4144
|
oidc_callback_url: z12.url().meta({
|
|
3997
4145
|
description: "The one callback URL to register at every identity provider, the same for every app and every provider. **Read-only** \u2014 it is minted by the cloud from its own public base URL, and a writable version of this field would let a caller point the return leg, which carries an authorization code, at a host they own."
|
|
@@ -3999,18 +4147,20 @@ var appAuthConfig = z12.object({
|
|
|
3999
4147
|
updated_at: z12.iso.datetime().meta({ description: "When the configuration was last written, as an ISO 8601 timestamp." })
|
|
4000
4148
|
});
|
|
4001
4149
|
var putAppAuthRegistrationRequest = appAuthConfig.pick({ self_registration: true, allowed_domains: true, allowed_origins: true }).strict();
|
|
4002
|
-
var
|
|
4003
|
-
var
|
|
4004
|
-
var
|
|
4150
|
+
var putAppAuthSignInRequest = appAuthConfig.pick({ sign_in_methods: true, two_factor: true }).strict();
|
|
4151
|
+
var putAppAuthUrlsRequest = appAuthConfig.pick({ app_url: true, invite_url: true, verify_url: true, reset_url: true, mcp_login_url: true }).strict();
|
|
4152
|
+
var putAppAuthMcpRequest = appAuthConfig.pick({ mcp_enabled: true }).strict();
|
|
4153
|
+
var putAppAuthLookRequest = appAuthConfig.pick({ hosted_accent: true }).strict();
|
|
4154
|
+
var mailTemplateKind = z12.enum(["invite", "verify", "reset", "login_code"]);
|
|
4005
4155
|
var appMailTemplate = z12.object({
|
|
4006
|
-
kind: mailTemplateKind.meta({ description: "Which of the
|
|
4156
|
+
kind: mailTemplateKind.meta({ description: "Which of the four mails this template replaces." }),
|
|
4007
4157
|
subject: z12.string().min(1).max(200).meta({ description: "The subject line, a Liquid template. Bounded because a subject is rendered into a header." }),
|
|
4008
4158
|
text: z12.string().min(1).max(2e4).meta({ description: "The plain-text body, a Liquid template. Required even when an HTML part is given: a mail with no text part is unreadable to a client that refuses HTML." }),
|
|
4009
4159
|
html: z12.string().min(1).max(1e5).nullable().meta({ description: "The optional HTML body, a Liquid template. `null` means this template is text-only, which is a complete mail and not a half-configured one." }),
|
|
4010
4160
|
updated_at: z12.iso.datetime().meta({ description: "When the template was last written, as an ISO 8601 timestamp." })
|
|
4011
4161
|
});
|
|
4012
4162
|
var appMailTemplateListResponse = z12.object({
|
|
4013
|
-
templates: z12.array(appMailTemplate).max(
|
|
4163
|
+
templates: z12.array(appMailTemplate).max(4).meta({
|
|
4014
4164
|
description: "The app's custom templates. A kind that does not appear is one using the Fleetless default text \u2014 an ordinary state, not a missing row."
|
|
4015
4165
|
})
|
|
4016
4166
|
});
|
|
@@ -4032,7 +4182,7 @@ var mailOutcome = z12.object({
|
|
|
4032
4182
|
})
|
|
4033
4183
|
});
|
|
4034
4184
|
|
|
4035
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4185
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
4036
4186
|
var clientLoginRequest = z13.object({
|
|
4037
4187
|
app_identifier: appIdentifier.meta({
|
|
4038
4188
|
description: "The app being logged in to: its globally unique, lowercase, underscore-separated identifier, chosen by the developer at creation. There is no organisation context at login, so this is what decides which app the credentials are checked for."
|
|
@@ -4054,6 +4204,49 @@ var clientLogoutRequest = z13.object({
|
|
|
4054
4204
|
description: "Any refresh token of the session to end. The whole token family is revoked server-side, so a token stolen before this call stops working too \u2014 clearing a client-side store is a gesture, not a revocation. The answer is `204`: a token the server does not recognise gets it too, since that is the end state being asked for."
|
|
4055
4205
|
})
|
|
4056
4206
|
});
|
|
4207
|
+
var clientLoginCodeRequest = z13.object({
|
|
4208
|
+
app_identifier: appIdentifier.meta({ description: "The app to sign in to. An identifier no app carries is `404 not_found`; the address is never the subject of a refusal." }),
|
|
4209
|
+
email: z13.email().meta({
|
|
4210
|
+
description: "The address to mail the code to, trimmed and compared case-insensitively. `202` whether or not it names an account of this app."
|
|
4211
|
+
})
|
|
4212
|
+
}).strict();
|
|
4213
|
+
var clientLoginCodeVerifyRequest = z13.object({
|
|
4214
|
+
app_identifier: appIdentifier.meta({ description: "The app the code was requested for." }),
|
|
4215
|
+
email: z13.email().meta({ description: "The address the code was mailed to, as typed when it was requested; trimmed and compared case-insensitively." }),
|
|
4216
|
+
code: loginCode.meta({ description: "The six digits from the mail, exactly \u2014 leading zeros included, no spaces." })
|
|
4217
|
+
}).strict();
|
|
4218
|
+
var twoFactorChallenge = z13.object({
|
|
4219
|
+
status: z13.enum(["two_factor_required", "two_factor_setup_required"]).meta({
|
|
4220
|
+
description: "`two_factor_required`: ask for the authenticator code. `two_factor_setup_required`: the app requires two-factor and the person has none yet, so set one up before any session exists."
|
|
4221
|
+
}),
|
|
4222
|
+
challenge: z13.string().min(1).meta({
|
|
4223
|
+
description: "The handle the next step spends. Valid five minutes; afterwards it answers `410 token_spent` and the sign-in starts over."
|
|
4224
|
+
})
|
|
4225
|
+
});
|
|
4226
|
+
var clientSignInResult = z13.union([sessionTokens, twoFactorChallenge]);
|
|
4227
|
+
var clientTwoFactorVerifyRequest = z13.object({
|
|
4228
|
+
challenge: z13.string().min(1).meta({ description: "The challenge the sign-in step answered." }),
|
|
4229
|
+
code: totpCode.optional().meta({ description: "The six-digit code the authenticator shows now. A code already accepted once is refused, so a replay of a seen code does not sign anybody in." }),
|
|
4230
|
+
recovery_code: recoveryCode.optional().meta({ description: "One of the ten recovery codes, `xxxxx-xxxxx`, in either case. Spent by its use." })
|
|
4231
|
+
}).strict().refine((b) => b.code === void 0 !== (b.recovery_code === void 0), { message: "Send exactly one of code and recovery_code." });
|
|
4232
|
+
var clientTwoFactorSetupRequest = z13.object({
|
|
4233
|
+
challenge: z13.string().min(1).optional().meta({
|
|
4234
|
+
description: "The `two_factor_setup_required` challenge, during sign-in. Absent when the call carries the app user's bearer instead."
|
|
4235
|
+
})
|
|
4236
|
+
}).strict();
|
|
4237
|
+
var clientTwoFactorSetupConfirmRequest = z13.object({
|
|
4238
|
+
challenge: z13.string().min(1).optional().meta({ description: "The same challenge as at `setup`, during sign-in; absent with a bearer." }),
|
|
4239
|
+
code: totpCode.meta({ description: "A code the new authenticator shows now. It proves the secret was copied correctly before anything depends on it." })
|
|
4240
|
+
}).strict();
|
|
4241
|
+
var clientTwoFactorSetupConfirmResponse = z13.object({
|
|
4242
|
+
recovery_codes: recoveryCodesList.meta({
|
|
4243
|
+
description: "The ten single-use recovery codes, lowercase, shown once. Any earlier set is void."
|
|
4244
|
+
}),
|
|
4245
|
+
session: sessionTokens.meta({ description: "The session the sign-in was waiting for, or a fresh one for the account settings." })
|
|
4246
|
+
});
|
|
4247
|
+
var clientTwoFactorDisableRequest = z13.object({
|
|
4248
|
+
code: totpCode.meta({ description: "A code the authenticator shows now." })
|
|
4249
|
+
}).strict();
|
|
4057
4250
|
var clientRegisterRequest = z13.object({
|
|
4058
4251
|
app_identifier: appIdentifier.meta({
|
|
4059
4252
|
description: "The app to register with. An identifier no app carries is `404 not_found` \u2014 an identifier is public, so naming it is no disclosure, and collapsing it into `registration_closed` sent a developer who mistyped their own identifier hunting a configuration bug that was not there. The **address** is never the subject of a refusal."
|
|
@@ -4061,8 +4254,8 @@ var clientRegisterRequest = z13.object({
|
|
|
4061
4254
|
email: z13.email().meta({
|
|
4062
4255
|
description: "The address to register. Unique per app, case-insensitively. An address this app already knows still answers `202`, without a mail \u2014 the answer may not say whether an account exists."
|
|
4063
4256
|
}),
|
|
4064
|
-
password: password.meta({
|
|
4065
|
-
description: "The password for the new account
|
|
4257
|
+
password: password.optional().meta({
|
|
4258
|
+
description: "The password for the new account, at least 12 characters. **Required while the app's password method is on, refused while it is off** \u2014 both as `400 validation_error` naming `password`. An email-code-only app registers people without one."
|
|
4066
4259
|
}),
|
|
4067
4260
|
display_name: z13.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
|
|
4068
4261
|
description: "An optional human name for the account. The developer's own UI decides whether to ask for it."
|
|
@@ -4097,7 +4290,9 @@ var clientAcceptInvitationRequest = z13.object({
|
|
|
4097
4290
|
token: z13.string().min(1).meta({
|
|
4098
4291
|
description: "The opaque token from the invitation link, valid seven days. Unknown, expired, revoked and already-accepted all answer `410 token_spent`."
|
|
4099
4292
|
}),
|
|
4100
|
-
password: password.meta({
|
|
4293
|
+
password: password.optional().meta({
|
|
4294
|
+
description: "The password the new account will use, at least 12 characters. **Required while the app's password method is on, refused while it is off** \u2014 both as `400 validation_error` naming `password`. An email-code-only app accepts invitations without one."
|
|
4295
|
+
}),
|
|
4101
4296
|
display_name: z13.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
|
|
4102
4297
|
description: "An optional name, overriding whatever the invitation pre-filled."
|
|
4103
4298
|
})
|
|
@@ -4111,7 +4306,10 @@ var clientProviderListResponse = z13.object({
|
|
|
4111
4306
|
slug: providerSlug.meta({ description: "The handle to put in the start URL: `GET /api/client/oidc/<slug>/start`." }),
|
|
4112
4307
|
name: z13.string().meta({ description: "What to write on the button, as the developer configured it." })
|
|
4113
4308
|
})).meta({
|
|
4114
|
-
description: "The app's **enabled** providers, slug and display name only. An app with none answers an empty array, which is the state of an app that offers password
|
|
4309
|
+
description: "The app's **enabled** providers, slug and display name only. An app with none answers an empty array, which is the state of an app that offers password or code sign-in alone."
|
|
4310
|
+
}),
|
|
4311
|
+
sign_in_methods: appSignInMethods.meta({
|
|
4312
|
+
description: "Which of password and emailed code the app accepts, so its sign-in page draws the right fields without guessing. The same value the developer set; public, like the provider buttons."
|
|
4115
4313
|
})
|
|
4116
4314
|
});
|
|
4117
4315
|
var clientOidcStartQuery = z13.object({
|
|
@@ -4218,10 +4416,13 @@ var clientIdentity = z13.object({
|
|
|
4218
4416
|
}),
|
|
4219
4417
|
email: z13.email().nullable().meta({
|
|
4220
4418
|
description: "The address of the Fleetless user or app user behind this session, and `null` for a server key, which is not a person."
|
|
4419
|
+
}),
|
|
4420
|
+
two_factor_enabled: z13.boolean().nullable().meta({
|
|
4421
|
+
description: "Whether the app user has a confirmed authenticator, so the app's account settings can offer to turn it on or off. `null` unless `kind` is `app_user`."
|
|
4221
4422
|
})
|
|
4222
4423
|
});
|
|
4223
4424
|
|
|
4224
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4425
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/realtime.js
|
|
4225
4426
|
var clientAuth = z14.object({
|
|
4226
4427
|
type: z14.literal("auth"),
|
|
4227
4428
|
token: z14.string().min(1)
|
|
@@ -4499,34 +4700,59 @@ var orgEventDropped = z14.object({
|
|
|
4499
4700
|
dropped: z14.number().int().positive()
|
|
4500
4701
|
}).strict();
|
|
4501
4702
|
|
|
4502
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4703
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/feedback.js
|
|
4503
4704
|
import { z as z15 } from "zod";
|
|
4504
|
-
var
|
|
4705
|
+
var FEEDBACK_KINDS = ["idea", "problem", "question", "other"];
|
|
4706
|
+
var feedbackKind = z15.enum(FEEDBACK_KINDS);
|
|
4707
|
+
var FEEDBACK_MESSAGE_MAX = 5e3;
|
|
4708
|
+
var feedbackRequest = z15.object({
|
|
4709
|
+
kind: feedbackKind.meta({
|
|
4710
|
+
description: "What the message is: an `idea`, a `problem`, a `question` or `other`. It only sorts the inbox; it changes nothing about how the message is handled."
|
|
4711
|
+
}),
|
|
4712
|
+
message: z15.string().trim().min(1).max(FEEDBACK_MESSAGE_MAX).meta({
|
|
4713
|
+
description: "What the developer wrote, trimmed. At most 5000 characters; a message that is only whitespace is refused."
|
|
4714
|
+
}),
|
|
4715
|
+
page: z15.string().startsWith("/").max(512).meta({
|
|
4716
|
+
description: "The console path the message was sent from, e.g. `/robots/:id/jobs` with its real id. A path, never a full URL, so no host and no query string reach the inbox by accident."
|
|
4717
|
+
})
|
|
4718
|
+
}).strict();
|
|
4719
|
+
var feedbackResponse = z15.object({
|
|
4720
|
+
id: z15.uuid().meta({
|
|
4721
|
+
description: "The stored message. It exists whatever `mail` says."
|
|
4722
|
+
}),
|
|
4723
|
+
mail: mailStatus.meta({
|
|
4724
|
+
description: "What happened to the notification mail: `sent`, `failed`, or `not_configured` when this cloud has no feedback address. The message is stored in every case, so a client shows success for all three."
|
|
4725
|
+
})
|
|
4726
|
+
}).strict();
|
|
4727
|
+
|
|
4728
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/client-robots.js
|
|
4729
|
+
import { z as z16 } from "zod";
|
|
4730
|
+
var clientRobotListItem = z16.object({
|
|
4505
4731
|
...robot.shape,
|
|
4506
4732
|
bridge_state: bridgeState.meta({
|
|
4507
4733
|
description: "The built-in `bridge_state` datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is."
|
|
4508
4734
|
}),
|
|
4509
|
-
published_version:
|
|
4735
|
+
published_version: z16.number().int().positive().nullable().meta({
|
|
4510
4736
|
description: 'The published configuration version, or `null` when nothing has been published yet. A robot with nothing published is still listed \u2014 "not configured yet" is a real state, and the caller is entitled to it \u2014 and its datasheet answers an empty exposure list.'
|
|
4511
4737
|
})
|
|
4512
4738
|
});
|
|
4513
|
-
var clientRobotListResponse =
|
|
4514
|
-
robots:
|
|
4739
|
+
var clientRobotListResponse = z16.object({
|
|
4740
|
+
robots: z16.array(clientRobotListItem).meta({
|
|
4515
4741
|
description: "Every robot the caller reaches, in name order with the id as the tiebreak. An app user reaches the robots their app attaches on which their role grants at least one slug or capability; a server key reaches every robot its app attaches; a developer reaches every robot of the organisation."
|
|
4516
4742
|
})
|
|
4517
4743
|
});
|
|
4518
4744
|
|
|
4519
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4520
|
-
import { z as
|
|
4521
|
-
var auditActor =
|
|
4522
|
-
kind:
|
|
4523
|
-
id:
|
|
4524
|
-
label:
|
|
4525
|
-
});
|
|
4526
|
-
var auditEvent =
|
|
4527
|
-
id:
|
|
4528
|
-
org_id:
|
|
4529
|
-
at:
|
|
4745
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/audit.js
|
|
4746
|
+
import { z as z17 } from "zod";
|
|
4747
|
+
var auditActor = z17.object({
|
|
4748
|
+
kind: z17.enum(["developer", "end_user", "app_user", "server_key", "bridge"]),
|
|
4749
|
+
id: z17.uuid(),
|
|
4750
|
+
label: z17.string().min(1).max(200)
|
|
4751
|
+
});
|
|
4752
|
+
var auditEvent = z17.object({
|
|
4753
|
+
id: z17.uuid(),
|
|
4754
|
+
org_id: z17.uuid(),
|
|
4755
|
+
at: z17.iso.datetime(),
|
|
4530
4756
|
/**
|
|
4531
4757
|
* A monotonic counter, ascending in write order, unique across the log.
|
|
4532
4758
|
*
|
|
@@ -4545,18 +4771,18 @@ var auditEvent = z16.object({
|
|
|
4545
4771
|
* Required, not optional: an event without a sequence cannot be ordered
|
|
4546
4772
|
* against one that has it, and a log with two orderings has none.
|
|
4547
4773
|
*/
|
|
4548
|
-
seq:
|
|
4774
|
+
seq: z17.number().int().positive(),
|
|
4549
4775
|
actor: auditActor,
|
|
4550
4776
|
/** Stable dotted name, e.g. `app_user.login`, `config.published`. */
|
|
4551
|
-
action:
|
|
4777
|
+
action: z17.string().min(1).max(80),
|
|
4552
4778
|
/**
|
|
4553
4779
|
* What the action was about, if anything — a robot, an app, a user. Free
|
|
4554
4780
|
* of ids the console cannot resolve: carry the label with it.
|
|
4555
4781
|
*/
|
|
4556
|
-
target:
|
|
4557
|
-
kind:
|
|
4558
|
-
id:
|
|
4559
|
-
label:
|
|
4782
|
+
target: z17.object({
|
|
4783
|
+
kind: z17.string().min(1).max(40),
|
|
4784
|
+
id: z17.string().min(1),
|
|
4785
|
+
label: z17.string().min(1).max(200)
|
|
4560
4786
|
}).nullable(),
|
|
4561
4787
|
/**
|
|
4562
4788
|
* Action-specific extras.
|
|
@@ -4570,10 +4796,10 @@ var auditEvent = z16.object({
|
|
|
4570
4796
|
* So: never credentials, never tokens. That is a rule, not a guarantee the
|
|
4571
4797
|
* schema enforces.
|
|
4572
4798
|
*/
|
|
4573
|
-
details:
|
|
4799
|
+
details: z17.record(z17.string(), z17.unknown()).nullable()
|
|
4574
4800
|
});
|
|
4575
4801
|
var auditTimestampMs = wireTimestampMs;
|
|
4576
|
-
var auditQuery =
|
|
4802
|
+
var auditQuery = z17.object({
|
|
4577
4803
|
/** Only events with a smaller `seq` — the next, older page. */
|
|
4578
4804
|
before_seq: wireSeqCursor.optional(),
|
|
4579
4805
|
/**
|
|
@@ -4582,9 +4808,9 @@ var auditQuery = z16.object({
|
|
|
4582
4808
|
* coercion's result in either `io` direction, so the artifact would describe
|
|
4583
4809
|
* a shape a query string can never carry.
|
|
4584
4810
|
*/
|
|
4585
|
-
limit:
|
|
4811
|
+
limit: z17.union([z17.string().regex(/^\d{1,4}$/), z17.number().int()]).transform((v) => Number(v)).pipe(z17.number().int().positive().max(500)).optional(),
|
|
4586
4812
|
/** Exact action name, e.g. `config.published`. No prefix matching: a filter that matches more than it says is not one. */
|
|
4587
|
-
action:
|
|
4813
|
+
action: z17.string().min(1).max(80).optional(),
|
|
4588
4814
|
/**
|
|
4589
4815
|
* Everything under a dotted prefix, e.g. `server_key.` for all three
|
|
4590
4816
|
* server-key actions.
|
|
@@ -4601,7 +4827,7 @@ var auditQuery = z16.object({
|
|
|
4601
4827
|
* happily. The cloud is the only enforcement point — the same residual
|
|
4602
4828
|
* `orgLatencyQuery` and `orgUsageQuery` already name.
|
|
4603
4829
|
*/
|
|
4604
|
-
action_prefix:
|
|
4830
|
+
action_prefix: z17.string().min(1).max(80).optional(),
|
|
4605
4831
|
/**
|
|
4606
4832
|
* Only events by this actor.
|
|
4607
4833
|
*
|
|
@@ -4613,9 +4839,21 @@ var auditQuery = z16.object({
|
|
|
4613
4839
|
* Not an injection question — the query is parameterised either way. It is a
|
|
4614
4840
|
* **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
|
|
4615
4841
|
*/
|
|
4616
|
-
actor_id:
|
|
4842
|
+
actor_id: z17.uuid().optional(),
|
|
4617
4843
|
/** Only events about this kind of target, e.g. `robot`. */
|
|
4618
|
-
target_kind:
|
|
4844
|
+
target_kind: z17.string().min(1).max(40).optional(),
|
|
4845
|
+
/**
|
|
4846
|
+
* Only events about this target — and, for a robot, also the events that
|
|
4847
|
+
* name it in `details.robot_id`, so a robot's log includes what was
|
|
4848
|
+
* started on it.
|
|
4849
|
+
*
|
|
4850
|
+
* **A string, not `z.uuid()`**, unlike `actor_id` above: `target.id` is a
|
|
4851
|
+
* string in this contract and text in the cloud's table, so a uuid rule
|
|
4852
|
+
* here would refuse ids the log can hold, and no value can fail a cast.
|
|
4853
|
+
*/
|
|
4854
|
+
target_id: z17.string().min(1).max(200).optional().meta({
|
|
4855
|
+
description: "Events whose target is this id; for a robot also the events that name it in `details.robot_id` (`action.invoked`, `service.called`, \u2026), so a robot's events include what was started on it."
|
|
4856
|
+
}),
|
|
4619
4857
|
/**
|
|
4620
4858
|
* Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
|
|
4621
4859
|
* same rule the history shapes follow.
|
|
@@ -4631,8 +4869,8 @@ var auditQuery = z16.object({
|
|
|
4631
4869
|
message: "action and action_prefix cannot be combined",
|
|
4632
4870
|
path: ["action_prefix"]
|
|
4633
4871
|
});
|
|
4634
|
-
var auditListResponse =
|
|
4635
|
-
events:
|
|
4872
|
+
var auditListResponse = z17.object({
|
|
4873
|
+
events: z17.array(auditEvent),
|
|
4636
4874
|
/**
|
|
4637
4875
|
* The `seq` a caller sends as `before_seq` to keep reading — or `null` when
|
|
4638
4876
|
* there is nothing further.
|
|
@@ -4643,32 +4881,37 @@ var auditListResponse = z16.object({
|
|
|
4643
4881
|
* not mean *no more* here. The same distinction `historySamples` was given
|
|
4644
4882
|
* `truncated` for.
|
|
4645
4883
|
*/
|
|
4646
|
-
next_cursor:
|
|
4884
|
+
next_cursor: z17.number().int().positive().nullable()
|
|
4647
4885
|
});
|
|
4648
4886
|
|
|
4649
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4650
|
-
import { z as
|
|
4651
|
-
var apiError =
|
|
4652
|
-
code:
|
|
4653
|
-
message:
|
|
4654
|
-
details:
|
|
4887
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/errors.js
|
|
4888
|
+
import { z as z18 } from "zod";
|
|
4889
|
+
var apiError = z18.object({
|
|
4890
|
+
code: z18.string().min(1),
|
|
4891
|
+
message: z18.string().min(1),
|
|
4892
|
+
details: z18.unknown().optional()
|
|
4655
4893
|
});
|
|
4656
|
-
var parameterViolation =
|
|
4657
|
-
field:
|
|
4894
|
+
var parameterViolation = z18.object({
|
|
4895
|
+
field: z18.string().min(1),
|
|
4658
4896
|
/** Which rule failed — `min`, `max`, `enum`, `pattern`, `required`, `undeclared`. */
|
|
4659
|
-
rule:
|
|
4660
|
-
message:
|
|
4897
|
+
rule: z18.string().min(1),
|
|
4898
|
+
message: z18.string().min(1)
|
|
4899
|
+
});
|
|
4900
|
+
var parameterInvalidDetails = z18.object({
|
|
4901
|
+
violations: z18.array(parameterViolation).min(1)
|
|
4661
4902
|
});
|
|
4662
|
-
var
|
|
4663
|
-
|
|
4903
|
+
var cancelRejectedDetails = z18.object({
|
|
4904
|
+
goals: z18.array(bridgeCancelResultEntry).min(1)
|
|
4664
4905
|
});
|
|
4665
|
-
var
|
|
4666
|
-
|
|
4906
|
+
var invalidCodeDetails = z18.object({
|
|
4907
|
+
attempts_left: z18.number().int().min(0).meta({
|
|
4908
|
+
description: "How many more wrong codes this code or challenge takes before it is spent. `0` means the next attempt answers `410 token_spent`."
|
|
4909
|
+
})
|
|
4667
4910
|
});
|
|
4668
4911
|
|
|
4669
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4670
|
-
import { z as
|
|
4671
|
-
var oauthErrorCode =
|
|
4912
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/oauth.js
|
|
4913
|
+
import { z as z19 } from "zod";
|
|
4914
|
+
var oauthErrorCode = z19.enum([
|
|
4672
4915
|
"invalid_request",
|
|
4673
4916
|
"invalid_client",
|
|
4674
4917
|
"invalid_grant",
|
|
@@ -4681,11 +4924,11 @@ var oauthErrorCode = z18.enum([
|
|
|
4681
4924
|
/** RFC 8707: the `resource` named is not one this server issues tokens for. */
|
|
4682
4925
|
"invalid_target"
|
|
4683
4926
|
]);
|
|
4684
|
-
var oauthError =
|
|
4927
|
+
var oauthError = z19.object({
|
|
4685
4928
|
error: oauthErrorCode,
|
|
4686
|
-
error_description:
|
|
4929
|
+
error_description: z19.string().min(1).max(500).optional(),
|
|
4687
4930
|
/** Echoed back per RFC 6749 §4.1.2.1 so a client can match the response. */
|
|
4688
|
-
state:
|
|
4931
|
+
state: z19.string().min(1).max(500).optional(),
|
|
4689
4932
|
/**
|
|
4690
4933
|
* **A Fleetless reason carried inside a standard envelope, and it exists
|
|
4691
4934
|
* because the alternative lost a distinction.**
|
|
@@ -4703,9 +4946,9 @@ var oauthError = z18.object({
|
|
|
4703
4946
|
* our own tooling switches on. RFC 6749 §5.2 permits additional members, and
|
|
4704
4947
|
* a client that ignores this one still behaves correctly.
|
|
4705
4948
|
*/
|
|
4706
|
-
fleetless_code:
|
|
4949
|
+
fleetless_code: z19.string().min(1).max(60).optional()
|
|
4707
4950
|
});
|
|
4708
|
-
var redirectUri =
|
|
4951
|
+
var redirectUri = z19.string().min(1).max(2e3).refine((v) => {
|
|
4709
4952
|
let url;
|
|
4710
4953
|
try {
|
|
4711
4954
|
url = new URL(v);
|
|
@@ -4720,180 +4963,180 @@ var redirectUri = z18.string().min(1).max(2e3).refine((v) => {
|
|
|
4720
4963
|
return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
4721
4964
|
return false;
|
|
4722
4965
|
}, { message: "redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment" });
|
|
4723
|
-
var codeChallengeMethod =
|
|
4966
|
+
var codeChallengeMethod = z19.enum(["S256"]);
|
|
4724
4967
|
var MCP_DCR_MAX_REDIRECT_URIS = 5;
|
|
4725
|
-
var dynamicClientRegistrationRequest =
|
|
4726
|
-
redirect_uris:
|
|
4968
|
+
var dynamicClientRegistrationRequest = z19.object({
|
|
4969
|
+
redirect_uris: z19.array(redirectUri).min(1).max(MCP_DCR_MAX_REDIRECT_URIS).meta({
|
|
4727
4970
|
description: `Where the authorization code may be returned, and the one field a registration cannot omit. Each must be an \`https\` URL, or \`http\` on an explicit loopback address for a native app that cannot hold a certificate, and none may carry a fragment. Between \`1\` and \`${MCP_DCR_MAX_REDIRECT_URIS}\` of them; duplicates are collapsed rather than counted twice. Matched **exactly** at the authorize step against what was registered here.`
|
|
4728
4971
|
}),
|
|
4729
|
-
client_name:
|
|
4972
|
+
client_name: z19.string().min(1).max(200).optional().meta({
|
|
4730
4973
|
description: 'The name the client calls itself. Optional: RFC 7591 makes every metadata field optional, so a registration without one is recorded under a default name. It is **not** vouched for by Fleetless and must never be rendered as if it were: a self-registered client chooses this string, and one has called itself *"Fleetless Official Helper"*.'
|
|
4731
4974
|
}),
|
|
4732
|
-
token_endpoint_auth_method:
|
|
4975
|
+
token_endpoint_auth_method: z19.enum(["none"]).optional().meta({
|
|
4733
4976
|
description: "`none`, RFC 7591's value for a public client, and the only value either server registers. Any other value is **refused rather than silently downgraded**: a client that believes it holds a secret and does not has a wrong mental model of its own security. There is no client secret to hold \u2014 mandatory PKCE (`S256`) is the defence."
|
|
4734
4977
|
}),
|
|
4735
|
-
grant_types:
|
|
4978
|
+
grant_types: z19.array(z19.enum(["authorization_code", "refresh_token"])).optional().meta({
|
|
4736
4979
|
description: "Accepted for conformance with RFC 7591 and then **ignored**: both MCP authorization servers grant `authorization_code` and `refresh_token` to every registration, and the answer states what was granted (\xA73.2.1) rather than what was asked."
|
|
4737
4980
|
}),
|
|
4738
|
-
response_types:
|
|
4981
|
+
response_types: z19.array(z19.enum(["code"])).optional().meta({
|
|
4739
4982
|
description: "Accepted for conformance and then **ignored**; the response names `code`, which is the only response type OAuth 2.1 leaves, the implicit grant having been removed."
|
|
4740
4983
|
}),
|
|
4741
|
-
scope:
|
|
4984
|
+
scope: z19.string().max(500).optional().meta({
|
|
4742
4985
|
description: "Accepted for conformance and then **ignored**. This authorization server issues no scopes at all, which is why the registration answer carries no `scope` field to echo one back in."
|
|
4743
4986
|
})
|
|
4744
4987
|
}).meta({
|
|
4745
4988
|
description: "What an MCP client sends to register itself, per RFC 7591. Unknown metadata is ignored rather than refused (\xA73.1), and the answer states what was actually granted rather than what was asked for (\xA73.2.1)."
|
|
4746
4989
|
});
|
|
4747
|
-
var dynamicClientRegistrationResponse =
|
|
4748
|
-
client_id:
|
|
4990
|
+
var dynamicClientRegistrationResponse = z19.object({
|
|
4991
|
+
client_id: z19.string().min(1).max(200).meta({
|
|
4749
4992
|
description: "The identifier this client sends at the authorize and token endpoints. Opaque, and not the app identifier."
|
|
4750
4993
|
}),
|
|
4751
|
-
client_name:
|
|
4994
|
+
client_name: z19.string().min(1).max(200).meta({
|
|
4752
4995
|
description: "The name the client registered under, echoed back. Chosen by the client and not vouched for by Fleetless."
|
|
4753
4996
|
}),
|
|
4754
|
-
redirect_uris:
|
|
4997
|
+
redirect_uris: z19.array(redirectUri).meta({
|
|
4755
4998
|
description: "The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else."
|
|
4756
4999
|
}),
|
|
4757
|
-
grant_types:
|
|
5000
|
+
grant_types: z19.array(z19.string()).meta({
|
|
4758
5001
|
description: 'The grants this client may use. Always exactly `["authorization_code", "refresh_token"]` \u2014 an exchange mints a refresh token and the token endpoint rotates it.'
|
|
4759
5002
|
}),
|
|
4760
|
-
response_types:
|
|
5003
|
+
response_types: z19.array(z19.string()).meta({
|
|
4761
5004
|
description: "The response types this client may ask for: `code`."
|
|
4762
5005
|
}),
|
|
4763
|
-
token_endpoint_auth_method:
|
|
5006
|
+
token_endpoint_auth_method: z19.literal("none").meta({
|
|
4764
5007
|
description: "`none` \u2014 this server registers public clients only, and PKCE rather than a secret is what protects the exchange."
|
|
4765
5008
|
}),
|
|
4766
|
-
client_id_issued_at:
|
|
5009
|
+
client_id_issued_at: z19.number().int().nonnegative().meta({
|
|
4767
5010
|
description: "When the registration was created, in seconds since the epoch, per RFC 7591."
|
|
4768
5011
|
}),
|
|
4769
|
-
client_secret_expires_at:
|
|
5012
|
+
client_secret_expires_at: z19.literal(0).meta({
|
|
4770
5013
|
description: "Always `0`, which is RFC 7591's way of saying the client secret never expires \u2014 there is none. The **registration** itself does expire: a self-registered client that never completes a flow is an unauthenticated write somebody left behind."
|
|
4771
5014
|
})
|
|
4772
5015
|
});
|
|
4773
|
-
var oauthCodeTokenRequest =
|
|
4774
|
-
grant_type:
|
|
5016
|
+
var oauthCodeTokenRequest = z19.object({
|
|
5017
|
+
grant_type: z19.literal("authorization_code").meta({
|
|
4775
5018
|
description: "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
|
|
4776
5019
|
}),
|
|
4777
|
-
code:
|
|
5020
|
+
code: z19.string().min(1).max(500).meta({
|
|
4778
5021
|
description: "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
|
|
4779
5022
|
}),
|
|
4780
5023
|
redirect_uri: redirectUri.meta({
|
|
4781
5024
|
description: "The same redirect URI the authorize request used. It is compared, not merely recorded."
|
|
4782
5025
|
}),
|
|
4783
|
-
client_id:
|
|
5026
|
+
client_id: z19.string().min(1).max(200).meta({
|
|
4784
5027
|
description: "The client making the exchange, as registered."
|
|
4785
5028
|
}),
|
|
4786
|
-
code_verifier:
|
|
5029
|
+
code_verifier: z19.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)").meta({
|
|
4787
5030
|
description: "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 \xA74.1 \u2014 it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
|
|
4788
5031
|
}),
|
|
4789
|
-
resource:
|
|
5032
|
+
resource: z19.url().optional().meta({
|
|
4790
5033
|
description: "The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else \u2014 which is what keeps a token minted for one app out of another app's endpoint."
|
|
4791
5034
|
})
|
|
4792
5035
|
}).meta({
|
|
4793
5036
|
description: "RFC 6749 \xA74.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per \xA74.1.3, though the server accepts a JSON body too."
|
|
4794
5037
|
});
|
|
4795
|
-
var oauthRefreshTokenRequest =
|
|
4796
|
-
grant_type:
|
|
5038
|
+
var oauthRefreshTokenRequest = z19.object({
|
|
5039
|
+
grant_type: z19.literal("refresh_token").meta({
|
|
4797
5040
|
description: "`refresh_token`: this request rotates a refresh token into a new access token and a new refresh token. The presented token is consumed; presenting it again revokes the whole session."
|
|
4798
5041
|
}),
|
|
4799
|
-
refresh_token:
|
|
5042
|
+
refresh_token: z19.string().min(1).max(500).meta({
|
|
4800
5043
|
description: "The refresh token from the last token response. Bound to the client that received it and to one identity space: presented by another client, or at the other MCP server, it is `invalid_grant` and stays unconsumed."
|
|
4801
5044
|
}),
|
|
4802
|
-
client_id:
|
|
5045
|
+
client_id: z19.string().min(1).max(200).meta({
|
|
4803
5046
|
description: "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
|
|
4804
5047
|
}),
|
|
4805
|
-
resource:
|
|
5048
|
+
resource: z19.url().optional().meta({
|
|
4806
5049
|
description: "The resource the new token is for, per RFC 8707. Optional; when named it must be the audience the session was issued for, or the answer is `invalid_target` and the refresh token is left untouched. The successor carries the same audience either way."
|
|
4807
5050
|
})
|
|
4808
5051
|
}).meta({
|
|
4809
5052
|
description: "RFC 6749 \xA76's refresh, as either MCP authorization server reads it. Every use rotates: the answer carries a new refresh token and the presented one is dead."
|
|
4810
5053
|
});
|
|
4811
|
-
var oauthTokenRequest =
|
|
5054
|
+
var oauthTokenRequest = z19.discriminatedUnion("grant_type", [oauthCodeTokenRequest, oauthRefreshTokenRequest]).meta({
|
|
4812
5055
|
description: "What an MCP token endpoint accepts: the authorization-code exchange, or a refresh. Any other `grant_type` is `unsupported_grant_type`, refused before a lookup happens."
|
|
4813
5056
|
});
|
|
4814
|
-
var oauthTokenResponse =
|
|
4815
|
-
access_token:
|
|
5057
|
+
var oauthTokenResponse = z19.object({
|
|
5058
|
+
access_token: z19.string().min(1).meta({
|
|
4816
5059
|
description: "The bearer token. It is the same token the client login mints \u2014 only the envelope differs, because an RFC-compliant client parses this one and knows nothing about Fleetless."
|
|
4817
5060
|
}),
|
|
4818
|
-
token_type:
|
|
5061
|
+
token_type: z19.literal("Bearer").meta({
|
|
4819
5062
|
description: "`Bearer`. RFC 6749 \xA75.1 makes the value case-insensitive for a client reading it; this is the spelling this server emits."
|
|
4820
5063
|
}),
|
|
4821
|
-
expires_in:
|
|
5064
|
+
expires_in: z19.number().int().positive().meta({
|
|
4822
5065
|
description: "How long the access token is valid, in **seconds**, per RFC 6749 \xA75.1. Not a timestamp, and not milliseconds."
|
|
4823
5066
|
}),
|
|
4824
|
-
refresh_token:
|
|
5067
|
+
refresh_token: z19.string().min(1).optional().meta({
|
|
4825
5068
|
description: "The refresh token. Both MCP token endpoints issue one on every exchange and every refresh; it rotates on every use, lives ninety days from its last use, and dies with the account's sessions \u2014 a block, a password change, a withdrawn consent. The console's own OAuth portal issues none."
|
|
4826
5069
|
}),
|
|
4827
|
-
scope:
|
|
5070
|
+
scope: z19.string().max(500).optional().meta({
|
|
4828
5071
|
description: "The scopes the issued token actually carries, space-separated."
|
|
4829
5072
|
})
|
|
4830
5073
|
});
|
|
4831
|
-
var authorizationServerMetadata =
|
|
4832
|
-
issuer:
|
|
5074
|
+
var authorizationServerMetadata = z19.object({
|
|
5075
|
+
issuer: z19.url().meta({
|
|
4833
5076
|
description: "The issuer identifier of this authorization server, per RFC 8414 \xA72. It is what a client checks a token's `iss` against."
|
|
4834
5077
|
}),
|
|
4835
|
-
authorization_endpoint:
|
|
5078
|
+
authorization_endpoint: z19.url().meta({
|
|
4836
5079
|
description: "Where a client sends the user to authorize."
|
|
4837
5080
|
}),
|
|
4838
|
-
token_endpoint:
|
|
5081
|
+
token_endpoint: z19.url().meta({
|
|
4839
5082
|
description: "The URL where a client exchanges an authorization code, or a refresh token, for tokens."
|
|
4840
5083
|
}),
|
|
4841
|
-
registration_endpoint:
|
|
5084
|
+
registration_endpoint: z19.url().optional().meta({
|
|
4842
5085
|
description: "The URL where a client may register itself, per RFC 7591. Absent when the app does not accept dynamic clients."
|
|
4843
5086
|
}),
|
|
4844
|
-
response_types_supported:
|
|
5087
|
+
response_types_supported: z19.array(z19.literal("code")).meta({
|
|
4845
5088
|
description: "The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1."
|
|
4846
5089
|
}),
|
|
4847
|
-
grant_types_supported:
|
|
5090
|
+
grant_types_supported: z19.array(z19.enum(["authorization_code", "refresh_token"])).meta({
|
|
4848
5091
|
description: "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
|
|
4849
5092
|
}),
|
|
4850
|
-
code_challenge_methods_supported:
|
|
5093
|
+
code_challenge_methods_supported: z19.array(codeChallengeMethod).meta({
|
|
4851
5094
|
description: "The PKCE challenge methods accepted: `S256` only. `plain` is not offered \u2014 a challenge equal to its verifier defends against nothing, and offering it would make a downgrade negotiable."
|
|
4852
5095
|
}),
|
|
4853
|
-
token_endpoint_auth_methods_supported:
|
|
5096
|
+
token_endpoint_auth_methods_supported: z19.array(z19.literal("none")).meta({
|
|
4854
5097
|
description: "How a client authenticates at the token endpoint: `none`, the public-client method, with PKCE protecting the exchange."
|
|
4855
5098
|
}),
|
|
4856
|
-
scopes_supported:
|
|
5099
|
+
scopes_supported: z19.array(z19.string()).optional().meta({
|
|
4857
5100
|
description: "The scopes this server knows about, where it publishes a list."
|
|
4858
5101
|
})
|
|
4859
5102
|
});
|
|
4860
|
-
var protectedResourceMetadata =
|
|
4861
|
-
resource:
|
|
5103
|
+
var protectedResourceMetadata = z19.object({
|
|
5104
|
+
resource: z19.url().meta({
|
|
4862
5105
|
description: "The resource identifier this document describes, per RFC 9728. A token whose audience names something else is rejected here rather than merely noted."
|
|
4863
5106
|
}),
|
|
4864
|
-
authorization_servers:
|
|
5107
|
+
authorization_servers: z19.array(z19.url()).min(1).meta({
|
|
4865
5108
|
description: "The authorization servers that may issue tokens for this resource. There is always at least one."
|
|
4866
5109
|
}),
|
|
4867
|
-
bearer_methods_supported:
|
|
5110
|
+
bearer_methods_supported: z19.array(z19.literal("header")).meta({
|
|
4868
5111
|
description: "How a token may be presented: in the `Authorization` header only, never in a query parameter or a form field."
|
|
4869
5112
|
}),
|
|
4870
|
-
scopes_supported:
|
|
5113
|
+
scopes_supported: z19.array(z19.string()).optional().meta({
|
|
4871
5114
|
description: "The scopes this resource understands, where it publishes a list."
|
|
4872
5115
|
})
|
|
4873
5116
|
});
|
|
4874
|
-
var oauthRedirectResponse =
|
|
4875
|
-
redirect_to:
|
|
5117
|
+
var oauthRedirectResponse = z19.object({
|
|
5118
|
+
redirect_to: z19.string().min(1).max(2e3)
|
|
4876
5119
|
});
|
|
4877
|
-
var oauthAuthorizeQuery =
|
|
4878
|
-
response_type:
|
|
5120
|
+
var oauthAuthorizeQuery = z19.object({
|
|
5121
|
+
response_type: z19.literal("code").meta({
|
|
4879
5122
|
description: "Always `code`. RFC 6749 \xA74.1.2.1 names `unsupported_response_type` for any other value, but `oauthErrorCode` has no such member \u2014 this server issues no other grant from this endpoint \u2014 so an unsupported value comes back on the callback as `invalid_request`."
|
|
4880
5123
|
}),
|
|
4881
|
-
client_id:
|
|
5124
|
+
client_id: z19.string().min(1).meta({
|
|
4882
5125
|
description: "The OAuth client, self-registered or the one well-known central client \u2014 **not** the app identifier. Unknown, expired-dynamic and mismatched clients all collapse into the same `400 invalid_client`, answered without a redirect."
|
|
4883
5126
|
}),
|
|
4884
|
-
redirect_uri:
|
|
5127
|
+
redirect_uri: z19.string().min(1).meta({
|
|
4885
5128
|
description: "One of the client's registered redirect URIs, compared **exactly** \u2014 string equality against the registered list, never a prefix or a host match. Both the shape (`redirectUri`) and the registration are checked, and a failure of either is a `400 invalid_request` with no redirect."
|
|
4886
5129
|
}),
|
|
4887
|
-
code_challenge:
|
|
5130
|
+
code_challenge: z19.string().min(1).meta({
|
|
4888
5131
|
description: "The PKCE challenge; the verifier is presented at the token endpoint. Only non-emptiness is checked here \u2014 length and alphabet are not \u2014 since the verifier is what has to match."
|
|
4889
5132
|
}),
|
|
4890
|
-
code_challenge_method:
|
|
5133
|
+
code_challenge_method: z19.literal("S256").meta({
|
|
4891
5134
|
description: "Only `S256`. `plain` is refused: a challenge equal to its verifier defends against nothing."
|
|
4892
5135
|
}),
|
|
4893
|
-
state:
|
|
5136
|
+
state: z19.string().optional().meta({
|
|
4894
5137
|
description: "Returned unchanged on the callback, and on the error redirect too, so a client can bind either answer to its own request."
|
|
4895
5138
|
}),
|
|
4896
|
-
resource:
|
|
5139
|
+
resource: z19.string().optional().meta({
|
|
4897
5140
|
description: "RFC 8707 resource indicator: the API origin or the MCP endpoint the token is for. Checked against the resources this server issues tokens for **on behalf of this client's app**; a mismatch is `invalid_target` on the callback."
|
|
4898
5141
|
})
|
|
4899
5142
|
// **No `scope`, because this authorization server issues none.** The field
|
|
@@ -4906,7 +5149,7 @@ var oauthAuthorizeQuery = z18.object({
|
|
|
4906
5149
|
description: "The authorization request an MCP client sends, per RFC 6749 \xA74.1.1 with mandatory PKCE. The handler reads it parameter by parameter rather than through one parse, because the answers differ: `client_id` and `redirect_uri` are refused flat, with no redirect, since until both are confirmed there is no trusted target to bounce a browser to, and everything after them is reported to the client's own callback as query parameters."
|
|
4907
5150
|
});
|
|
4908
5151
|
|
|
4909
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
5152
|
+
// node_modules/.pnpm/@fleetless+contracts@6.0.0/node_modules/@fleetless/contracts/dist/routes.js
|
|
4910
5153
|
var MCP_APP = MCP_APP_PATHS(":appIdentifier");
|
|
4911
5154
|
var APP_IDENTIFIER = {
|
|
4912
5155
|
name: "appIdentifier",
|
|
@@ -4923,11 +5166,123 @@ var IN_HANDLER_ROUTES = [
|
|
|
4923
5166
|
// The one route whose bearer is **optional**: it answers the same document
|
|
4924
5167
|
// with or without one, and only `already_granted` moves.
|
|
4925
5168
|
"GET /api/client/mcp/interactions/:id",
|
|
5169
|
+
// Two-factor setup: during sign-in the challenge in the body is the
|
|
5170
|
+
// credential, from the app's account settings the app user's bearer is.
|
|
5171
|
+
// Either one, decided in the handler.
|
|
5172
|
+
"POST /api/client/two-factor/setup",
|
|
5173
|
+
"POST /api/client/two-factor/setup/confirm",
|
|
4926
5174
|
"GET /api/asset-links/missing",
|
|
4927
5175
|
"GET /api/asset-links/:token"
|
|
4928
5176
|
];
|
|
4929
5177
|
var DEVELOPER_GUARD = ["unauthorized", "token_expired", "token_revoked"];
|
|
4930
5178
|
var CLIENT_GUARD = ["unauthorized", "token_expired", "token_revoked", "forbidden"];
|
|
5179
|
+
function developerSignInRoutes(prefix) {
|
|
5180
|
+
const section = prefix === "/console/oauth" ? "developer-auth" : "mcp";
|
|
5181
|
+
const done = prefix === "/console/oauth" ? "the console callback with the authorization code" : "the consent screen for a self-registered client, or straight to the callback for the central one";
|
|
5182
|
+
const page = (path, summary, notes) => ({
|
|
5183
|
+
method: "GET",
|
|
5184
|
+
path: `${prefix}${path}`,
|
|
5185
|
+
section,
|
|
5186
|
+
summary,
|
|
5187
|
+
audience: "internal",
|
|
5188
|
+
auth: "none",
|
|
5189
|
+
rateLimited: false,
|
|
5190
|
+
ownerTier: false,
|
|
5191
|
+
status: 200,
|
|
5192
|
+
params: [{ name: "id", description: "The interaction id of this sign-in; the step before redirects the browser here." }],
|
|
5193
|
+
query: null,
|
|
5194
|
+
request: null,
|
|
5195
|
+
response: null,
|
|
5196
|
+
errors: [],
|
|
5197
|
+
transport: "http",
|
|
5198
|
+
notes
|
|
5199
|
+
});
|
|
5200
|
+
const step = (path, summary, response, errors, notes) => ({
|
|
5201
|
+
method: "POST",
|
|
5202
|
+
path: `${prefix}${path}`,
|
|
5203
|
+
section,
|
|
5204
|
+
summary,
|
|
5205
|
+
audience: "internal",
|
|
5206
|
+
auth: "none",
|
|
5207
|
+
rateLimited: true,
|
|
5208
|
+
ownerTier: false,
|
|
5209
|
+
status: 200,
|
|
5210
|
+
params: [],
|
|
5211
|
+
query: null,
|
|
5212
|
+
request: null,
|
|
5213
|
+
response,
|
|
5214
|
+
errors: ["rate_limited", ...errors],
|
|
5215
|
+
transport: "http",
|
|
5216
|
+
notes
|
|
5217
|
+
});
|
|
5218
|
+
return [
|
|
5219
|
+
step("/identify", "Takes the email address, mails a sign-in code and hands back the code step.", null, ["validation_error", "token_spent", "wrong_browser"], `The page answers \`Check your email\` **for every address**: a known one gets \`Your Fleetless sign-in code\`, six digits valid ten minutes; an unknown one gets a mail saying no Fleetless account uses it, with a link to sign up (or the waiting list while sign-up is closed). So the page never reveals who has an account, and the address is trimmed and compared case-insensitively. A request within sixty seconds of the last one for the same address renders the same page without a second mail. The browser-proof cookie is set here when the interaction has none yet, and checked when it has. A browser form post gets the code card; a JSON caller gets \`{ "next": "${prefix}/code" }\`, which has no schema. A dead interaction is \`410 token_spent\`.`),
|
|
5220
|
+
step("/code", "Checks the emailed code and finishes the sign-in, or hands back the second step.", oauthRedirectResponse, ["validation_error", "token_spent", "wrong_browser", "invalid_code"], `A wrong code renders the code card again with the attempts left (\`400 invalid_code\`, \`details.attempts_left\`); a code spent, past its ten minutes or out of its five attempts is \`410 token_spent\` and a new one has to be asked for. Spaces inside a typed code are removed before it is checked. With no second factor to give, a browser gets a \`303\` to ${done}, and a JSON caller that URL as \`redirect_to\`. Otherwise the next step: a JSON caller gets \`{ "next" }\` naming \`` + prefix + "/two-factor` \u2014 the person has a passkey or an authenticator \u2014 or `" + prefix + "/two-factor/setup` \u2014 the organisation requires one and the person has none \u2014 and a browser the page itself. Audited as `developer.login` with `details.method` `email_code` once the sign-in completes."),
|
|
5221
|
+
page("/two-factor/:id", "Serves the second step: the authenticator code, or the passkey prompt.", "HTML. Six boxes for the authenticator code, `Use a passkey`, and `Use a recovery code`; a developer with passkeys only sees the passkey prompt directly. The browser-proof cookie is checked on this GET too. A dead interaction renders the `410` page."),
|
|
5222
|
+
page("/two-factor/:id/recovery", "Serves the recovery-code page of the second step.", "HTML. One field for a recovery code, and the way out when none is left: an owner of the organisation can reset the member's two-factor in Settings \u203A Team."),
|
|
5223
|
+
step("/two-factor", "Checks an authenticator code or a recovery code and finishes the sign-in.", oauthRedirectResponse, ["validation_error", "token_spent", "wrong_browser", "invalid_code"], `The form carries either \`code\` or \`recovery_code\`. **An authenticator code is accepted at most once**, so the same code sent twice finishes one sign-in. A recovery code is spent by its use. Five wrong codes end the interaction (\`410 token_spent\`). A browser gets a \`303\` to ${done}; a JSON caller that URL as \`redirect_to\`.`),
|
|
5224
|
+
step("/passkey/options", "Answers the WebAuthn request options for a passkey sign-in or second step.", webauthnOptionsResponse, ["token_spent", "wrong_browser"], "Before an address is known the options name no credential, so the browser offers every discoverable passkey for `fleetless.dev` (`Sign in with a passkey`); after the code step they name the account's own passkeys. User verification is required. The challenge is bound to the interaction and single-use. **This is the first step of a passkey sign-in**, which has no email step: the browser-proof cookie is set here when the interaction has none yet, and checked when it has \u2014 so `POST " + prefix + "/passkey` can require it."),
|
|
5225
|
+
step("/passkey", "Checks a passkey assertion; a passkey completes the sign-in on its own.", oauthRedirectResponse, ["validation_error", "token_spent", "wrong_browser", "invalid_credentials"], `**A passkey is a full sign-in**: it proves possession and user verification, two factors, so it skips the emailed code and the second step \u2014 used as the second step, it finishes it. An assertion that does not verify, or names no passkey of an account, is \`401 invalid_credentials\`, the same answer for both. When the organisation requires two-factor, a passkey satisfies it. A browser gets a \`303\` to ${done}; a JSON caller that URL as \`redirect_to\`. Audited as \`developer.login\` with \`details.method\` \`passkey\`.`),
|
|
5226
|
+
page("/two-factor/setup/:id", 'Serves the "your organisation requires two-factor" choice between a passkey and an authenticator.', "HTML. Reached when the organisation requires two-factor and the person has none; no session exists until the setup is done. Two options, the passkey recommended because it also signs the person in without an emailed code. `Signed in as <email> \xB7 Sign out` under it."),
|
|
5227
|
+
step("/two-factor/setup/totp", "Starts an authenticator setup inside the sign-in and hands back its QR code and key.", twoFactorSetupResponse, ["token_spent", "wrong_browser"], "A browser gets the page with the QR code, the key and the confirm field; a JSON caller the secret and `otpauth_url`. Nothing is stored as confirmed yet."),
|
|
5228
|
+
step("/two-factor/setup/totp/confirm", "Confirms the new authenticator and hands back the ten recovery codes.", recoveryCodesResponse, ["validation_error", "token_spent", "wrong_browser", "invalid_code"], "A code that does not match is `400 invalid_code`. On success the authenticator is on and ten recovery codes are shown once; `I saved my recovery codes` then finishes the sign-in. Audited as `developer.two_factor_added` with `details.kind` `authenticator`."),
|
|
5229
|
+
step("/two-factor/setup/passkey/options", "Answers the WebAuthn creation options for a passkey set up inside the sign-in.", webauthnOptionsResponse, ["token_spent", "wrong_browser"], "The same options `POST /api/auth/passkeys/options` answers a signed-in developer: relying party `fleetless.dev`, user verification required, a discoverable credential."),
|
|
5230
|
+
step("/two-factor/setup/passkey", "Registers the passkey and hands back the ten recovery codes.", recoveryCodesResponse, ["validation_error", "token_spent", "wrong_browser"], "A ceremony that does not verify is `400 validation_error`. On success the passkey is stored, named after the browser's device where it says so, and ten recovery codes are shown once. Audited as `developer.two_factor_added` with `details.kind` `passkey`."),
|
|
5231
|
+
step("/two-factor/setup/done", "Finishes the sign-in once the recovery codes are saved.", oauthRedirectResponse, ["token_spent", "wrong_browser"], `\`I saved my recovery codes\` gates the button on the page; the step itself only checks that a second factor now exists. A browser gets a \`303\` to ${done}; a JSON caller that URL as \`redirect_to\`.`)
|
|
5232
|
+
];
|
|
5233
|
+
}
|
|
5234
|
+
var HOSTED_TOKEN = {
|
|
5235
|
+
name: "token",
|
|
5236
|
+
description: "The opaque token from the mailed link; it is never sent as a query parameter."
|
|
5237
|
+
};
|
|
5238
|
+
var HOSTED_INTERACTION = {
|
|
5239
|
+
name: "interaction",
|
|
5240
|
+
description: "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
5241
|
+
};
|
|
5242
|
+
function hostedPage(method, path, summary, notes, params = []) {
|
|
5243
|
+
return {
|
|
5244
|
+
method,
|
|
5245
|
+
path: `/app/:appIdentifier${path}`,
|
|
5246
|
+
section: "client-auth",
|
|
5247
|
+
summary,
|
|
5248
|
+
audience: "internal",
|
|
5249
|
+
auth: "none",
|
|
5250
|
+
rateLimited: method === "POST",
|
|
5251
|
+
ownerTier: false,
|
|
5252
|
+
status: 200,
|
|
5253
|
+
params: [APP_IDENTIFIER, ...params],
|
|
5254
|
+
query: null,
|
|
5255
|
+
request: null,
|
|
5256
|
+
response: null,
|
|
5257
|
+
errors: method === "POST" ? ["rate_limited"] : [],
|
|
5258
|
+
transport: "http",
|
|
5259
|
+
notes
|
|
5260
|
+
};
|
|
5261
|
+
}
|
|
5262
|
+
var HOSTED_FORM = "Renders a form; only its `POST` spends the token, so a mail scanner opening the link changes nothing. ";
|
|
5263
|
+
var HOSTED_DEAD = "A spent, expired or unknown token renders the `410` page with the next step for its kind.";
|
|
5264
|
+
var HOSTED_POST = "HTML: the next page on success, the same page with the problem named on a refusal. ";
|
|
5265
|
+
var HOSTED_APP_ROUTES = [
|
|
5266
|
+
hostedPage("GET", "/logo", "Serves the app's logo for its hosted pages.", "The stored PNG or SVG, as written through `PUT /api/apps/:id/auth-config/logo`; `404` when the app has none. **Served sandboxed**: `X-Content-Type-Options: nosniff` and `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox`, so a script inside an SVG never runs, even opened directly. The hosted pages embed it as an image only."),
|
|
5267
|
+
hostedPage("GET", "/mcp/:interaction", "Serves the hosted MCP sign-in, the page an unset `mcp_login_url` falls back to.", "HTML: the app's identity providers, then the sign-in the app offers \u2014 email and password with `Email me a sign-in code instead`, or email and `Email me a code` for a code-only app \u2014 and `Create one` when self-registration is on. A dead interaction renders the `410` page. The browser-proof cookie is set here.", [HOSTED_INTERACTION]),
|
|
5268
|
+
hostedPage("POST", "/mcp/:interaction/password", "Checks the email and password of the hosted MCP sign-in.", HOSTED_POST + "One refusal for every miss, as `POST /api/client/login` answers. With a second factor to give, the two-factor page follows; otherwise the consent page.", [HOSTED_INTERACTION]),
|
|
5269
|
+
hostedPage("POST", "/mcp/:interaction/code", "Mails a sign-in code for the hosted MCP sign-in and renders the code page.", HOSTED_POST + "The same page for a known and an unknown address, as `POST /api/client/login/code` answers; six boxes, `Resend code in 0:42`, and `Use password instead` where the app has passwords on.", [HOSTED_INTERACTION]),
|
|
5270
|
+
hostedPage("POST", "/mcp/:interaction/code/verify", "Checks the emailed code of the hosted MCP sign-in.", HOSTED_POST + "A wrong code renders the code page with the attempts left; a spent one asks for a new code. With a second factor to give, the two-factor page follows; otherwise the consent page.", [HOSTED_INTERACTION]),
|
|
5271
|
+
hostedPage("POST", "/mcp/:interaction/consent", "Records the allow-or-deny of the hosted MCP sign-in and sends the browser back to the client.", "Answers a `303` to the MCP client's callback, carrying the code or `error=access_denied`, as `POST /api/client/mcp/interactions/:id/approve` and `\u2026/deny` do. Fail-closed: anything but the Allow value denies. The browser-proof cookie set at the sign-in must match, or the `wrong_browser` page renders.", [HOSTED_INTERACTION]),
|
|
5272
|
+
hostedPage("POST", "/two-factor", "Checks an authenticator or recovery code on a hosted page and finishes the sign-in it interrupted.", HOSTED_POST + "The form carries the challenge the step before answered and either `code` or `recovery_code`, checked as `POST /api/client/two-factor/verify` checks them. Success continues where the sign-in was going: the MCP consent, or the done page."),
|
|
5273
|
+
hostedPage("POST", "/two-factor/setup", "Starts an authenticator setup on a hosted page, when the app requires two-factor.", HOSTED_POST + "Renders the QR code, the key with `Copy key`, and the confirm field, for the challenge the step before answered."),
|
|
5274
|
+
hostedPage("POST", "/two-factor/setup/confirm", "Confirms the new authenticator on a hosted page and shows the ten recovery codes.", HOSTED_POST + "A wrong code renders the setup page again. On success the ten recovery codes are shown once, with `Copy` and `Download .txt`; `I saved my recovery codes` gates `Continue`."),
|
|
5275
|
+
hostedPage("GET", "/invite/:token", "Serves the hosted invitation page, the one an unset `invite_url` falls back to.", "HTML. " + HOSTED_FORM + "Asks for a name, and a password only while the app has passwords on; announces the two-factor setup when the app requires it. " + HOSTED_DEAD, [HOSTED_TOKEN]),
|
|
5276
|
+
hostedPage("POST", "/invite", "Accepts an invitation from the hosted invitation page.", HOSTED_POST + "Spends the token as `POST /api/client/invitations/accept` does; the two-factor setup follows when the app requires it, then the done page."),
|
|
5277
|
+
hostedPage("GET", "/reset/:token", "Serves the hosted new-password page, the one an unset `reset_url` falls back to.", "HTML. " + HOSTED_FORM + "Says that two-factor stays on. " + HOSTED_DEAD, [HOSTED_TOKEN]),
|
|
5278
|
+
hostedPage("POST", "/reset", "Sets the new password from the hosted new-password page.", HOSTED_POST + "Spends the token as `POST /api/client/password/reset/confirm` does; a person with an authenticator gives a code before the done page."),
|
|
5279
|
+
hostedPage("GET", "/forgot", 'Serves the hosted "forgot your password" page.', "HTML: the address field. Reached from the hosted sign-in and the dead-link page."),
|
|
5280
|
+
hostedPage("POST", "/forgot", 'Mails a reset link from the hosted "forgot your password" page.', HOSTED_POST + 'The same "check your mail" page for a known and an unknown address, as `POST /api/client/password/reset` answers.'),
|
|
5281
|
+
hostedPage("GET", "/sign-up", "Serves the hosted sign-up page, when the app has self-registration on.", 'HTML: the address, a password while the app has passwords on, and an optional name. With self-registration off it renders the "registration closed" page.'),
|
|
5282
|
+
hostedPage("POST", "/sign-up", "Creates an account from the hosted sign-up page and mails the confirmation link.", HOSTED_POST + 'Registers as `POST /api/client/register` does, and renders the same "check your mail" page for a new and a known address.'),
|
|
5283
|
+
hostedPage("GET", "/verify/:token", "Serves the hosted email-confirmation page, the one an unset `verify_url` falls back to.", "HTML. " + HOSTED_FORM + HOSTED_DEAD, [HOSTED_TOKEN]),
|
|
5284
|
+
hostedPage("POST", "/verify", "Confirms the address from the hosted email-confirmation page.", HOSTED_POST + "Spends the token as `POST /api/client/verify-email` does; the two-factor setup follows when the app requires it, then the done page.")
|
|
5285
|
+
];
|
|
4931
5286
|
var ROUTES = [
|
|
4932
5287
|
/* ------------------------------------------------------------- health */
|
|
4933
5288
|
{
|
|
@@ -4949,24 +5304,6 @@ var ROUTES = [
|
|
|
4949
5304
|
notes: "Answers `{ ok, dependencies: { database, storage, liveKit } }` \u2014 a cloud-local shape, not a wire contract, so nothing here pins it. The status is always `200`: each dependency is probed independently and a failure is reported in the body rather than thrown, because `/healthz` must never itself be a reason the process looks down. Read `ok`, not the status code."
|
|
4950
5305
|
},
|
|
4951
5306
|
/* ----------------------------------------------------- developer auth */
|
|
4952
|
-
{
|
|
4953
|
-
method: "POST",
|
|
4954
|
-
path: "/api/auth/signup",
|
|
4955
|
-
section: "developer-auth",
|
|
4956
|
-
summary: "Creates an org and its founding Owner, and answers a developer session.",
|
|
4957
|
-
audience: "developer",
|
|
4958
|
-
auth: "none",
|
|
4959
|
-
rateLimited: true,
|
|
4960
|
-
ownerTier: false,
|
|
4961
|
-
status: 201,
|
|
4962
|
-
params: [],
|
|
4963
|
-
query: null,
|
|
4964
|
-
request: signUpRequest,
|
|
4965
|
-
response: signUpResponse,
|
|
4966
|
-
errors: ["rate_limited", "signup_closed", "validation_error", "email_taken"],
|
|
4967
|
-
transport: "http",
|
|
4968
|
-
notes: "While the deployment runs in closed beta this answers `403 signup_closed` before it looks at the body \u2014 there is nothing for a validation message, or an `email_taken` answer, to be right about when nothing will be created. Email is globally unique, so an address already registered in any org is refused."
|
|
4969
|
-
},
|
|
4970
5307
|
{
|
|
4971
5308
|
method: "POST",
|
|
4972
5309
|
path: "/api/auth/refresh",
|
|
@@ -4983,7 +5320,7 @@ var ROUTES = [
|
|
|
4983
5320
|
response: sessionTokens,
|
|
4984
5321
|
errors: ["rate_limited", "validation_error", "token_expired", "token_revoked"],
|
|
4985
5322
|
transport: "http",
|
|
4986
|
-
notes: "The whole family is re-checked here, not just the token: an account that has been removed from the org, or whose `token_version` was bumped by
|
|
5323
|
+
notes: "The whole family is re-checked here, not just the token: an account that has been removed from the org, or whose `token_version` was bumped by an owner's two-factor reset, cannot mint a fresh console token and answers `token_revoked`. Refusing that only on the other routes would leave a session that is dead everywhere but here."
|
|
4987
5324
|
},
|
|
4988
5325
|
{
|
|
4989
5326
|
method: "POST",
|
|
@@ -5038,11 +5375,12 @@ var ROUTES = [
|
|
|
5038
5375
|
transport: "http",
|
|
5039
5376
|
notes: "No Owner tier: this can only ever touch the caller's own row, so there is nothing for a tier check to gate. Saving the name already held writes nothing and records no audit event \u2014 the org activity stream reaches every developer with the console open, and an event for a no-op would misreport that something changed."
|
|
5040
5377
|
},
|
|
5378
|
+
/* ------------------------------------ a developer's own second factors */
|
|
5041
5379
|
{
|
|
5042
|
-
method: "
|
|
5043
|
-
path: "/api/auth/
|
|
5380
|
+
method: "GET",
|
|
5381
|
+
path: "/api/auth/two-factor",
|
|
5044
5382
|
section: "developer-auth",
|
|
5045
|
-
summary: "
|
|
5383
|
+
summary: "Answers the calling developer's passkeys, authenticator, recovery codes left and the org's policy.",
|
|
5046
5384
|
audience: "developer",
|
|
5047
5385
|
auth: "developer",
|
|
5048
5386
|
rateLimited: false,
|
|
@@ -5050,162 +5388,233 @@ var ROUTES = [
|
|
|
5050
5388
|
status: 200,
|
|
5051
5389
|
params: [],
|
|
5052
5390
|
query: null,
|
|
5053
|
-
request:
|
|
5054
|
-
response:
|
|
5055
|
-
errors: [...DEVELOPER_GUARD
|
|
5391
|
+
request: null,
|
|
5392
|
+
response: developerTwoFactor,
|
|
5393
|
+
errors: [...DEVELOPER_GUARD],
|
|
5056
5394
|
transport: "http",
|
|
5057
|
-
notes: "
|
|
5395
|
+
notes: "What Settings \u203A Profile \u203A Security draws. No key material, secret or code travels here \u2014 the passkeys are names and dates, the authenticator is a date, the recovery codes are a count."
|
|
5058
5396
|
},
|
|
5059
5397
|
{
|
|
5060
5398
|
method: "POST",
|
|
5061
|
-
path: "/api/auth/
|
|
5399
|
+
path: "/api/auth/passkeys/options",
|
|
5062
5400
|
section: "developer-auth",
|
|
5063
|
-
summary: "
|
|
5401
|
+
summary: "Answers the WebAuthn creation options for registering a passkey.",
|
|
5064
5402
|
audience: "developer",
|
|
5065
|
-
auth: "
|
|
5066
|
-
rateLimited:
|
|
5403
|
+
auth: "developer",
|
|
5404
|
+
rateLimited: false,
|
|
5067
5405
|
ownerTier: false,
|
|
5068
|
-
status:
|
|
5406
|
+
status: 200,
|
|
5069
5407
|
params: [],
|
|
5070
5408
|
query: null,
|
|
5071
|
-
request:
|
|
5072
|
-
response:
|
|
5073
|
-
errors: [
|
|
5409
|
+
request: null,
|
|
5410
|
+
response: webauthnOptionsResponse,
|
|
5411
|
+
errors: [...DEVELOPER_GUARD],
|
|
5074
5412
|
transport: "http",
|
|
5075
|
-
notes:
|
|
5413
|
+
notes: "Hand `options` to the browser's WebAuthn API as it is. The relying party is `fleetless.dev`, so the passkey works on the auth portal and in the console alike; user verification is required and the credential is discoverable, so it can sign the person in without an address. The passkeys the caller already has are excluded. The challenge is single-use and expires with the ceremony."
|
|
5076
5414
|
},
|
|
5077
|
-
/* ------------------------------------------- client auth (portal pages) */
|
|
5078
5415
|
{
|
|
5079
|
-
method: "
|
|
5080
|
-
path: "/
|
|
5081
|
-
section: "
|
|
5082
|
-
summary:
|
|
5083
|
-
audience: "
|
|
5084
|
-
auth: "
|
|
5416
|
+
method: "POST",
|
|
5417
|
+
path: "/api/auth/passkeys",
|
|
5418
|
+
section: "developer-auth",
|
|
5419
|
+
summary: "Registers a passkey from the browser's answer to the creation options.",
|
|
5420
|
+
audience: "developer",
|
|
5421
|
+
auth: "developer",
|
|
5085
5422
|
rateLimited: false,
|
|
5086
5423
|
ownerTier: false,
|
|
5087
|
-
status:
|
|
5424
|
+
status: 201,
|
|
5088
5425
|
params: [],
|
|
5089
5426
|
query: null,
|
|
5090
|
-
request:
|
|
5091
|
-
response:
|
|
5092
|
-
errors: [],
|
|
5427
|
+
request: createPasskeyRequest,
|
|
5428
|
+
response: createPasskeyResponse,
|
|
5429
|
+
errors: [...DEVELOPER_GUARD, "validation_error"],
|
|
5093
5430
|
transport: "http",
|
|
5094
|
-
notes:
|
|
5431
|
+
notes: "A ceremony that does not verify \u2014 a wrong challenge, origin or relying party, no user verification \u2014 is `400 validation_error` naming `credential`. When this is the account's first second factor, ten recovery codes are issued and answered once; otherwise `recovery_codes` is `null` and the existing ones stay valid. Audited as `developer.two_factor_added` with `details.kind` `passkey`."
|
|
5095
5432
|
},
|
|
5096
5433
|
{
|
|
5097
|
-
method: "
|
|
5098
|
-
path: "/
|
|
5099
|
-
section: "
|
|
5100
|
-
summary:
|
|
5101
|
-
audience: "
|
|
5102
|
-
auth: "
|
|
5434
|
+
method: "PATCH",
|
|
5435
|
+
path: "/api/auth/passkeys/:id",
|
|
5436
|
+
section: "developer-auth",
|
|
5437
|
+
summary: "Renames one of the caller's passkeys.",
|
|
5438
|
+
audience: "developer",
|
|
5439
|
+
auth: "developer",
|
|
5103
5440
|
rateLimited: false,
|
|
5104
5441
|
ownerTier: false,
|
|
5105
5442
|
status: 200,
|
|
5106
|
-
params: [{ name: "
|
|
5443
|
+
params: [{ name: "id", description: "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`." }],
|
|
5107
5444
|
query: null,
|
|
5108
|
-
request:
|
|
5109
|
-
response:
|
|
5110
|
-
errors: [],
|
|
5111
|
-
transport: "http"
|
|
5112
|
-
notes: 'HTML. An unknown, spent or expired token renders one "link no longer valid" page at `410` \u2014 they are one refusal on the wire already, and splitting them here would tell a stranger which tokens ever existed. No rate limiter: the GET changes nothing, and the POST it leads to is limited per IP.'
|
|
5445
|
+
request: renamePasskeyRequest,
|
|
5446
|
+
response: developerPasskey,
|
|
5447
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5448
|
+
transport: "http"
|
|
5113
5449
|
},
|
|
5114
5450
|
{
|
|
5115
|
-
method: "
|
|
5116
|
-
path: "/
|
|
5117
|
-
section: "
|
|
5118
|
-
summary: "
|
|
5119
|
-
audience: "
|
|
5120
|
-
auth: "
|
|
5451
|
+
method: "DELETE",
|
|
5452
|
+
path: "/api/auth/passkeys/:id",
|
|
5453
|
+
section: "developer-auth",
|
|
5454
|
+
summary: "Removes one of the caller's passkeys.",
|
|
5455
|
+
audience: "developer",
|
|
5456
|
+
auth: "developer",
|
|
5121
5457
|
rateLimited: false,
|
|
5122
5458
|
ownerTier: false,
|
|
5123
|
-
status:
|
|
5124
|
-
params: [],
|
|
5459
|
+
status: 204,
|
|
5460
|
+
params: [{ name: "id", description: "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`." }],
|
|
5125
5461
|
query: null,
|
|
5126
5462
|
request: null,
|
|
5127
5463
|
response: null,
|
|
5128
|
-
errors: [],
|
|
5464
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict"],
|
|
5129
5465
|
transport: "http",
|
|
5130
|
-
notes: "
|
|
5466
|
+
notes: "`409 target_state_conflict` names `two_factor` with rule `required_by_org` when this is the caller's last second factor and the organisation requires one. Removing the last one otherwise also voids the recovery codes. Audited as `developer.two_factor_removed` with `details.kind` `passkey`."
|
|
5131
5467
|
},
|
|
5132
5468
|
{
|
|
5133
5469
|
method: "POST",
|
|
5134
|
-
path: "/api/auth/
|
|
5470
|
+
path: "/api/auth/totp",
|
|
5135
5471
|
section: "developer-auth",
|
|
5136
|
-
summary: "
|
|
5472
|
+
summary: "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
5137
5473
|
audience: "developer",
|
|
5138
|
-
auth: "
|
|
5139
|
-
rateLimited:
|
|
5474
|
+
auth: "developer",
|
|
5475
|
+
rateLimited: false,
|
|
5140
5476
|
ownerTier: false,
|
|
5141
|
-
status:
|
|
5477
|
+
status: 200,
|
|
5142
5478
|
params: [],
|
|
5143
5479
|
query: null,
|
|
5144
|
-
request:
|
|
5145
|
-
response:
|
|
5146
|
-
errors: [
|
|
5480
|
+
request: null,
|
|
5481
|
+
response: twoFactorSetupResponse,
|
|
5482
|
+
errors: [...DEVELOPER_GUARD],
|
|
5147
5483
|
transport: "http",
|
|
5148
|
-
notes: "
|
|
5484
|
+
notes: "The secret is pending until `POST /api/auth/totp/confirm` accepts a code from it; a second call replaces a pending secret. A developer who already has an authenticator keeps it until the new one is confirmed, which is how `Replace\u2026` works."
|
|
5149
5485
|
},
|
|
5150
5486
|
{
|
|
5151
5487
|
method: "POST",
|
|
5152
|
-
path: "/api/
|
|
5488
|
+
path: "/api/auth/totp/confirm",
|
|
5153
5489
|
section: "developer-auth",
|
|
5154
|
-
summary: "
|
|
5490
|
+
summary: "Confirms the pending authenticator with a code it shows now.",
|
|
5155
5491
|
audience: "developer",
|
|
5156
|
-
auth: "
|
|
5492
|
+
auth: "developer",
|
|
5157
5493
|
rateLimited: true,
|
|
5158
5494
|
ownerTier: false,
|
|
5159
|
-
status:
|
|
5495
|
+
status: 200,
|
|
5160
5496
|
params: [],
|
|
5161
5497
|
query: null,
|
|
5162
|
-
request:
|
|
5163
|
-
response:
|
|
5164
|
-
errors: ["rate_limited", "validation_error"],
|
|
5498
|
+
request: totpConfirmRequest,
|
|
5499
|
+
response: totpConfirmResponse,
|
|
5500
|
+
errors: [...DEVELOPER_GUARD, "rate_limited", "validation_error", "invalid_code", "token_spent"],
|
|
5165
5501
|
transport: "http",
|
|
5166
|
-
notes: "
|
|
5502
|
+
notes: "A code that does not match the pending secret is `400 invalid_code`; no pending setup is `410 token_spent`. On success the new authenticator replaces any earlier one. Ten recovery codes are answered when it is the account's first second factor, otherwise `null`. Audited as `developer.two_factor_added` with `details.kind` `authenticator`."
|
|
5167
5503
|
},
|
|
5168
|
-
/* ---------------------------------------------------------------- org */
|
|
5169
5504
|
{
|
|
5170
|
-
method: "
|
|
5171
|
-
path: "/api/
|
|
5172
|
-
section: "
|
|
5173
|
-
summary: "
|
|
5505
|
+
method: "DELETE",
|
|
5506
|
+
path: "/api/auth/totp",
|
|
5507
|
+
section: "developer-auth",
|
|
5508
|
+
summary: "Removes the caller's authenticator app.",
|
|
5174
5509
|
audience: "developer",
|
|
5175
5510
|
auth: "developer",
|
|
5176
5511
|
rateLimited: false,
|
|
5177
5512
|
ownerTier: false,
|
|
5178
|
-
status:
|
|
5513
|
+
status: 204,
|
|
5179
5514
|
params: [],
|
|
5180
|
-
query:
|
|
5515
|
+
query: null,
|
|
5181
5516
|
request: null,
|
|
5182
|
-
response:
|
|
5183
|
-
errors: [...DEVELOPER_GUARD, "
|
|
5517
|
+
response: null,
|
|
5518
|
+
errors: [...DEVELOPER_GUARD, "not_found", "target_state_conflict"],
|
|
5184
5519
|
transport: "http",
|
|
5185
|
-
notes: "`
|
|
5520
|
+
notes: "`404 not_found` when there is no authenticator. `409 target_state_conflict` names `two_factor` with rule `required_by_org` when it is the caller's last second factor and the organisation requires one. Audited as `developer.two_factor_removed` with `details.kind` `authenticator`."
|
|
5186
5521
|
},
|
|
5187
5522
|
{
|
|
5188
|
-
method: "
|
|
5189
|
-
path: "/api/
|
|
5190
|
-
section: "
|
|
5191
|
-
summary: "
|
|
5523
|
+
method: "POST",
|
|
5524
|
+
path: "/api/auth/recovery-codes",
|
|
5525
|
+
section: "developer-auth",
|
|
5526
|
+
summary: "Issues ten new recovery codes and voids the old ones.",
|
|
5192
5527
|
audience: "developer",
|
|
5193
5528
|
auth: "developer",
|
|
5194
5529
|
rateLimited: false,
|
|
5195
5530
|
ownerTier: false,
|
|
5196
5531
|
status: 200,
|
|
5197
5532
|
params: [],
|
|
5198
|
-
query:
|
|
5533
|
+
query: null,
|
|
5199
5534
|
request: null,
|
|
5200
|
-
response:
|
|
5201
|
-
|
|
5202
|
-
errors: [...DEVELOPER_GUARD, "validation_error"],
|
|
5535
|
+
response: recoveryCodesResponse,
|
|
5536
|
+
errors: [...DEVELOPER_GUARD, "target_state_conflict"],
|
|
5203
5537
|
transport: "http",
|
|
5204
|
-
notes: "
|
|
5538
|
+
notes: "The codes are shown this once. `409 target_state_conflict` names `two_factor` with rule `off` when the caller has no second factor: recovery codes only stand in for one. Audited as `developer.recovery_codes_generated`."
|
|
5205
5539
|
},
|
|
5206
|
-
/*
|
|
5540
|
+
/* ------------------------------------------- client auth (portal pages) */
|
|
5207
5541
|
{
|
|
5208
|
-
method: "
|
|
5542
|
+
method: "GET",
|
|
5543
|
+
path: "/favicon.svg",
|
|
5544
|
+
section: "client-auth",
|
|
5545
|
+
summary: "Serves the Fleetless icon for the auth portal's and the MCP welcome page's browser tab.",
|
|
5546
|
+
audience: "internal",
|
|
5547
|
+
auth: "none",
|
|
5548
|
+
rateLimited: false,
|
|
5549
|
+
ownerTier: false,
|
|
5550
|
+
status: 200,
|
|
5551
|
+
params: [],
|
|
5552
|
+
query: null,
|
|
5553
|
+
request: null,
|
|
5554
|
+
response: null,
|
|
5555
|
+
errors: [],
|
|
5556
|
+
transport: "http",
|
|
5557
|
+
notes: "An SVG, not JSON. Those pages carry a Content-Security-Policy that admits no `data:` image, so the icon is a file on their own origin \u2014 the one source `img-src 'self'` names. Cached for a day: the bytes change when the brand does, not per deploy."
|
|
5558
|
+
},
|
|
5559
|
+
{
|
|
5560
|
+
method: "POST",
|
|
5561
|
+
path: "/api/waitlist",
|
|
5562
|
+
section: "developer-auth",
|
|
5563
|
+
summary: "Adds an address to the closed-beta waiting list.",
|
|
5564
|
+
audience: "developer",
|
|
5565
|
+
auth: "none",
|
|
5566
|
+
rateLimited: true,
|
|
5567
|
+
ownerTier: false,
|
|
5568
|
+
status: 202,
|
|
5569
|
+
params: [],
|
|
5570
|
+
query: null,
|
|
5571
|
+
request: waitlistRequest,
|
|
5572
|
+
response: null,
|
|
5573
|
+
errors: ["rate_limited", "validation_error"],
|
|
5574
|
+
transport: "http",
|
|
5575
|
+
notes: "Answers `202` whether or not the address was already listed: the landing page's form must not be an oracle for who signed up. The operator notification is detached from the response \u2014 awaiting it made latency answer the question the status code refuses to \u2014 and is capped by its own global ceiling, above which the row is still written and the mail is skipped."
|
|
5576
|
+
},
|
|
5577
|
+
/* ---------------------------------------------------------------- org */
|
|
5578
|
+
{
|
|
5579
|
+
method: "GET",
|
|
5580
|
+
path: "/api/audit",
|
|
5581
|
+
section: "org",
|
|
5582
|
+
summary: "Reads the org's audit log, newest first, cursor-paged over the durable sequence number.",
|
|
5583
|
+
audience: "developer",
|
|
5584
|
+
auth: "developer",
|
|
5585
|
+
rateLimited: false,
|
|
5586
|
+
ownerTier: false,
|
|
5587
|
+
status: 200,
|
|
5588
|
+
params: [],
|
|
5589
|
+
query: auditQuery,
|
|
5590
|
+
request: null,
|
|
5591
|
+
response: auditListResponse,
|
|
5592
|
+
errors: [...DEVELOPER_GUARD, "validation_error"],
|
|
5593
|
+
transport: "http",
|
|
5594
|
+
notes: "`action` and `action_prefix` are mutually exclusive, a cross-field rule no JSON Schema can express \u2014 this route is where it is enforced. Nothing redacts an event's `details`: it is returned exactly as the call site wrote it."
|
|
5595
|
+
},
|
|
5596
|
+
{
|
|
5597
|
+
method: "GET",
|
|
5598
|
+
path: "/api/audit/export",
|
|
5599
|
+
section: "org",
|
|
5600
|
+
summary: "Downloads every audit event matching the same filters as a CSV attachment.",
|
|
5601
|
+
audience: "developer",
|
|
5602
|
+
auth: "developer",
|
|
5603
|
+
rateLimited: false,
|
|
5604
|
+
ownerTier: false,
|
|
5605
|
+
status: 200,
|
|
5606
|
+
params: [],
|
|
5607
|
+
query: auditQuery,
|
|
5608
|
+
request: null,
|
|
5609
|
+
response: null,
|
|
5610
|
+
contentType: "text/csv",
|
|
5611
|
+
errors: [...DEVELOPER_GUARD, "validation_error"],
|
|
5612
|
+
transport: "http",
|
|
5613
|
+
notes: "Answers `text/csv; charset=utf-8` with a `Content-Disposition` attachment, not JSON \u2014 so it has no response schema. `AUDIT_CSV_COLUMNS` names the columns and their order. Takes the same filters as `GET /api/audit` but refuses `before_seq` and `limit` with `400 validation_error`: an export is not a page, it is everything the filter matches up to a fixed row ceiling."
|
|
5614
|
+
},
|
|
5615
|
+
/* --------------------------------------------------------------- apps */
|
|
5616
|
+
{
|
|
5617
|
+
method: "POST",
|
|
5209
5618
|
path: "/api/apps",
|
|
5210
5619
|
section: "apps",
|
|
5211
5620
|
summary: "Creates an app, optionally attaching robots to it at the same time.",
|
|
@@ -5328,7 +5737,7 @@ var ROUTES = [
|
|
|
5328
5737
|
response: role,
|
|
5329
5738
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error"],
|
|
5330
5739
|
transport: "http",
|
|
5331
|
-
notes: 'The body is `{ "name": string }` \u2014 non-empty, trimmed, at most
|
|
5740
|
+
notes: 'The body is `{ "name": string }` \u2014 non-empty, trimmed, at most 60 characters as on `role.name` \u2014 and is deliberately not a contract shape: contracts define the `role` this answers with, not this one trivial request. **The answer is a bare `role`, not an envelope**, unlike the listing beside it.'
|
|
5332
5741
|
},
|
|
5333
5742
|
{
|
|
5334
5743
|
method: "GET",
|
|
@@ -5410,6 +5819,48 @@ var ROUTES = [
|
|
|
5410
5819
|
transport: "http",
|
|
5411
5820
|
notes: "Built by the same builder the MCP server's own `robot_describe` uses, so the two cannot drift. It answers what the role *would* be offered and consults nothing about any user's actual MCP entitlement. A robot the role grants nothing on still appears, with an empty `exposures` \u2014 dropping it would read as \"not attached\", which is a different fact."
|
|
5412
5821
|
},
|
|
5822
|
+
{
|
|
5823
|
+
method: "PATCH",
|
|
5824
|
+
path: "/api/apps/:id/roles/:roleId",
|
|
5825
|
+
section: "apps",
|
|
5826
|
+
summary: "Renames a role; its users keep it.",
|
|
5827
|
+
audience: "developer",
|
|
5828
|
+
auth: "developer",
|
|
5829
|
+
rateLimited: false,
|
|
5830
|
+
ownerTier: false,
|
|
5831
|
+
status: 200,
|
|
5832
|
+
params: [
|
|
5833
|
+
{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." },
|
|
5834
|
+
{ name: "roleId", description: "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`." }
|
|
5835
|
+
],
|
|
5836
|
+
query: null,
|
|
5837
|
+
request: roleRenameRequest,
|
|
5838
|
+
response: role,
|
|
5839
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error", "role_name_taken"],
|
|
5840
|
+
transport: "http",
|
|
5841
|
+
notes: "Names are unique per app, compared exactly as stored after trimming. Built-in roles can be renamed."
|
|
5842
|
+
},
|
|
5843
|
+
{
|
|
5844
|
+
method: "DELETE",
|
|
5845
|
+
path: "/api/apps/:id/roles/:roleId",
|
|
5846
|
+
section: "apps",
|
|
5847
|
+
summary: "Deletes a role, moving its users, pending invitations and default-role status to another role.",
|
|
5848
|
+
audience: "developer",
|
|
5849
|
+
auth: "developer",
|
|
5850
|
+
rateLimited: false,
|
|
5851
|
+
ownerTier: false,
|
|
5852
|
+
status: 204,
|
|
5853
|
+
params: [
|
|
5854
|
+
{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." },
|
|
5855
|
+
{ name: "roleId", description: "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`." }
|
|
5856
|
+
],
|
|
5857
|
+
query: roleDeleteQuery,
|
|
5858
|
+
request: null,
|
|
5859
|
+
response: null,
|
|
5860
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error", "role_in_use", "last_role"],
|
|
5861
|
+
transport: "http",
|
|
5862
|
+
notes: "Without `move_to`, a role that app users or pending invitations hold, or that is the app's default, answers `409 role_in_use` with `{ users, invitations, is_default }`. With `move_to` \u2014 another role of the same app, else `400 validation_error` \u2014 one transaction moves `app_users.role_id`, pending invitations and `default_role_id`, then deletes the role and its permissions. The app's only role answers `409 last_role`. Built-in roles can be deleted like any other."
|
|
5863
|
+
},
|
|
5413
5864
|
{
|
|
5414
5865
|
method: "POST",
|
|
5415
5866
|
path: "/api/apps/:id/server-keys",
|
|
@@ -5593,9 +6044,27 @@ var ROUTES = [
|
|
|
5593
6044
|
query: null,
|
|
5594
6045
|
request: null,
|
|
5595
6046
|
response: mailOutcome,
|
|
5596
|
-
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict"],
|
|
6047
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "target_state_conflict", "method_not_allowed"],
|
|
6048
|
+
transport: "http",
|
|
6049
|
+
notes: "The support door beside `POST /api/client/password/reset`, refused like it with `403 method_not_allowed` while the app has the password method off: the same one-hour token and the same link, triggered by a developer for a user who asked them rather than the form. **No enumeration discipline applies** \u2014 the caller is authenticated into the app and can read the user list \u2014 so this one answers what actually happened: `{ \"mail\": mailStatus }`, where `not_configured` is a deployment without a mailer and `failed` is the state worth somebody's attention. The link points at the app's `reset_url`, or at the hosted reset page when the app has configured none. `409 target_state_conflict` names `password` with rule `not_set` for an account that has none \u2014 an OIDC-only app user, whom a reset link would hand a second, quieter door \u2014 and `status` with rule `blocked` for a blocked one, since `POST /api/client/password/reset` mails a blocked account nothing and the two doors may not disagree. Setting the password directly is deliberately not offered; a developer who could would hold their customers' credentials."
|
|
6050
|
+
},
|
|
6051
|
+
{
|
|
6052
|
+
method: "DELETE",
|
|
6053
|
+
path: "/api/apps/:id/users/:userId/two-factor",
|
|
6054
|
+
section: "apps",
|
|
6055
|
+
summary: "Removes an app user's authenticator and recovery codes and ends every session they hold.",
|
|
6056
|
+
audience: "developer",
|
|
6057
|
+
auth: "developer",
|
|
6058
|
+
rateLimited: false,
|
|
6059
|
+
ownerTier: false,
|
|
6060
|
+
status: 204,
|
|
6061
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "userId", description: "The app user's uuid, from `GET /api/apps/:id/users`; a user of another app answers `404`." }],
|
|
6062
|
+
query: null,
|
|
6063
|
+
request: null,
|
|
6064
|
+
response: null,
|
|
6065
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
5597
6066
|
transport: "http",
|
|
5598
|
-
notes: "The support door
|
|
6067
|
+
notes: "The support door for a person who lost their authenticator and their recovery codes. The authenticator and every recovery code go, and so does every session of the account \u2014 whoever held one may be the reason for the reset. **A user with no second factor answers `204` too**: that is the end state being asked for. When the app requires two-factor, the person sets it up again at their next sign-in, before any session exists. Audited as `app_user.two_factor_reset`."
|
|
5599
6068
|
},
|
|
5600
6069
|
/* ---------------------------- the MCP clients one app user has connected */
|
|
5601
6070
|
{
|
|
@@ -5672,7 +6141,7 @@ var ROUTES = [
|
|
|
5672
6141
|
response: appInvitation,
|
|
5673
6142
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "email_taken", "target_state_conflict", "rate_limited"],
|
|
5674
6143
|
transport: "http",
|
|
5675
|
-
notes: "**An app user, not a team member.** `POST /api/org/invitations` is the other space and leads to the console; this link leads into the developer's own app. The role is resolved and stored now, so a later change to `default_role_id` does not re-aim a link already sent. An invitation **always bypasses `allowed_domains`**. \n\nThe answer carries `accept_url
|
|
6144
|
+
notes: "**An app user, not a team member.** `POST /api/org/invitations` is the other space and leads to the console; this link leads into the developer's own app. The role is resolved and stored now, so a later change to `default_role_id` does not re-aim a link already sent. An invitation **always bypasses `allowed_domains`**. \n\nThe answer carries `accept_url`: the app's `invite_url` with the token in it, or the Fleetless-hosted invitation page when the app has configured none \u2014 so mailing it is never refused for a missing URL. `409 target_state_conflict` names `default_role_id` when `role_id` is absent and the app has no default role, or its default no longer resolves: an invitation that names no role has nothing to hand its acceptor, so it is refused here rather than at the acceptance a week later. `409 email_taken` is an address the app already has as a user; `404 not_found` is the app or a `role_id` that is not one of its roles. \n\nCreating shares the reissue route's ceiling of **five invitation mails a minute per app**, answering `429 rate_limited` with `retry_after_ms`: re-creating an invitation for one address replaces it and mails again, so a limit that bound only reissue would be a limit on the wrong door."
|
|
5676
6145
|
},
|
|
5677
6146
|
{
|
|
5678
6147
|
method: "POST",
|
|
@@ -5806,7 +6275,7 @@ var ROUTES = [
|
|
|
5806
6275
|
method: "GET",
|
|
5807
6276
|
path: "/api/apps/:id/auth-config",
|
|
5808
6277
|
section: "apps",
|
|
5809
|
-
summary: "Reads the app's auth settings:
|
|
6278
|
+
summary: "Reads the app's auth settings: sign-in methods, two-factor, registration, pages, the hosted look and the MCP switch.",
|
|
5810
6279
|
audience: "developer",
|
|
5811
6280
|
auth: "developer",
|
|
5812
6281
|
rateLimited: false,
|
|
@@ -5818,7 +6287,7 @@ var ROUTES = [
|
|
|
5818
6287
|
response: appAuthConfig,
|
|
5819
6288
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
5820
6289
|
transport: "http",
|
|
5821
|
-
notes: "One row per app, created with the app and never absent \u2014 an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a client-supplied value would be a lie about when the row last changed."
|
|
6290
|
+
notes: "One row per app, created with the app and never absent \u2014 an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a client-supplied value would be a lie about when the row last changed. `hosted_pages` and `hosted_logo_url` are read-only as well: the cloud mints both from the auth portal's base URL and the app's identifier."
|
|
5822
6291
|
},
|
|
5823
6292
|
{
|
|
5824
6293
|
method: "PUT",
|
|
@@ -5836,13 +6305,31 @@ var ROUTES = [
|
|
|
5836
6305
|
response: appAuthConfig,
|
|
5837
6306
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5838
6307
|
transport: "http",
|
|
5839
|
-
notes: "**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's \u2014 see `GET`'s notes for why. \n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. \n\nThe merge is server-side against the stored row, so this write never disturbs
|
|
6308
|
+
notes: "**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's \u2014 see `GET`'s notes for why. \n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. \n\nThe merge is server-side against the stored row, so this write never disturbs another slice."
|
|
6309
|
+
},
|
|
6310
|
+
{
|
|
6311
|
+
method: "PUT",
|
|
6312
|
+
path: "/api/apps/:id/auth-config/sign-in",
|
|
6313
|
+
section: "apps",
|
|
6314
|
+
summary: "Replaces how the app's users sign in and whether they give a second factor.",
|
|
6315
|
+
audience: "developer",
|
|
6316
|
+
auth: "developer",
|
|
6317
|
+
rateLimited: false,
|
|
6318
|
+
ownerTier: false,
|
|
6319
|
+
status: 200,
|
|
6320
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6321
|
+
query: null,
|
|
6322
|
+
request: putAppAuthSignInRequest,
|
|
6323
|
+
response: appAuthConfig,
|
|
6324
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
6325
|
+
transport: "http",
|
|
6326
|
+
notes: "**A replace, not a merge, and `.strict()`**: `sign_in_methods` and `two_factor` both arrive or the write is refused. Both methods off is `400 validation_error` naming `sign_in_methods.password` \u2014 an app needs at least one door besides its identity providers. \n\nTurning a method off refuses its routes with `method_not_allowed` from the next request on; a stored password stays stored. Setting `two_factor` to `required` signs nobody out: each person without an authenticator sets one up at their next sign-in, before any session exists. Audited with both old and new values. The merge is server-side against the stored row, so this write never disturbs another slice."
|
|
5840
6327
|
},
|
|
5841
6328
|
{
|
|
5842
6329
|
method: "PUT",
|
|
5843
6330
|
path: "/api/apps/:id/auth-config/urls",
|
|
5844
6331
|
section: "apps",
|
|
5845
|
-
summary: "Replaces the
|
|
6332
|
+
summary: "Replaces the app's home page and the four pages Fleetless's mails and MCP sign-in point at.",
|
|
5846
6333
|
audience: "developer",
|
|
5847
6334
|
auth: "developer",
|
|
5848
6335
|
rateLimited: false,
|
|
@@ -5854,13 +6341,13 @@ var ROUTES = [
|
|
|
5854
6341
|
response: appAuthConfig,
|
|
5855
6342
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5856
6343
|
transport: "http",
|
|
5857
|
-
notes: "**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `
|
|
6344
|
+
notes: "**A replace, not a merge, and `.strict()`**: `app_url`, `invite_url`, `verify_url`, `reset_url` and `mcp_login_url` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's \u2014 see `GET`'s notes for why. \n\nEach may be `null`, and then the Fleetless-hosted page in `hosted_pages` stands in for it: nothing is refused for a missing URL. `400 validation_error` is where the field rules land: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once \u2014 a second occurrence leaves one literal in a mailed link, refused here rather than failing silently once the mail is sent \u2014 and `app_url` takes the same host rule with no placeholder. `mcp_login_url` moved here from the `mcp` slice, because one screen owns all four pages. \n\nThe merge is server-side against the stored row, so this write never disturbs another slice."
|
|
5858
6345
|
},
|
|
5859
6346
|
{
|
|
5860
6347
|
method: "PUT",
|
|
5861
6348
|
path: "/api/apps/:id/auth-config/mcp",
|
|
5862
6349
|
section: "apps",
|
|
5863
|
-
summary: "
|
|
6350
|
+
summary: "Turns the app's MCP endpoint on or off.",
|
|
5864
6351
|
audience: "developer",
|
|
5865
6352
|
auth: "developer",
|
|
5866
6353
|
rateLimited: false,
|
|
@@ -5872,7 +6359,61 @@ var ROUTES = [
|
|
|
5872
6359
|
response: appAuthConfig,
|
|
5873
6360
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
5874
6361
|
transport: "http",
|
|
5875
|
-
notes: "**A replace, not a merge, and `.strict()`**: `mcp_enabled`
|
|
6362
|
+
notes: "**A replace, not a merge, and `.strict()`**: `mcp_enabled` arrives or the write is refused. It used to take `mcp_login_url` as well, because on without a URL refused every sign-in; the hosted MCP sign-in now stands in for an unset URL, and the URL moved to the `urls` slice. A body still carrying it is `400 validation_error`. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's. The merge is server-side against the stored row, so this write never disturbs another slice."
|
|
6363
|
+
},
|
|
6364
|
+
{
|
|
6365
|
+
method: "PUT",
|
|
6366
|
+
path: "/api/apps/:id/auth-config/look",
|
|
6367
|
+
section: "apps",
|
|
6368
|
+
summary: "Replaces the hosted pages' accent colour.",
|
|
6369
|
+
audience: "developer",
|
|
6370
|
+
auth: "developer",
|
|
6371
|
+
rateLimited: false,
|
|
6372
|
+
ownerTier: false,
|
|
6373
|
+
status: 200,
|
|
6374
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6375
|
+
query: null,
|
|
6376
|
+
request: putAppAuthLookRequest,
|
|
6377
|
+
response: appAuthConfig,
|
|
6378
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found"],
|
|
6379
|
+
transport: "http",
|
|
6380
|
+
notes: "**A replace, and `.strict()`**: `hosted_accent` arrives, `#rrggbb` in lowercase, or `null` for the neutral shell's own accent. The logo is its own write, `PUT /api/apps/:id/auth-config/logo`, because it is an image rather than a field. The merge is server-side against the stored row, so this write never disturbs another slice."
|
|
6381
|
+
},
|
|
6382
|
+
{
|
|
6383
|
+
method: "PUT",
|
|
6384
|
+
path: "/api/apps/:id/auth-config/logo",
|
|
6385
|
+
section: "apps",
|
|
6386
|
+
summary: "Stores the logo the hosted pages show above the app's name.",
|
|
6387
|
+
audience: "developer",
|
|
6388
|
+
auth: "developer",
|
|
6389
|
+
rateLimited: false,
|
|
6390
|
+
ownerTier: false,
|
|
6391
|
+
status: 200,
|
|
6392
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6393
|
+
query: null,
|
|
6394
|
+
request: null,
|
|
6395
|
+
response: appAuthConfig,
|
|
6396
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "unsupported_media_type"],
|
|
6397
|
+
transport: "http",
|
|
6398
|
+
notes: "The body is the **raw image**, not JSON, so it has no request schema: `Content-Type` is one of `HOSTED_LOGO_TYPES` (`image/png`, `image/svg+xml`) and anything else is `415 unsupported_media_type`. At most `HOSTED_LOGO_MAX_BYTES` (100 KB); a larger body, or one that is not the image its type names, is `400 validation_error`. A new logo replaces the stored one. The hosted pages load it from `hosted_logo_url` as an image only, and the cloud serves an SVG sandboxed, so a script inside one never runs."
|
|
6399
|
+
},
|
|
6400
|
+
{
|
|
6401
|
+
method: "DELETE",
|
|
6402
|
+
path: "/api/apps/:id/auth-config/logo",
|
|
6403
|
+
section: "apps",
|
|
6404
|
+
summary: "Removes the logo from the hosted pages.",
|
|
6405
|
+
audience: "developer",
|
|
6406
|
+
auth: "developer",
|
|
6407
|
+
rateLimited: false,
|
|
6408
|
+
ownerTier: false,
|
|
6409
|
+
status: 200,
|
|
6410
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }],
|
|
6411
|
+
query: null,
|
|
6412
|
+
request: null,
|
|
6413
|
+
response: appAuthConfig,
|
|
6414
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
6415
|
+
transport: "http",
|
|
6416
|
+
notes: "Answers the whole configuration, with `hosted_logo_url` now `null`; the hosted pages show the app's name alone. An app with no logo answers the same: that is the end state being asked for."
|
|
5876
6417
|
},
|
|
5877
6418
|
{
|
|
5878
6419
|
method: "GET",
|
|
@@ -5890,7 +6431,7 @@ var ROUTES = [
|
|
|
5890
6431
|
response: appMailTemplateListResponse,
|
|
5891
6432
|
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
|
|
5892
6433
|
transport: "http",
|
|
5893
|
-
notes: 'Answers `{ "templates": [appMailTemplate, \u2026] }` with **only the kinds that have a custom template** \u2014 at most
|
|
6434
|
+
notes: 'Answers `{ "templates": [appMailTemplate, \u2026] }` with **only the kinds that have a custom template** \u2014 at most four. A kind that does not appear is one using the Fleetless default text, which is an ordinary state and not a missing row. Mails to *Fleetless* users, a team invitation or a console sign-in code, are not in this list and are deliberately not customisable: they are about this platform, not about the developer\'s product.'
|
|
5894
6435
|
},
|
|
5895
6436
|
{
|
|
5896
6437
|
method: "GET",
|
|
@@ -5902,7 +6443,7 @@ var ROUTES = [
|
|
|
5902
6443
|
rateLimited: false,
|
|
5903
6444
|
ownerTier: false,
|
|
5904
6445
|
status: 200,
|
|
5905
|
-
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the
|
|
6446
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the four mails this template replaces \u2014 a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`." }],
|
|
5906
6447
|
query: null,
|
|
5907
6448
|
request: null,
|
|
5908
6449
|
response: appMailTemplate,
|
|
@@ -5920,7 +6461,7 @@ var ROUTES = [
|
|
|
5920
6461
|
rateLimited: false,
|
|
5921
6462
|
ownerTier: false,
|
|
5922
6463
|
status: 200,
|
|
5923
|
-
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the
|
|
6464
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the four mails this template replaces \u2014 a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`." }],
|
|
5924
6465
|
query: null,
|
|
5925
6466
|
request: putAppMailTemplateRequest,
|
|
5926
6467
|
response: appMailTemplate,
|
|
@@ -5938,7 +6479,7 @@ var ROUTES = [
|
|
|
5938
6479
|
rateLimited: false,
|
|
5939
6480
|
ownerTier: false,
|
|
5940
6481
|
status: 204,
|
|
5941
|
-
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the
|
|
6482
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the four mails this template replaces \u2014 a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`." }],
|
|
5942
6483
|
query: null,
|
|
5943
6484
|
request: null,
|
|
5944
6485
|
response: null,
|
|
@@ -5956,7 +6497,7 @@ var ROUTES = [
|
|
|
5956
6497
|
rateLimited: false,
|
|
5957
6498
|
ownerTier: false,
|
|
5958
6499
|
status: 200,
|
|
5959
|
-
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the
|
|
6500
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the four mails this template replaces \u2014 a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`." }],
|
|
5960
6501
|
query: null,
|
|
5961
6502
|
request: mailTemplatePreviewRequest,
|
|
5962
6503
|
response: mailTemplatePreviewResponse,
|
|
@@ -5974,7 +6515,7 @@ var ROUTES = [
|
|
|
5974
6515
|
rateLimited: false,
|
|
5975
6516
|
ownerTier: false,
|
|
5976
6517
|
status: 202,
|
|
5977
|
-
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the
|
|
6518
|
+
params: [{ name: "id", description: "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`." }, { name: "kind", description: "Which of the four mails this template replaces \u2014 a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`." }],
|
|
5978
6519
|
query: null,
|
|
5979
6520
|
request: mailTemplatePreviewRequest,
|
|
5980
6521
|
response: mailOutcome,
|
|
@@ -6094,7 +6635,7 @@ var ROUTES = [
|
|
|
6094
6635
|
method: "POST",
|
|
6095
6636
|
path: "/api/org/invitations/accept",
|
|
6096
6637
|
section: "users",
|
|
6097
|
-
summary: "Spends an invitation token and creates the
|
|
6638
|
+
summary: "Spends an invitation token and creates the account it was addressed to.",
|
|
6098
6639
|
audience: "developer",
|
|
6099
6640
|
auth: "none",
|
|
6100
6641
|
rateLimited: true,
|
|
@@ -6106,7 +6647,7 @@ var ROUTES = [
|
|
|
6106
6647
|
response: null,
|
|
6107
6648
|
errors: ["rate_limited", "validation_error", "token_spent", "email_taken"],
|
|
6108
6649
|
transport: "http",
|
|
6109
|
-
notes: '**`204`, not a session.** The console signs in through its own OAuth portal, so a session minted here would be a second credential door for one account \u2014 and every security property would then have to be right in two places. Unknown, expired and already-accepted tokens collapse into `410 token_spent`. A browser form post gets the rendered "you\'re in" page instead.'
|
|
6650
|
+
notes: '**`204`, not a session.** The console signs in through its own OAuth portal, so a session minted here would be a second credential door for one account \u2014 and every security property would then have to be right in two places. **No password**: the mailed link proves the address, so accepting needs no code either, and the new member signs in by emailed code from then on. When the organisation requires two-factor, the member sets one up at their first sign-in. Unknown, expired and already-accepted tokens collapse into `410 token_spent`. A browser form post gets the rendered "you\'re in" page instead.'
|
|
6110
6651
|
},
|
|
6111
6652
|
{
|
|
6112
6653
|
method: "GET",
|
|
@@ -6124,7 +6665,7 @@ var ROUTES = [
|
|
|
6124
6665
|
response: null,
|
|
6125
6666
|
errors: [],
|
|
6126
6667
|
transport: "http",
|
|
6127
|
-
notes: 'HTML, served by the cloud from the auth portal origin; the form on it posts to `POST /api/org/invitations/accept`. An unknown, spent or expired token renders the "link no longer valid" page at `410`, which
|
|
6668
|
+
notes: 'HTML, served by the cloud from the auth portal origin; the form on it posts to `POST /api/org/invitations/accept`. **The GET spends nothing** \u2014 a mail scanner opening the link must not accept the invitation \u2014 only the form\'s POST does. An unknown, spent or expired token renders the "link no longer valid" page at `410`, which says to ask the organisation for a new invitation: the person holding a dead link cannot re-issue it.'
|
|
6128
6669
|
},
|
|
6129
6670
|
{
|
|
6130
6671
|
method: "PATCH",
|
|
@@ -6180,11 +6721,29 @@ var ROUTES = [
|
|
|
6180
6721
|
transport: "http",
|
|
6181
6722
|
notes: "Owner tier, unconditionally \u2014 this is the route the whole owner-exclusive list is about. A uuid that is not a Fleetless user of this org answers `404 not_found`, the same as one that does not exist anywhere: the `409 target_state_conflict` documented here until the two-space cut had exactly one producer, the Org Admins membership check, and went with it. Demoting the last Owner is `409 last_owner`, decided by a row lock inside the writing transaction rather than by a read beforehand. Setting the tier already held changes nothing and writes no audit event. No session is revoked: a tier is re-read from the row on every request, so no issued token carries a stale copy of it."
|
|
6182
6723
|
},
|
|
6724
|
+
{
|
|
6725
|
+
method: "DELETE",
|
|
6726
|
+
path: "/api/org/users/:id/two-factor",
|
|
6727
|
+
section: "users",
|
|
6728
|
+
summary: "Removes a team member's passkeys, authenticator and recovery codes and ends their sessions.",
|
|
6729
|
+
audience: "developer",
|
|
6730
|
+
auth: "developer",
|
|
6731
|
+
rateLimited: false,
|
|
6732
|
+
ownerTier: true,
|
|
6733
|
+
status: 204,
|
|
6734
|
+
params: [{ name: "id", description: "The Fleetless user's uuid, as listed by `GET /api/org/users`." }],
|
|
6735
|
+
query: null,
|
|
6736
|
+
request: null,
|
|
6737
|
+
response: null,
|
|
6738
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "invalid_uuid", "not_found", "target_state_conflict"],
|
|
6739
|
+
transport: "http",
|
|
6740
|
+
notes: "Owner tier: the door for a member who lost every second factor and every recovery code. Every session of the member ends, and when the organisation requires two-factor they set one up again at their next sign-in. **An owner cannot reset their own** \u2014 `409 target_state_conflict` naming `user_id` with rule `self`; Settings \u203A Profile is where they change it. A member with no second factor answers `204` too. Audited as `developer.two_factor_reset`, naming the owner who did it."
|
|
6741
|
+
},
|
|
6183
6742
|
{
|
|
6184
6743
|
method: "PATCH",
|
|
6185
6744
|
path: "/api/org",
|
|
6186
6745
|
section: "org",
|
|
6187
|
-
summary: "Renames the org.",
|
|
6746
|
+
summary: "Renames the org, requires two-factor for its members, or both.",
|
|
6188
6747
|
audience: "developer",
|
|
6189
6748
|
auth: "developer",
|
|
6190
6749
|
rateLimited: false,
|
|
@@ -6196,7 +6755,7 @@ var ROUTES = [
|
|
|
6196
6755
|
response: patchOrgResponse,
|
|
6197
6756
|
errors: [...DEVELOPER_GUARD, "tier_required", "validation_error"],
|
|
6198
6757
|
transport: "http",
|
|
6199
|
-
notes: 'Answers `{ "org": org }`. Owner tier, and the gate runs before the body is looked at, so a malformed
|
|
6758
|
+
notes: 'Answers `{ "org": org }`. Owner tier, and the gate runs before the body is looked at, so a malformed patch and a forbidden one answer the same way \u2014 `403 tier_required` for a developer, whichever field they sent. An empty body is `400 validation_error`. Writing the values already held writes nothing and records no audit event. \n\n`require_two_factor` on signs nobody out: each member without a passkey or authenticator sets one up at their next sign-in, before any session exists, on the console and the central MCP endpoint alike. Server keys and robot bridges are not people and are not affected. Audited as `org.two_factor_required_changed`.'
|
|
6200
6759
|
},
|
|
6201
6760
|
/* --------------------------------------------------------------- mcp */
|
|
6202
6761
|
{
|
|
@@ -6269,7 +6828,7 @@ var ROUTES = [
|
|
|
6269
6828
|
response: null,
|
|
6270
6829
|
errors: [],
|
|
6271
6830
|
transport: "http",
|
|
6272
|
-
notes: "**The query schema is what this endpoint accepts, not what it parses**: the handler reads it parameter by parameter because the answers differ, and one parse would collapse them. Client and `redirect_uri` are validated first and a failure there never redirects, the same open-redirect discipline the app flow applies; those refusals are `oauthError`. Exact `redirect_uri` matching for both client kinds \u2014 the loopback-port wildcard of RFC 8252 \xA77.3 belongs to the one central client alone, whose URIs are configured ahead of time and cannot name an ephemeral port. A client that registered itself seconds ago can name the port it bound, and widening the wildcard there would only widen where a stolen `client_id` may send a browser. Nothing about the person is decided here \u2014 the next card asks for an email address and the
|
|
6831
|
+
notes: "**The query schema is what this endpoint accepts, not what it parses**: the handler reads it parameter by parameter because the answers differ, and one parse would collapse them. Client and `redirect_uri` are validated first and a failure there never redirects, the same open-redirect discipline the app flow applies; those refusals are `oauthError`. Exact `redirect_uri` matching for both client kinds \u2014 the loopback-port wildcard of RFC 8252 \xA77.3 belongs to the one central client alone, whose URIs are configured ahead of time and cannot name an ephemeral port. A client that registered itself seconds ago can name the port it bound, and widening the wildcard there would only widen where a stolen `client_id` may send a browser. Nothing about the person is decided here \u2014 the next card asks for an email address, or a passkey, and the steps after it resolve the account; this route knows only the client."
|
|
6273
6832
|
},
|
|
6274
6833
|
{
|
|
6275
6834
|
method: "GET",
|
|
@@ -6287,44 +6846,9 @@ var ROUTES = [
|
|
|
6287
6846
|
response: null,
|
|
6288
6847
|
errors: [],
|
|
6289
6848
|
transport: "http",
|
|
6290
|
-
notes: "HTML, and a GET rather than the body of the authorize response \u2014 so it is reloadable, bookmarkable and survives a back button, which the inline page it replaced was not. An expired, consumed, unknown or hand-edited interaction renders one page at `410`, and so does a client whose dynamic registration lapsed in between."
|
|
6291
|
-
},
|
|
6292
|
-
{
|
|
6293
|
-
method: "POST",
|
|
6294
|
-
path: "/mcp/oauth/identify",
|
|
6295
|
-
section: "mcp",
|
|
6296
|
-
summary: "Takes the email address and hands back the password step.",
|
|
6297
|
-
audience: "internal",
|
|
6298
|
-
auth: "none",
|
|
6299
|
-
rateLimited: true,
|
|
6300
|
-
ownerTier: false,
|
|
6301
|
-
status: 200,
|
|
6302
|
-
params: [],
|
|
6303
|
-
query: null,
|
|
6304
|
-
request: null,
|
|
6305
|
-
response: null,
|
|
6306
|
-
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
6307
|
-
transport: "http",
|
|
6308
|
-
notes: 'The identifier-first step, with nothing left to identify: Fleetless users are password-only, so **this step does not read the address at all** \u2014 it renders the password card for a known address, an unknown one and an empty one alike, and the login step below answers the same `401` for all three. That is a property of the shape rather than of two branches agreeing: there is no lookup here whose result could differ. A browser form post gets the password card; a JSON caller gets `{ "next" }`, which has no schema. Still rate limited per (route, ip, email), because it is an unauthenticated endpoint that renders a page.'
|
|
6309
|
-
},
|
|
6310
|
-
{
|
|
6311
|
-
method: "POST",
|
|
6312
|
-
path: "/mcp/oauth/login",
|
|
6313
|
-
section: "mcp",
|
|
6314
|
-
summary: "Checks the password and hands back where the MCP sign-in continues.",
|
|
6315
|
-
audience: "internal",
|
|
6316
|
-
auth: "none",
|
|
6317
|
-
rateLimited: true,
|
|
6318
|
-
ownerTier: false,
|
|
6319
|
-
status: 200,
|
|
6320
|
-
params: [],
|
|
6321
|
-
query: null,
|
|
6322
|
-
request: null,
|
|
6323
|
-
response: oauthRedirectResponse,
|
|
6324
|
-
errors: ["rate_limited", "validation_error", "token_spent", "invalid_credentials"],
|
|
6325
|
-
transport: "http",
|
|
6326
|
-
notes: 'The body is `{ "interaction_id", "email", "password" }`, read field by field rather than through a contract shape. A browser gets a `303` \u2014 to the consent screen for a self-registered client, or straight to the callback for the central one \u2014 where a JSON caller gets this `200` and `redirect_to`.'
|
|
6849
|
+
notes: "HTML, and a GET rather than the body of the authorize response \u2014 so it is reloadable, bookmarkable and survives a back button, which the inline page it replaced was not. An expired, consumed, unknown or hand-edited interaction renders one page at `410`, and so does a client whose dynamic registration lapsed in between. It offers the email field and `Sign in with a passkey`, and opening it binds the interaction to this browser with the browser-proof cookie, which every sign-in step after it checks."
|
|
6327
6850
|
},
|
|
6851
|
+
...developerSignInRoutes("/mcp/oauth"),
|
|
6328
6852
|
{
|
|
6329
6853
|
method: "GET",
|
|
6330
6854
|
path: "/mcp/oauth/consent/:id",
|
|
@@ -6335,7 +6859,7 @@ var ROUTES = [
|
|
|
6335
6859
|
rateLimited: false,
|
|
6336
6860
|
ownerTier: false,
|
|
6337
6861
|
status: 200,
|
|
6338
|
-
params: [{ name: "id", description: "The interaction id from the sign-in; the
|
|
6862
|
+
params: [{ name: "id", description: "The interaction id from the sign-in; the last sign-in step redirects the browser here." }],
|
|
6339
6863
|
query: null,
|
|
6340
6864
|
request: null,
|
|
6341
6865
|
response: null,
|
|
@@ -6414,31 +6938,32 @@ var ROUTES = [
|
|
|
6414
6938
|
response: null,
|
|
6415
6939
|
errors: [],
|
|
6416
6940
|
transport: "http",
|
|
6417
|
-
notes: "HTML. An expired, consumed, unknown or hand-edited interaction renders one page at `410`: which of the four it was is not a fact a stranger may learn, and to the person it is one fact anyway. The page resolves nothing about the address typed into it, so there is no enumeration oracle here at all."
|
|
6941
|
+
notes: "HTML: the email field, `Email me a code`, and `Sign in with a passkey`. Opening it binds the interaction to this browser with the browser-proof cookie, which every sign-in step after it checks. An expired, consumed, unknown or hand-edited interaction renders one page at `410`: which of the four it was is not a fact a stranger may learn, and to the person it is one fact anyway. The page resolves nothing about the address typed into it, so there is no enumeration oracle here at all."
|
|
6418
6942
|
},
|
|
6943
|
+
...developerSignInRoutes("/console/oauth"),
|
|
6419
6944
|
{
|
|
6420
|
-
method: "
|
|
6421
|
-
path: "/console/oauth/
|
|
6945
|
+
method: "GET",
|
|
6946
|
+
path: "/console/oauth/signup/:id",
|
|
6422
6947
|
section: "developer-auth",
|
|
6423
|
-
summary: "
|
|
6948
|
+
summary: "Serves step one of console sign-up, the email card.",
|
|
6424
6949
|
audience: "internal",
|
|
6425
6950
|
auth: "none",
|
|
6426
|
-
rateLimited:
|
|
6951
|
+
rateLimited: false,
|
|
6427
6952
|
ownerTier: false,
|
|
6428
6953
|
status: 200,
|
|
6429
|
-
params: [],
|
|
6954
|
+
params: [{ name: "id", description: "The interaction id minted by `GET /console/oauth/authorize` with `?prompt=create`." }],
|
|
6430
6955
|
query: null,
|
|
6431
6956
|
request: null,
|
|
6432
6957
|
response: null,
|
|
6433
|
-
errors: [
|
|
6958
|
+
errors: [],
|
|
6434
6959
|
transport: "http",
|
|
6435
|
-
notes: '
|
|
6960
|
+
notes: 'HTML: the email field and `Email me a code`, the first of three steps \u2014 email, code, organization. While the deployment runs in closed beta this renders the "sign-up is closed" card at `403` instead, keeping the interaction alive and pointing back at sign-in and the waiting list \u2014 the person may well already have an account.'
|
|
6436
6961
|
},
|
|
6437
6962
|
{
|
|
6438
6963
|
method: "POST",
|
|
6439
|
-
path: "/console/oauth/
|
|
6964
|
+
path: "/console/oauth/signup",
|
|
6440
6965
|
section: "developer-auth",
|
|
6441
|
-
summary: "
|
|
6966
|
+
summary: "Takes the sign-up email, mails a code and hands back the code step.",
|
|
6442
6967
|
audience: "internal",
|
|
6443
6968
|
auth: "none",
|
|
6444
6969
|
rateLimited: true,
|
|
@@ -6447,34 +6972,16 @@ var ROUTES = [
|
|
|
6447
6972
|
params: [],
|
|
6448
6973
|
query: null,
|
|
6449
6974
|
request: null,
|
|
6450
|
-
response: oauthRedirectResponse,
|
|
6451
|
-
errors: ["rate_limited", "token_spent", "invalid_credentials"],
|
|
6452
|
-
transport: "http",
|
|
6453
|
-
notes: 'This is the only place a Fleetless developer password may be typed; `POST /api/auth/login` is gone, because a second credential door means every security property has to be right in two places. A browser form post gets a `303` to the callback URL; a JSON caller gets that same URL as `redirect_to` at `200`. An argon2 verify runs whether or not the address exists, and the Org Admins check runs after it \u2014 filtering first would hand back a faster "no" for a non-admin account, which is a timing oracle. Wrong password, unknown address and "not an org admin" render identical bytes under one `401`.'
|
|
6454
|
-
},
|
|
6455
|
-
{
|
|
6456
|
-
method: "GET",
|
|
6457
|
-
path: "/console/oauth/signup/:id",
|
|
6458
|
-
section: "developer-auth",
|
|
6459
|
-
summary: "Serves step one of console sign-up, the account card.",
|
|
6460
|
-
audience: "internal",
|
|
6461
|
-
auth: "none",
|
|
6462
|
-
rateLimited: false,
|
|
6463
|
-
ownerTier: false,
|
|
6464
|
-
status: 200,
|
|
6465
|
-
params: [{ name: "id", description: "The interaction id minted by `GET /console/oauth/authorize` with `?prompt=create`." }],
|
|
6466
|
-
query: null,
|
|
6467
|
-
request: null,
|
|
6468
6975
|
response: null,
|
|
6469
|
-
errors: [],
|
|
6976
|
+
errors: ["rate_limited", "token_spent", "signup_closed", "validation_error", "email_taken"],
|
|
6470
6977
|
transport: "http",
|
|
6471
|
-
notes: '
|
|
6978
|
+
notes: 'A browser form post gets the code card; a JSON caller gets `{ "next", "email" }`, which has no schema. **No password**: the code mailed here, six digits valid ten minutes, proves the address. A per-interaction proof cookie is set here \u2014 it is what stops a third party from finishing a sign-up somebody else started. Sign-up is the one surface whose job is to say an address is taken, so `409 email_taken` is not a leak here.'
|
|
6472
6979
|
},
|
|
6473
6980
|
{
|
|
6474
6981
|
method: "POST",
|
|
6475
|
-
path: "/console/oauth/signup",
|
|
6982
|
+
path: "/console/oauth/signup/code",
|
|
6476
6983
|
section: "developer-auth",
|
|
6477
|
-
summary: "
|
|
6984
|
+
summary: "Checks the sign-up code and hands back the organization step.",
|
|
6478
6985
|
audience: "internal",
|
|
6479
6986
|
auth: "none",
|
|
6480
6987
|
rateLimited: true,
|
|
@@ -6484,9 +6991,9 @@ var ROUTES = [
|
|
|
6484
6991
|
query: null,
|
|
6485
6992
|
request: null,
|
|
6486
6993
|
response: null,
|
|
6487
|
-
errors: ["rate_limited", "token_spent", "signup_closed", "validation_error", "
|
|
6994
|
+
errors: ["rate_limited", "token_spent", "signup_closed", "wrong_browser", "validation_error", "invalid_code"],
|
|
6488
6995
|
transport: "http",
|
|
6489
|
-
notes: 'A
|
|
6996
|
+
notes: 'A wrong code renders the code card again with the attempts left (`400 invalid_code`); a spent, expired or exhausted one is `410 token_spent`. The step before must have run in **this** browser (`401 wrong_browser`). A browser form post gets the organization card, the address shown `confirmed`; a JSON caller gets `{ "next" }`, which has no schema.'
|
|
6490
6997
|
},
|
|
6491
6998
|
{
|
|
6492
6999
|
method: "POST",
|
|
@@ -6504,7 +7011,7 @@ var ROUTES = [
|
|
|
6504
7011
|
response: oauthRedirectResponse,
|
|
6505
7012
|
errors: ["rate_limited", "token_spent", "signup_closed", "wrong_browser", "validation_error", "email_taken"],
|
|
6506
7013
|
transport: "http",
|
|
6507
|
-
notes: "The
|
|
7014
|
+
notes: "The form carries `org_name` only. The org and its founding Owner are created in one transaction, and only after the code step confirmed the address. Both steps before must have run in **this** browser: a missing or mismatched proof cookie is `401 wrong_browser` and the person is sent back to step one. `409 email_taken` when the address was taken meanwhile. A browser form post gets a `303` to the console callback; a JSON caller gets `redirect_to` at `200`. This is the only way an organisation is created: `POST /api/auth/signup` is gone, because without a password it would hand a session to anybody who names an address."
|
|
6508
7015
|
},
|
|
6509
7016
|
{
|
|
6510
7017
|
method: "POST",
|
|
@@ -6524,6 +7031,8 @@ var ROUTES = [
|
|
|
6524
7031
|
transport: "http",
|
|
6525
7032
|
notes: "Called by the console's own server, never by a browser. Refusals use RFC 6749 \xA75.2's flat `oauthError` shape \u2014 it is a token endpoint, and that is the dialect a caller of one expects \u2014 so it emits none of the codes in this reference. Proof of possession is checked before the replay check, and the single-use consume is atomic, so exactly one caller ever mints. The Org Admins membership is re-read here: the code was minted earlier, and a user moved out in between must not get a console session."
|
|
6526
7033
|
},
|
|
7034
|
+
/* --------------------------------- the Fleetless-hosted app pages */
|
|
7035
|
+
...HOSTED_APP_ROUTES,
|
|
6527
7036
|
/* ---------------------------------------------------- mcp (the endpoint) */
|
|
6528
7037
|
{
|
|
6529
7038
|
method: "GET",
|
|
@@ -6650,7 +7159,7 @@ var ROUTES = [
|
|
|
6650
7159
|
response: authorizationServerMetadata,
|
|
6651
7160
|
errors: ["not_found"],
|
|
6652
7161
|
transport: "http",
|
|
6653
|
-
notes: "RFC 8414, for the issuer `<PUBLIC_API_BASE_URL>/mcp/<identifier>` \u2014 the same path rule as the document above, and the same `404` for a switched-off app. `registration_endpoint` is present for the reason the central document states: a client that finds it registers itself and never asks a person for a `client_id`. \n\n**`issuer`, `token_endpoint` and the resource identifier are minted from the canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request's `Host`**, because a client checks a minted token's `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an auth-portal origin
|
|
7162
|
+
notes: "RFC 8414, for the issuer `<PUBLIC_API_BASE_URL>/mcp/<identifier>` \u2014 the same path rule as the document above, and the same `404` for a switched-off app. `registration_endpoint` is present for the reason the central document states: a client that finds it registers itself and never asks a person for a `client_id`. \n\n**`issuer`, `token_endpoint` and the resource identifier are minted from the canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request's `Host`**, because a client checks a minted token's `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an auth-portal origin**: the authorization step renders no page itself. It redirects to the app's own `mcp_login_url`, or to the hosted MCP sign-in on the auth portal when the app has configured none."
|
|
6654
7163
|
},
|
|
6655
7164
|
{
|
|
6656
7165
|
method: "POST",
|
|
@@ -6674,7 +7183,7 @@ var ROUTES = [
|
|
|
6674
7183
|
method: "GET",
|
|
6675
7184
|
path: MCP_APP.authorize,
|
|
6676
7185
|
section: "mcp",
|
|
6677
|
-
summary: "Starts an MCP sign-in and redirects the browser to the app's own login page.",
|
|
7186
|
+
summary: "Starts an MCP sign-in and redirects the browser to the app's own login page, or to the hosted one.",
|
|
6678
7187
|
audience: "client",
|
|
6679
7188
|
auth: "none",
|
|
6680
7189
|
rateLimited: false,
|
|
@@ -6684,9 +7193,9 @@ var ROUTES = [
|
|
|
6684
7193
|
query: oauthAuthorizeQuery,
|
|
6685
7194
|
request: null,
|
|
6686
7195
|
response: null,
|
|
6687
|
-
errors: ["not_found"
|
|
7196
|
+
errors: ["not_found"],
|
|
6688
7197
|
transport: "http",
|
|
6689
|
-
notes: 'The same query as `GET /mcp/oauth/authorize`, read the same way \u2014 parameter by parameter, because the answers differ and one parse would collapse them. \n\n**
|
|
7198
|
+
notes: 'The same query as `GET /mcp/oauth/authorize`, read the same way \u2014 parameter by parameter, because the answers differ and one parse would collapse them. \n\n**This route renders no page.** It writes an interaction \u2014 ten minutes, as the OIDC ones live \u2014 and redirects to `appAuthConfig.mcp_login_url` with `{interaction}` filled in. The app then authenticates the person with its own UI, reads `GET /api/client/mcp/interactions/:id` to show the client\'s claimed name and the scopes it asked for, and calls approve or deny. **An app with no `mcp_login_url` is redirected to the hosted MCP sign-in** (`GET /app/:appIdentifier/mcp/:interaction`), which runs the same steps on the auth portal; nothing is refused for a missing URL. \n\nClient and `redirect_uri` are validated first and a failure there never redirects \u2014 the open-redirect discipline `GET /mcp/oauth/authorize` and `GET /api/client/oidc/:slug/start` both keep \u2014 and those refusals are RFC 6749\'s flat `oauthError`, which is why none of them appear above. `redirect_uri` is matched **exactly** against the registration, with no loopback-port wildcard: every client here registered itself minutes ago and can name the port it bound, so a wildcard would only widen where a stolen `client_id` may send a browser. \n\nThe code above is the `apiError` envelope because it is a refusal about the **app**, decided before an OAuth parameter is looked at. **`404 not_found` covers an identifier no app carries AND an app with MCP switched off** \u2014 the same single answer the two metadata documents, `register` and the transport give. An earlier draft answered `403 mcp_disabled` here, on the argument that a client which registered while the switch was on is owed the difference between "turned off" and "mistyped"; that argument does not survive the caller being anonymous. This route takes no credential, so the extra code was readable by anyone who could type an identifier, and it handed back precisely the existence distinction every neighbouring route collapses. `mcp_disabled` survives only where the caller has already proved they belong to the app \u2014 the two decision routes under `/api/client/mcp/interactions/:id`.'
|
|
6690
7199
|
},
|
|
6691
7200
|
{
|
|
6692
7201
|
method: "POST",
|
|
@@ -6720,10 +7229,46 @@ var ROUTES = [
|
|
|
6720
7229
|
params: [],
|
|
6721
7230
|
query: null,
|
|
6722
7231
|
request: clientLoginRequest,
|
|
6723
|
-
response:
|
|
6724
|
-
errors: ["rate_limited", "validation_error", "invalid_credentials"],
|
|
7232
|
+
response: clientSignInResult,
|
|
7233
|
+
errors: ["rate_limited", "validation_error", "invalid_credentials", "method_not_allowed"],
|
|
6725
7234
|
transport: "http",
|
|
6726
|
-
notes: 'One refusal for every miss \u2014 unknown app, unknown address, wrong password, a `blocked` account and one still `pending_verification` \u2014 because the caller supplies the `app_identifier` unauthenticated, so "this app knows this user" is not a fact the answer may carry. The argon2 verify is paid unconditionally, including for an unknown app identifier, so response time is not an oracle either.'
|
|
7235
|
+
notes: 'One refusal for every miss \u2014 unknown app, unknown address, wrong password, a `blocked` account and one still `pending_verification` \u2014 because the caller supplies the `app_identifier` unauthenticated, so "this app knows this user" is not a fact the answer may carry. The argon2 verify is paid unconditionally, including for an unknown app identifier, so response time is not an oracle either. \n\n**The answer is a `clientSignInResult`**: session tokens, or a `twoFactorChallenge` when the person has a confirmed authenticator or the app requires one \u2014 then no session exists until `POST /api/client/two-factor/verify` or the setup is done. `403 method_not_allowed` when the app has the password method off; it names the app\'s policy, not a person.'
|
|
7236
|
+
},
|
|
7237
|
+
{
|
|
7238
|
+
method: "POST",
|
|
7239
|
+
path: "/api/client/login/code",
|
|
7240
|
+
section: "client-auth",
|
|
7241
|
+
summary: "Mails a six-digit sign-in code, and answers the same whether or not the address exists.",
|
|
7242
|
+
audience: "client",
|
|
7243
|
+
auth: "none",
|
|
7244
|
+
rateLimited: true,
|
|
7245
|
+
ownerTier: false,
|
|
7246
|
+
status: 202,
|
|
7247
|
+
params: [],
|
|
7248
|
+
query: null,
|
|
7249
|
+
request: clientLoginCodeRequest,
|
|
7250
|
+
response: null,
|
|
7251
|
+
errors: ["rate_limited", "validation_error", "not_found", "method_not_allowed"],
|
|
7252
|
+
transport: "http",
|
|
7253
|
+
notes: "**`202` and an empty body for every request the policy allows**, in status, body and timing, whether or not the address names an active account of this app \u2014 a decoy like `POST /api/client/resend-verification`, so this is no enumeration oracle. A mail goes out for an `active` account and for one still `pending_verification` \u2014 spending the code proves the address, as the verification link would \u2014 and never for a `blocked` one or an unknown address. The code is six digits, valid ten minutes, takes five wrong attempts, and a new request expires the previous one for the same address; a request within sixty seconds of the last sends no second mail. The address is trimmed and compared case-insensitively. `404 not_found` is the **app identifier**, never the address; `403 method_not_allowed` when the app has the email-code method off. Limited per app, address and IP, so it cannot be used to mail somebody repeatedly."
|
|
7254
|
+
},
|
|
7255
|
+
{
|
|
7256
|
+
method: "POST",
|
|
7257
|
+
path: "/api/client/login/code/verify",
|
|
7258
|
+
section: "client-auth",
|
|
7259
|
+
summary: "Spends a mailed sign-in code and answers a session or a two-factor challenge.",
|
|
7260
|
+
audience: "client",
|
|
7261
|
+
auth: "none",
|
|
7262
|
+
rateLimited: true,
|
|
7263
|
+
ownerTier: false,
|
|
7264
|
+
status: 200,
|
|
7265
|
+
params: [],
|
|
7266
|
+
query: null,
|
|
7267
|
+
request: clientLoginCodeVerifyRequest,
|
|
7268
|
+
response: clientSignInResult,
|
|
7269
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent", "method_not_allowed"],
|
|
7270
|
+
transport: "http",
|
|
7271
|
+
notes: "A wrong code is `400 invalid_code` with `details.attempts_left` (`invalidCodeDetails`). A code that is spent, past its ten minutes, out of attempts, or was never mailed is `410 token_spent` \u2014 one answer, because telling them apart would say whether a code was ever sent to that address; the recovery is the same, ask for a new code. The address is trimmed and compared case-insensitively, so the address typed at the request and here need not match in case. \n\n**The answer is a `clientSignInResult`**, like the password login: tokens, or a `twoFactorChallenge` when the person has an authenticator or the app requires one. A pending-verification account that spends a code is activated \u2014 reading a mail at that address is the proof verification asks for."
|
|
6727
7272
|
},
|
|
6728
7273
|
{
|
|
6729
7274
|
method: "POST",
|
|
@@ -6741,7 +7286,7 @@ var ROUTES = [
|
|
|
6741
7286
|
response: null,
|
|
6742
7287
|
errors: ["rate_limited", "validation_error", "not_found", "registration_closed", "domain_not_allowed", "target_state_conflict", "quota_exceeded"],
|
|
6743
7288
|
transport: "http",
|
|
6744
|
-
notes: "**`202` and an empty body for every request policy allows** \u2014 a new address, one this app already knows and one it does not answer identically, in status, body and timing. An answer that depended on existence would be the account-enumeration oracle the whole client family is built to avoid. The account cannot log in until the mailed link is spent; `POST /api/client/verify-email` is what does that. \n\n**An address on an account still `pending_verification` is re-registered, not ignored.** The password and display name from this call replace what is stored, every outstanding verification link for the address stops working, and a fresh one is mailed. Otherwise whoever typed an address first would own the password of the account its real owner later verifies. An address on an `active` account changes nothing and sends nothing \u2014 that account has already been proven, and its way back in is `POST /api/client/password/reset`. Neither case is visible in the answer. \n\nThe refusals it *does* make are about policy or about what the caller typed, never about a person. `403 registration_closed` when the app has self-registration off and `403 domain_not_allowed` when the address is outside `allowed_domains`: both are the developer's own configuration, and a stranger learns the app's policy rather than who is in it. **A password under twelve characters is part of that `400 validation_error`** and not a code of its own \u2014 the minimum is the `password` field's schema rule, and the error names the field, which is what a form needs to mark it. `404 not_found` names an **app identifier no app carries**, and never an address: an app identifier is already public (it is in the MCP metadata path and in the developer's own URLs), while collapsing it into `registration_closed` sent a developer who mistyped their own identifier hunting a configuration bug that was not there. `409 target_state_conflict` when the app has
|
|
7289
|
+
notes: "**`202` and an empty body for every request policy allows** \u2014 a new address, one this app already knows and one it does not answer identically, in status, body and timing. An answer that depended on existence would be the account-enumeration oracle the whole client family is built to avoid. The account cannot log in until the mailed link is spent; `POST /api/client/verify-email` is what does that. \n\n**An address on an account still `pending_verification` is re-registered, not ignored.** The password and display name from this call replace what is stored, every outstanding verification link for the address stops working, and a fresh one is mailed. Otherwise whoever typed an address first would own the password of the account its real owner later verifies. An address on an `active` account changes nothing and sends nothing \u2014 that account has already been proven, and its way back in is `POST /api/client/password/reset`. Neither case is visible in the answer. \n\nThe refusals it *does* make are about policy or about what the caller typed, never about a person. `403 registration_closed` when the app has self-registration off and `403 domain_not_allowed` when the address is outside `allowed_domains`: both are the developer's own configuration, and a stranger learns the app's policy rather than who is in it. **A password under twelve characters is part of that `400 validation_error`** and not a code of its own \u2014 the minimum is the `password` field's schema rule, and the error names the field, which is what a form needs to mark it. The same `400` names `password` when one is missing while the app's password method is on, or sent while it is off: an email-code-only app registers people without one. `404 not_found` names an **app identifier no app carries**, and never an address: an app identifier is already public (it is in the MCP metadata path and in the developer's own URLs), while collapsing it into `registration_closed` sent a developer who mistyped their own identifier hunting a configuration bug that was not there. `409 target_state_conflict` when the app has no default role \u2014 there would be no role to give the person. An app with no `verify_url` is not refused: the mailed link points at the hosted confirmation page instead. \n\n**`409 quota_exceeded` when the org is at its `max_end_users` limit**, counted across every app of the org. It is the one refusal here that is answered **before the address is looked at** \u2014 and that ordering is the point rather than an implementation detail: a quota checked after the existence branch would answer `202` for an address the app already knows and `409` for one it does not, which is precisely the enumeration oracle every other line of this route exists to close. At the quota, every registration is refused identically, including one that would only have re-mailed a pending account's link."
|
|
6745
7290
|
},
|
|
6746
7291
|
{
|
|
6747
7292
|
method: "POST",
|
|
@@ -6756,10 +7301,10 @@ var ROUTES = [
|
|
|
6756
7301
|
params: [],
|
|
6757
7302
|
query: null,
|
|
6758
7303
|
request: clientVerifyEmailRequest,
|
|
6759
|
-
response:
|
|
7304
|
+
response: clientSignInResult,
|
|
6760
7305
|
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
6761
7306
|
transport: "http",
|
|
6762
|
-
notes: "**The answer is a session, not a `204
|
|
7307
|
+
notes: "**The answer is a session, not a `204`** \u2014 or, as on every sign-in step, a `twoFactorChallenge` when the app requires two-factor (`clientSignInResult`). Somebody who has just proved they can read the mail should not be asked to type their password again on the next screen, and the app has an access token to carry them into it. The token is spent first and the account is activated second, as **two writes**: the spend is the atomic one, so a link opened twice cannot mint two sessions, but a process that died between them would leave a spent token on an account still `pending_verification`, whose recovery is `POST /api/client/resend-verification`. Spending the token also proves the address, so a later `PATCH` may return the account to `active` after a block. \n\n**One refusal for every token that does not work: `410 token_spent`** \u2014 unknown, past its twenty-four hours, or already used. There is one code because distinguishing them would tell a stranger whether a token ever existed, and because the recovery is the same in all three cases: ask for a fresh link with `POST /api/client/resend-verification`. An app rendering this refusal should offer that and nothing conditional on which of the three it was."
|
|
6763
7308
|
},
|
|
6764
7309
|
{
|
|
6765
7310
|
method: "POST",
|
|
@@ -6793,9 +7338,9 @@ var ROUTES = [
|
|
|
6793
7338
|
query: null,
|
|
6794
7339
|
request: clientPasswordResetRequest,
|
|
6795
7340
|
response: null,
|
|
6796
|
-
errors: ["rate_limited", "validation_error", "not_found"],
|
|
7341
|
+
errors: ["rate_limited", "validation_error", "not_found", "method_not_allowed"],
|
|
6797
7342
|
transport: "http",
|
|
6798
|
-
notes: "
|
|
7343
|
+
notes: "The pair of app identifier and address is the identifier: an app user's address is unique only within their app. `403 method_not_allowed` when the app has the password method off \u2014 a reset link whose confirmation would be refused is not mailed; the code names the app's policy, not a person. Status, body and timing are identical for a known and an unknown address. An account with no password \u2014 one created through an identity provider \u2014 is mailed nothing and still answers `202`. `404 not_found` is the **app identifier**, never the address. The link points at the app's `reset_url`, or at the hosted reset page when the app has configured none."
|
|
6799
7344
|
},
|
|
6800
7345
|
{
|
|
6801
7346
|
method: "POST",
|
|
@@ -6810,10 +7355,10 @@ var ROUTES = [
|
|
|
6810
7355
|
params: [],
|
|
6811
7356
|
query: null,
|
|
6812
7357
|
request: clientPasswordResetConfirmRequest,
|
|
6813
|
-
response:
|
|
6814
|
-
errors: ["rate_limited", "validation_error", "token_spent"],
|
|
7358
|
+
response: clientSignInResult,
|
|
7359
|
+
errors: ["rate_limited", "validation_error", "token_spent", "method_not_allowed"],
|
|
6815
7360
|
transport: "http",
|
|
6816
|
-
notes: "**Every refresh family of that account is revoked**, then a fresh pair is minted for the caller \u2014 a forgotten password is one of the two states where somebody else may be holding a live session, and the person completing the reset is the one who should keep theirs. The account is activated if it was still `pending_verification`: reading a mail at that address is the same proof verification asks for. \n\n**One refusal for every token that does not work: `410 token_spent`** \u2014 unknown, past its hour, or already used. There is one code because distinguishing them would tell a stranger whether a token ever existed, and the recovery is identical either way: ask for a new link. A replacement password under twelve characters is a `400 validation_error` naming the `new_password` field \u2014 the twelve-character minimum is that field's schema rule, and it is refused the way any other malformed field is."
|
|
7361
|
+
notes: "**A new password does not bypass the second factor**: a person with an authenticator, or in an app that requires one, gets a `twoFactorChallenge` instead of tokens (`clientSignInResult`), and the authenticator stays on. `403 method_not_allowed` when the app has the password method off. \n\n**Every refresh family of that account is revoked**, then a fresh pair is minted for the caller \u2014 a forgotten password is one of the two states where somebody else may be holding a live session, and the person completing the reset is the one who should keep theirs. The account is activated if it was still `pending_verification`: reading a mail at that address is the same proof verification asks for. \n\n**One refusal for every token that does not work: `410 token_spent`** \u2014 unknown, past its hour, or already used. There is one code because distinguishing them would tell a stranger whether a token ever existed, and the recovery is identical either way: ask for a new link. A replacement password under twelve characters is a `400 validation_error` naming the `new_password` field \u2014 the twelve-character minimum is that field's schema rule, and it is refused the way any other malformed field is."
|
|
6817
7362
|
},
|
|
6818
7363
|
{
|
|
6819
7364
|
method: "POST",
|
|
@@ -6828,10 +7373,10 @@ var ROUTES = [
|
|
|
6828
7373
|
params: [],
|
|
6829
7374
|
query: null,
|
|
6830
7375
|
request: clientAcceptInvitationRequest,
|
|
6831
|
-
response:
|
|
7376
|
+
response: clientSignInResult,
|
|
6832
7377
|
errors: ["rate_limited", "validation_error", "token_spent", "email_taken", "target_state_conflict", "quota_exceeded"],
|
|
6833
7378
|
transport: "http",
|
|
6834
|
-
notes: "**An app invitation, not a team one.** `POST /api/org/invitations/accept` is the other space and answers `204`; this one answers a session, because the person is landing in the developer's app and there is no second door for them to sign in through. The role is the one the invitation fixed at creation, so a later change to the app's default role does not re-aim a link already in somebody's inbox, and the invitation **bypasses `allowed_domains`** \u2014 a developer inviting somebody by hand has already made the decision the whitelist automates. \n\n**One refusal for every token that does not work: `410 token_spent`** \u2014 unknown, expired past the seven days, revoked by the developer, or already accepted. There is one code because telling them apart would say whether a token ever existed, and because the one thing the holder of a dead link can do is ask the developer for a new one, whichever of the four it was. A chosen password under twelve characters is part of the `400 validation_error`, naming the `password` field. `409 email_taken` is an address this app has acquired since the invitation was written **as an account that is already in use** \u2014 the invitation stays outstanding rather than being spent, so the developer can revoke it or point the person at the login. An address that registered itself and is still `pending_verification` is not that state: accepting sets the password the invitee just chose, activates the account and gives it the invitation's role, because reading the invitation mail proves the address the verification link was waiting on. \n\n`409 target_state_conflict` names `role_id` with rule `not_set` when the role the invitation was fixed to has since been deleted and the app has no default role to fall back on: there is no access to hand the acceptor, and creating an account with none would be worse than saying so. \n\n**`409 quota_exceeded` when accepting would CREATE an account and the org is at its `max_end_users` limit**, counted across every app of the org. An invitation that names a row the developer already created, and one whose address is held by an unfinished self-registration, both finish an account that already counts \u2014 those are not refused, because the org is not one account larger afterwards. The token is not spent by the refusal: the developer can raise the limit, or delete somebody, and the same link still works."
|
|
7379
|
+
notes: "**An app invitation, not a team one.** `POST /api/org/invitations/accept` is the other space and answers `204`; this one answers a session, because the person is landing in the developer's app and there is no second door for them to sign in through. The role is the one the invitation fixed at creation, so a later change to the app's default role does not re-aim a link already in somebody's inbox, and the invitation **bypasses `allowed_domains`** \u2014 a developer inviting somebody by hand has already made the decision the whitelist automates. The answer is a `clientSignInResult`: a `twoFactorChallenge` instead of tokens when the app requires two-factor. `password` is required while the app's password method is on and refused while it is off, both as `400 validation_error` naming the field. \n\n**One refusal for every token that does not work: `410 token_spent`** \u2014 unknown, expired past the seven days, revoked by the developer, or already accepted. There is one code because telling them apart would say whether a token ever existed, and because the one thing the holder of a dead link can do is ask the developer for a new one, whichever of the four it was. A chosen password under twelve characters is part of the `400 validation_error`, naming the `password` field. `409 email_taken` is an address this app has acquired since the invitation was written **as an account that is already in use** \u2014 the invitation stays outstanding rather than being spent, so the developer can revoke it or point the person at the login. An address that registered itself and is still `pending_verification` is not that state: accepting sets the password the invitee just chose, activates the account and gives it the invitation's role, because reading the invitation mail proves the address the verification link was waiting on. \n\n`409 target_state_conflict` names `role_id` with rule `not_set` when the role the invitation was fixed to has since been deleted and the app has no default role to fall back on: there is no access to hand the acceptor, and creating an account with none would be worse than saying so. \n\n**`409 quota_exceeded` when accepting would CREATE an account and the org is at its `max_end_users` limit**, counted across every app of the org. An invitation that names a row the developer already created, and one whose address is held by an unfinished self-registration, both finish an account that already counts \u2014 those are not refused, because the org is not one account larger afterwards. The token is not spent by the refusal: the developer can raise the limit, or delete somebody, and the same link still works."
|
|
6835
7380
|
},
|
|
6836
7381
|
{
|
|
6837
7382
|
method: "POST",
|
|
@@ -6883,9 +7428,9 @@ var ROUTES = [
|
|
|
6883
7428
|
query: null,
|
|
6884
7429
|
request: passwordChangeRequest,
|
|
6885
7430
|
response: sessionTokens,
|
|
6886
|
-
errors: [...CLIENT_GUARD, "validation_error", "invalid_credentials", "target_state_conflict"],
|
|
7431
|
+
errors: [...CLIENT_GUARD, "validation_error", "invalid_credentials", "target_state_conflict", "method_not_allowed"],
|
|
6887
7432
|
transport: "http",
|
|
6888
|
-
notes: 'The guard admits all three caller kinds, but a password belongs to an app user specifically \u2014 a developer bearer or a server key reaching this is `401 unauthorized`. Every other session of the account ends; the answer is the replacement pair, so the tab that made the change stays signed in. An app user belongs to one app, so "every session" is this app\'s. An account that has **no password** \u2014 an OIDC-only app user, which the schema admits \u2014 answers `409 target_state_conflict` naming the `password` field with rule `not_set`, not `401`: the session is live and the token is fine, it is the account that has nothing to change, and telling such a caller to sign in again sends them round a loop that ends here.'
|
|
7433
|
+
notes: '`403 method_not_allowed` when the app has the password method off: a stored password stays stored but is not in use, so it is not changed either. The guard admits all three caller kinds, but a password belongs to an app user specifically \u2014 a developer bearer or a server key reaching this is `401 unauthorized`. Every other session of the account ends; the answer is the replacement pair, so the tab that made the change stays signed in. An app user belongs to one app, so "every session" is this app\'s. An account that has **no password** \u2014 an OIDC-only app user, which the schema admits \u2014 answers `409 target_state_conflict` naming the `password` field with rule `not_set`, not `401`: the session is live and the token is fine, it is the account that has nothing to change, and telling such a caller to sign in again sends them round a loop that ends here.'
|
|
6889
7434
|
},
|
|
6890
7435
|
{
|
|
6891
7436
|
method: "GET",
|
|
@@ -6905,6 +7450,80 @@ var ROUTES = [
|
|
|
6905
7450
|
transport: "http",
|
|
6906
7451
|
notes: "The one route that answers for all three caller kinds \u2014 a developer bearer, an app-user bearer and a server key \u2014 which is why the shape names each of `developer_id`, `app_user_id` and `server_key_id` and fills exactly one."
|
|
6907
7452
|
},
|
|
7453
|
+
/* ------------------------------------------------ app-user two-factor */
|
|
7454
|
+
{
|
|
7455
|
+
method: "POST",
|
|
7456
|
+
path: "/api/client/two-factor/verify",
|
|
7457
|
+
section: "client-auth",
|
|
7458
|
+
summary: "Answers a two-factor challenge with an authenticator or recovery code, and answers the session.",
|
|
7459
|
+
audience: "client",
|
|
7460
|
+
auth: "none",
|
|
7461
|
+
rateLimited: true,
|
|
7462
|
+
ownerTier: false,
|
|
7463
|
+
status: 200,
|
|
7464
|
+
params: [],
|
|
7465
|
+
query: null,
|
|
7466
|
+
request: clientTwoFactorVerifyRequest,
|
|
7467
|
+
response: sessionTokens,
|
|
7468
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent"],
|
|
7469
|
+
transport: "http",
|
|
7470
|
+
notes: "The challenge is the one a sign-in step answered with `two_factor_required`; it lives five minutes and takes five wrong codes, after which it is `410 token_spent` and the sign-in starts over. A wrong code is `400 invalid_code` with `details.attempts_left`. **A code is accepted at most once**: the same authenticator code sent twice, even at the same moment, signs in exactly once. A recovery code is spent by its use and audited as `app_user.recovery_code_used`. Exactly one of `code` and `recovery_code`, or `400 validation_error`."
|
|
7471
|
+
},
|
|
7472
|
+
{
|
|
7473
|
+
method: "POST",
|
|
7474
|
+
path: "/api/client/two-factor/setup",
|
|
7475
|
+
section: "client-auth",
|
|
7476
|
+
summary: "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
7477
|
+
audience: "client",
|
|
7478
|
+
auth: "in_handler",
|
|
7479
|
+
rateLimited: true,
|
|
7480
|
+
ownerTier: false,
|
|
7481
|
+
status: 200,
|
|
7482
|
+
params: [],
|
|
7483
|
+
query: null,
|
|
7484
|
+
request: clientTwoFactorSetupRequest,
|
|
7485
|
+
requestOptional: true,
|
|
7486
|
+
response: twoFactorSetupResponse,
|
|
7487
|
+
errors: ["rate_limited", "validation_error", "token_spent", "unauthorized", "target_state_conflict"],
|
|
7488
|
+
transport: "http",
|
|
7489
|
+
notes: "**Two ways in, decided in the handler.** During sign-in the body carries the `two_factor_setup_required` challenge, and that is the credential; from the app's own account settings the app user's bearer is, with no challenge and an empty or missing body. Neither is `401 unauthorized`, and a dead challenge is `410 token_spent`. `409 target_state_conflict` names `two_factor` with rule `off` when the app's policy is `off`. The secret is not in use until `POST /api/client/two-factor/setup/confirm` accepts a code from it; a second call replaces a pending secret, and an account that already has an authenticator keeps it until the new one is confirmed."
|
|
7490
|
+
},
|
|
7491
|
+
{
|
|
7492
|
+
method: "POST",
|
|
7493
|
+
path: "/api/client/two-factor/setup/confirm",
|
|
7494
|
+
section: "client-auth",
|
|
7495
|
+
summary: "Confirms the new authenticator with a code and answers the recovery codes and a session.",
|
|
7496
|
+
audience: "client",
|
|
7497
|
+
auth: "in_handler",
|
|
7498
|
+
rateLimited: true,
|
|
7499
|
+
ownerTier: false,
|
|
7500
|
+
status: 200,
|
|
7501
|
+
params: [],
|
|
7502
|
+
query: null,
|
|
7503
|
+
request: clientTwoFactorSetupConfirmRequest,
|
|
7504
|
+
response: clientTwoFactorSetupConfirmResponse,
|
|
7505
|
+
errors: ["rate_limited", "validation_error", "invalid_code", "token_spent", "unauthorized"],
|
|
7506
|
+
transport: "http",
|
|
7507
|
+
notes: "The same two ways in as `setup`. A code that does not match the pending secret is `400 invalid_code`; no pending setup, or a dead challenge, is `410 token_spent`. On success the authenticator is on, ten recovery codes are issued \u2014 shown this once, any earlier set void \u2014 and the answer carries a session: the one the sign-in was waiting for, or, from account settings, a fresh one while every other session of the account ends. Audited as `app_user.two_factor_enabled`."
|
|
7508
|
+
},
|
|
7509
|
+
{
|
|
7510
|
+
method: "DELETE",
|
|
7511
|
+
path: "/api/client/two-factor",
|
|
7512
|
+
section: "client-auth",
|
|
7513
|
+
summary: "Turns the signed-in app user's authenticator off.",
|
|
7514
|
+
audience: "client",
|
|
7515
|
+
auth: "developer_or_client",
|
|
7516
|
+
rateLimited: true,
|
|
7517
|
+
ownerTier: false,
|
|
7518
|
+
status: 204,
|
|
7519
|
+
params: [],
|
|
7520
|
+
query: null,
|
|
7521
|
+
request: clientTwoFactorDisableRequest,
|
|
7522
|
+
response: null,
|
|
7523
|
+
errors: [...CLIENT_GUARD, "rate_limited", "validation_error", "invalid_code", "target_state_conflict"],
|
|
7524
|
+
transport: "http",
|
|
7525
|
+
notes: "The app user's own door; a developer bearer or a server key is `401 unauthorized`, because the factor is the person's. A current code proves they still hold the authenticator: a stolen session alone cannot remove it. The authenticator and every recovery code go. `409 target_state_conflict` names `two_factor` with rule `required` while the app requires two-factor, and with rule `off` when there is none to remove. Audited as `app_user.two_factor_disabled`. The developer's support door is `DELETE /api/apps/:id/users/:userId/two-factor`."
|
|
7526
|
+
},
|
|
6908
7527
|
/* ---------------------------------------- app-user sign-in through an IdP */
|
|
6909
7528
|
{
|
|
6910
7529
|
method: "GET",
|
|
@@ -6958,7 +7577,7 @@ var ROUTES = [
|
|
|
6958
7577
|
response: null,
|
|
6959
7578
|
errors: ["rate_limited"],
|
|
6960
7579
|
transport: "http",
|
|
6961
|
-
notes: "**One callback URL for every app and every provider**, and the value of `appAuthConfig.oidc_callback_url` \u2014 the string a developer registers at their IdP. `CLIENT_OIDC_CALLBACK_PATH` in `client-auth.ts` is the single spelling of this path; the URL is that path on the cloud's canonical public base, never a friendly alias, because the provider compares the redirect target against the one string it was given. \n\n**Rate limited per ip, generously.** The first draft left this route unlimited on the argument that the caller is an identity provider redirecting somebody's browser, so a limiter would drop real sign-ins on the strength of traffic none of those people sent. The cost of that argument is a `state` obtained from one `start` being replayable for the interaction's full ten minutes, unbounded and unauthenticated, with every replay driving a server-side POST to the developer's token endpoint and one audit row into their org \u2014 an amplifier against a third party. The interaction is now spent on **every** terminal outcome, refusals included, which closes the replay itself; the limiter is the ceiling on how fast the attempts may arrive at all. It is per ip and sized for a browser, so a person completing a sign-in never meets it. `429 rate_limited` is the one `apiError` this route can answer, and it is not a sign-in outcome \u2014 it is a refusal to begin the work, which is why it does not ride back to the app as an `?error=`. \n\nThe query is the **provider's** rather than a Fleetless shape, and `clientOidcCallbackQuery` describes it **without being strict**: `state` always, `code` on success, `error` and `error_description` on the provider's own refusal, and whatever else that provider adds \u2014 RFC 9207's `iss`, a `session_state`, a vendor field. Refusing those would refuse conforming providers, the trap `POST /mcp/oauth/register` documents avoiding. `state` is the required field because it is the only one Fleetless minted. \n\n**It lists `rate_limited` and no other code, because every sign-in outcome it has is a redirect.** Success and failure alike are a `302` to the app's own `redirect_uri`: `?code=\u2026&state=\u2026` when a session was resolved, `?error=<clientOidcErrorCode>&state=\u2026` when it was not, so the app renders its own message and can bind either answer to the request it started.
|
|
7580
|
+
notes: "**One callback URL for every app and every provider**, and the value of `appAuthConfig.oidc_callback_url` \u2014 the string a developer registers at their IdP. `CLIENT_OIDC_CALLBACK_PATH` in `client-auth.ts` is the single spelling of this path; the URL is that path on the cloud's canonical public base, never a friendly alias, because the provider compares the redirect target against the one string it was given. \n\n**Rate limited per ip, generously.** The first draft left this route unlimited on the argument that the caller is an identity provider redirecting somebody's browser, so a limiter would drop real sign-ins on the strength of traffic none of those people sent. The cost of that argument is a `state` obtained from one `start` being replayable for the interaction's full ten minutes, unbounded and unauthenticated, with every replay driving a server-side POST to the developer's token endpoint and one audit row into their org \u2014 an amplifier against a third party. The interaction is now spent on **every** terminal outcome, refusals included, which closes the replay itself; the limiter is the ceiling on how fast the attempts may arrive at all. It is per ip and sized for a browser, so a person completing a sign-in never meets it. `429 rate_limited` is the one `apiError` this route can answer, and it is not a sign-in outcome \u2014 it is a refusal to begin the work, which is why it does not ride back to the app as an `?error=`. \n\nThe query is the **provider's** rather than a Fleetless shape, and `clientOidcCallbackQuery` describes it **without being strict**: `state` always, `code` on success, `error` and `error_description` on the provider's own refusal, and whatever else that provider adds \u2014 RFC 9207's `iss`, a `session_state`, a vendor field. Refusing those would refuse conforming providers, the trap `POST /mcp/oauth/register` documents avoiding. `state` is the required field because it is the only one Fleetless minted. \n\n**It lists `rate_limited` and no other code, because every sign-in outcome it has is a redirect.** Success and failure alike are a `302` to the app's own `redirect_uri`: `?code=\u2026&state=\u2026` when a session was resolved, `?error=<clientOidcErrorCode>&state=\u2026` when it was not, so the app renders its own message and can bind either answer to the request it started. This route renders no page for an outcome. \n\n**The one exception is a `state` that resolves to no interaction** \u2014 unknown, hand-edited, or past its ten minutes. Then there is no confirmed redirect target to carry the answer to, and bouncing a browser to an unvalidated one is the hole the whole flow is arranged to avoid, so the cloud renders an HTML problem page at `400`. It is HTML rather than an `apiError`, which is why no code is listed: a code here would document an envelope no caller receives, and this manifest's other HTML pages (`GET /mcp/oauth/interaction/:id`, `GET /console/oauth/interaction/:id`) say their status in prose for the same reason."
|
|
6962
7581
|
},
|
|
6963
7582
|
{
|
|
6964
7583
|
method: "POST",
|
|
@@ -8149,6 +8768,24 @@ var ROUTES = [
|
|
|
8149
8768
|
transport: "http",
|
|
8150
8769
|
notes: "A window longer than `USAGE_WINDOW_MAX_DAYS` is refused naming the field, not silently capped: a caller who asked for more than the platform will answer is owed a refusal, not a shorter answer they will mistake for the whole picture. `from_day <= to_day` is a cross-field rule no JSON Schema can express and is enforced here. The window is echoed back."
|
|
8151
8770
|
},
|
|
8771
|
+
{
|
|
8772
|
+
method: "POST",
|
|
8773
|
+
path: "/api/feedback",
|
|
8774
|
+
section: "org",
|
|
8775
|
+
summary: "Sends a message from a developer to the people who build Fleetless.",
|
|
8776
|
+
audience: "developer",
|
|
8777
|
+
auth: "developer",
|
|
8778
|
+
rateLimited: true,
|
|
8779
|
+
ownerTier: false,
|
|
8780
|
+
status: 202,
|
|
8781
|
+
params: [],
|
|
8782
|
+
query: null,
|
|
8783
|
+
request: feedbackRequest,
|
|
8784
|
+
response: feedbackResponse,
|
|
8785
|
+
errors: [...DEVELOPER_GUARD, "validation_error", "rate_limited"],
|
|
8786
|
+
transport: "http",
|
|
8787
|
+
notes: "The message is stored before any mail is tried, so `202` means it is kept whatever `mail` says: `sent`, `failed`, or `not_configured` when this cloud has no feedback address. At most 10 messages per developer per hour; the 11th answers `429 rate_limited` with `retry_after_ms`. Replies come by mail, to the sender's address."
|
|
8788
|
+
},
|
|
8152
8789
|
/* ------------------------------------------------- assets (robot upload) */
|
|
8153
8790
|
{
|
|
8154
8791
|
method: "POST",
|
|
@@ -8291,13 +8928,19 @@ var ServerKeyCredentials = class {
|
|
|
8291
8928
|
function displayNameField(displayName) {
|
|
8292
8929
|
return displayName === void 0 ? {} : { display_name: displayName };
|
|
8293
8930
|
}
|
|
8931
|
+
function passwordField(password2) {
|
|
8932
|
+
return password2 === void 0 ? {} : { password: password2 };
|
|
8933
|
+
}
|
|
8934
|
+
function challengeField(challenge) {
|
|
8935
|
+
return challenge === void 0 ? {} : { challenge };
|
|
8936
|
+
}
|
|
8294
8937
|
function createPublicAuthCalls(http, appIdentifier2) {
|
|
8295
8938
|
return {
|
|
8296
8939
|
async register(input) {
|
|
8297
8940
|
const body = {
|
|
8298
8941
|
app_identifier: appIdentifier2,
|
|
8299
8942
|
email: input.email,
|
|
8300
|
-
|
|
8943
|
+
...passwordField(input.password),
|
|
8301
8944
|
...displayNameField(input.displayName)
|
|
8302
8945
|
};
|
|
8303
8946
|
await http.request("/api/client/register", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
@@ -8309,6 +8952,10 @@ function createPublicAuthCalls(http, appIdentifier2) {
|
|
|
8309
8952
|
async requestPasswordReset(email) {
|
|
8310
8953
|
const body = { app_identifier: appIdentifier2, email };
|
|
8311
8954
|
await http.request("/api/client/password/reset", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
8955
|
+
},
|
|
8956
|
+
async requestLoginCode(email) {
|
|
8957
|
+
const body = { app_identifier: appIdentifier2, email };
|
|
8958
|
+
await http.request("/api/client/login/code", { method: "POST", skipAuth: true, expectEmptyBody: true, body });
|
|
8312
8959
|
}
|
|
8313
8960
|
};
|
|
8314
8961
|
}
|
|
@@ -8316,6 +8963,11 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8316
8963
|
async function storeSession(tokens) {
|
|
8317
8964
|
await tokenStore.save(tokens);
|
|
8318
8965
|
}
|
|
8966
|
+
async function completeSignIn(result) {
|
|
8967
|
+
if ("status" in result) return { status: result.status, challenge: result.challenge };
|
|
8968
|
+
await storeSession(result);
|
|
8969
|
+
return { status: "signed_in" };
|
|
8970
|
+
}
|
|
8319
8971
|
async function identity() {
|
|
8320
8972
|
return http.request("/api/client/me", {});
|
|
8321
8973
|
}
|
|
@@ -8323,13 +8975,18 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8323
8975
|
...createPublicAuthCalls(http, appIdentifier2),
|
|
8324
8976
|
async verifyEmail(token) {
|
|
8325
8977
|
const body = { token };
|
|
8326
|
-
const
|
|
8327
|
-
|
|
8978
|
+
const result = await http.request("/api/client/verify-email", { method: "POST", skipAuth: true, body });
|
|
8979
|
+
return completeSignIn(result);
|
|
8328
8980
|
},
|
|
8329
8981
|
async login(email, password2) {
|
|
8330
8982
|
const body = { app_identifier: appIdentifier2, email, password: password2 };
|
|
8331
|
-
const
|
|
8332
|
-
|
|
8983
|
+
const result = await http.request("/api/client/login", { method: "POST", skipAuth: true, body });
|
|
8984
|
+
return completeSignIn(result);
|
|
8985
|
+
},
|
|
8986
|
+
async verifyLoginCode(email, code) {
|
|
8987
|
+
const body = { app_identifier: appIdentifier2, email, code };
|
|
8988
|
+
const result = await http.request("/api/client/login/code/verify", { method: "POST", skipAuth: true, body });
|
|
8989
|
+
return completeSignIn(result);
|
|
8333
8990
|
},
|
|
8334
8991
|
async logout() {
|
|
8335
8992
|
const session = await tokenStore.load();
|
|
@@ -8343,6 +9000,13 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8343
9000
|
await tokenStore.save(null);
|
|
8344
9001
|
},
|
|
8345
9002
|
async me() {
|
|
9003
|
+
const session = await tokenStore.load();
|
|
9004
|
+
if (!session) {
|
|
9005
|
+
throw new FleetlessError(
|
|
9006
|
+
"no_session",
|
|
9007
|
+
"auth.me() has no session to ask about \u2014 call login()/verifyEmail()/confirmPasswordReset()/acceptInvitation() and get back { status: 'signed_in' } first; a pending two-factor challenge does not count as signed in."
|
|
9008
|
+
);
|
|
9009
|
+
}
|
|
8346
9010
|
return identity();
|
|
8347
9011
|
},
|
|
8348
9012
|
async changePassword(currentPassword, newPassword) {
|
|
@@ -8352,23 +9016,61 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
8352
9016
|
},
|
|
8353
9017
|
async confirmPasswordReset(token, newPassword) {
|
|
8354
9018
|
const body = { token, new_password: newPassword };
|
|
8355
|
-
const
|
|
8356
|
-
|
|
9019
|
+
const result = await http.request("/api/client/password/reset/confirm", { method: "POST", skipAuth: true, body });
|
|
9020
|
+
return completeSignIn(result);
|
|
8357
9021
|
},
|
|
8358
9022
|
async acceptInvitation(input) {
|
|
8359
9023
|
const body = {
|
|
8360
9024
|
token: input.token,
|
|
8361
|
-
|
|
9025
|
+
...passwordField(input.password),
|
|
8362
9026
|
...displayNameField(input.displayName)
|
|
8363
9027
|
};
|
|
8364
|
-
const
|
|
9028
|
+
const result = await http.request("/api/client/invitations/accept", { method: "POST", skipAuth: true, body });
|
|
9029
|
+
return completeSignIn(result);
|
|
9030
|
+
},
|
|
9031
|
+
async verifyTwoFactor(input) {
|
|
9032
|
+
const body = {
|
|
9033
|
+
challenge: input.challenge,
|
|
9034
|
+
...input.code !== void 0 ? { code: input.code } : {},
|
|
9035
|
+
...input.recoveryCode !== void 0 ? { recovery_code: input.recoveryCode } : {}
|
|
9036
|
+
};
|
|
9037
|
+
const tokens = await http.request("/api/client/two-factor/verify", { method: "POST", skipAuth: true, body });
|
|
8365
9038
|
await storeSession(tokens);
|
|
8366
9039
|
},
|
|
9040
|
+
async beginTwoFactorSetup(input) {
|
|
9041
|
+
const challenge = input?.challenge;
|
|
9042
|
+
const body = challengeField(challenge);
|
|
9043
|
+
const response = await http.request("/api/client/two-factor/setup", {
|
|
9044
|
+
method: "POST",
|
|
9045
|
+
skipAuth: challenge !== void 0,
|
|
9046
|
+
body
|
|
9047
|
+
});
|
|
9048
|
+
return { secret: response.secret, otpauthUrl: response.otpauth_url };
|
|
9049
|
+
},
|
|
9050
|
+
async confirmTwoFactorSetup(input) {
|
|
9051
|
+
const body = { code: input.code, ...challengeField(input.challenge) };
|
|
9052
|
+
const response = await http.request("/api/client/two-factor/setup/confirm", {
|
|
9053
|
+
method: "POST",
|
|
9054
|
+
skipAuth: input.challenge !== void 0,
|
|
9055
|
+
body
|
|
9056
|
+
});
|
|
9057
|
+
await storeSession(response.session);
|
|
9058
|
+
return { recoveryCodes: response.recovery_codes };
|
|
9059
|
+
},
|
|
9060
|
+
async disableTwoFactor(code) {
|
|
9061
|
+
const body = { code };
|
|
9062
|
+
await http.request("/api/client/two-factor", { method: "DELETE", expectEmptyBody: true, body });
|
|
9063
|
+
},
|
|
8367
9064
|
async listProviders() {
|
|
8368
9065
|
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
8369
9066
|
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
8370
9067
|
return response.providers;
|
|
8371
9068
|
},
|
|
9069
|
+
async signInMethods() {
|
|
9070
|
+
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
9071
|
+
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
9072
|
+
return { password: response.sign_in_methods.password, emailCode: response.sign_in_methods.email_code };
|
|
9073
|
+
},
|
|
8372
9074
|
async beginOidcLogin(input) {
|
|
8373
9075
|
const state = generateState();
|
|
8374
9076
|
const codeVerifier = generateCodeVerifier();
|
|
@@ -8452,11 +9154,11 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8452
9154
|
const subject = option === "serverKey" ? "a server key" : "a supplied credential";
|
|
8453
9155
|
const holder = option === "serverKey" ? "a server-key client" : "a client with a supplied credential";
|
|
8454
9156
|
return {
|
|
8455
|
-
// **`register`, `resendVerification
|
|
8456
|
-
// allowed here** — see `createPublicAuthCalls`.
|
|
8457
|
-
// that name their own subject and answer
|
|
8458
|
-
// sign-up or forgot-password
|
|
8459
|
-
// already has.
|
|
9157
|
+
// **`register`, `resendVerification`, `requestPasswordReset` and
|
|
9158
|
+
// `requestLoginCode` are allowed here** — see `createPublicAuthCalls`.
|
|
9159
|
+
// They are public routes that name their own subject and answer
|
|
9160
|
+
// nothing, so a server-rendered sign-up or forgot-password (or
|
|
9161
|
+
// sign-in-by-code) page can use the one client its backend already has.
|
|
8460
9162
|
...createPublicAuthCalls(http, appIdentifier2),
|
|
8461
9163
|
async verifyEmail() {
|
|
8462
9164
|
serverKeyRefusal("verifyEmail", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
@@ -8464,6 +9166,9 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8464
9166
|
async login() {
|
|
8465
9167
|
serverKeyRefusal("login", `${subject} IS the credential; there is nothing to exchange`);
|
|
8466
9168
|
},
|
|
9169
|
+
async verifyLoginCode() {
|
|
9170
|
+
serverKeyRefusal("verifyLoginCode", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
9171
|
+
},
|
|
8467
9172
|
async logout() {
|
|
8468
9173
|
serverKeyRefusal("logout", `${subject} holds no session to end`);
|
|
8469
9174
|
},
|
|
@@ -8479,11 +9184,31 @@ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
|
8479
9184
|
async acceptInvitation() {
|
|
8480
9185
|
serverKeyRefusal("acceptInvitation", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
8481
9186
|
},
|
|
9187
|
+
async verifyTwoFactor() {
|
|
9188
|
+
serverKeyRefusal("verifyTwoFactor", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
9189
|
+
},
|
|
9190
|
+
async beginTwoFactorSetup() {
|
|
9191
|
+
serverKeyRefusal("beginTwoFactorSetup", `a two-factor setup is a person's own account's, and ${subject} is not a person's session`);
|
|
9192
|
+
},
|
|
9193
|
+
async confirmTwoFactorSetup() {
|
|
9194
|
+
serverKeyRefusal(
|
|
9195
|
+
"confirmTwoFactorSetup",
|
|
9196
|
+
`a two-factor setup is a person's own account's, and ${holder} is not a person's session and has nowhere to store the one this route answers with`
|
|
9197
|
+
);
|
|
9198
|
+
},
|
|
9199
|
+
async disableTwoFactor() {
|
|
9200
|
+
serverKeyRefusal("disableTwoFactor", `turning an authenticator off is a person's own decision, and ${subject} is not a person`);
|
|
9201
|
+
},
|
|
8482
9202
|
async listProviders() {
|
|
8483
9203
|
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
8484
9204
|
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
8485
9205
|
return response.providers;
|
|
8486
9206
|
},
|
|
9207
|
+
async signInMethods() {
|
|
9208
|
+
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
9209
|
+
const response = await http.request(`/api/client/providers?${query.toString()}`, { skipAuth: true });
|
|
9210
|
+
return { password: response.sign_in_methods.password, emailCode: response.sign_in_methods.email_code };
|
|
9211
|
+
},
|
|
8487
9212
|
async beginOidcLogin() {
|
|
8488
9213
|
serverKeyRefusal("beginOidcLogin", "a federated sign-in is inherently an app user's browser flow");
|
|
8489
9214
|
},
|